# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gettext import gettext as _
import gi

gi.require_version("Adw", "1")
gi.require_version("GtkSource", "5")
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, GtkSource

import logging
from typing import Callable, Optional

from iotas.attachment_helpers import flush_all_attachments_on_disk
from iotas.backup_manager import BackupManager
import iotas.const as const
from iotas.config_manager import ConfigManager
from iotas.database import Database, DbCursor
from iotas.migration_assistant import MigrationAssistant
from iotas.note_database import NoteDatabase
from iotas.preferences_dialog import PreferencesDialog
from iotas.nextcloud_sync_worker import NextcloudSyncWorker
from iotas.sync_manager import SyncManager
from iotas.widgets import load_widgets
from iotas.window import Window


class Application(Adw.Application):
    development_mode = const.IS_DEVEL
    application_id = const.APP_ID

    def __init__(self, *args) -> None:
        super().__init__(
            *args,
            application_id=self.application_id,
            flags=Gio.ApplicationFlags.HANDLES_OPEN,
            resource_base_path="/org/gnome/World/Iotas",
            register_session=True,
        )

        ConfigManager.set_app_id(const.APP_ID)
        self.__config_manager = ConfigManager.get_default()

        self.__add_cli_options()

        self.__index_category_label_css_provider: Gtk.CssProvider = None
        style_manager = Adw.StyleManager.get_default()
        style_manager.connect("notify::dark", lambda _o, _v: self.__apply_css())
        style_manager.connect("notify::high-contrast", lambda _o, _v: self.__apply_css())

        self.cursors: dict[str, DbCursor] = {}
        self.__actions: dict[str, Gio.SimpleAction] = {}
        self.__window: Optional[Adw.ApplicationWindow] = None
        self.__preferences_dialog: Optional[PreferencesDialog] = None
        self.__previous_version = ""

        self.connect("startup", lambda _o: self.__on_startup())
        self.connect("activate", lambda _o: self.__on_activate())
        self.connect("handle-local-options", self.__on_handle_local_options)

        self.__config_manager.connect_changed(
            ConfigManager.INDEX_CATEGORY_STYLE,
            self.__apply_index_category_label_css,
        )

    def disconnect_nextcloud(self) -> None:
        """Disconnect from Nextcloud instance and exit."""
        if not self.__sync_manager.authenticated:
            return
        assert self.__window  # mypy

        self.__sync_manager.sign_out()
        self.db.delete_all_clean_synced_notes()
        flush_all_attachments_on_disk()
        self.__config_manager.nextcloud_username = ""
        self.__window.close()

    def do_open(self, files: list[Gio.File], n_files: int, _hint: str) -> None:
        """Handle opening "files". In this case URI for the temporary handler.

        :param list[Gio.File] files: Files
        :param int n_files: Number of files
        :param str _hint: Hint
        """

        # Note: Iotas' addition of this URI handler is a stopgap measure until the freedesktop
        # intent system is fleshed out. The handling provided here will be unceremoniously removed
        # when that system is available and may be removed or modified without notice beforehand;
        # this is not a stable public interface.

        if n_files != 1:
            self.quit()
            return
        file = files[0]
        uri = GLib.Variant("s", file.get_uri())
        self.activate_action("create-note-from-uri", uri)

    def reset_sync_marker(self) -> None:
        """Resets the timestamp for transfer with Nextcloud Notes."""
        self.__sync_manager.reset_marker()

    def reset_database(self) -> None:
        """Destroy the database and quit."""
        self.db_base.trash()
        flush_all_attachments_on_disk()
        self.__config_manager.nextcloud_prune_threshold = 0
        self.quit()

    def apply_style(self) -> None:
        """Apply style preference."""
        manager = Adw.StyleManager.get_default()
        style = self.__config_manager.style
        if style == "dark":
            manager.props.color_scheme = Adw.ColorScheme.FORCE_DARK
        elif style == "light":
            manager.props.color_scheme = Adw.ColorScheme.FORCE_LIGHT
        else:
            manager.props.color_scheme = Adw.ColorScheme.DEFAULT

    @GObject.Property(type=bool, default=False)
    def debug_session(self) -> bool:
        return self.__debug_session

    @GObject.Property(type=str)
    def previous_version(self) -> str:
        return self.__previous_version

    @previous_version.setter
    def set_previous_version(self, value: str) -> None:
        self.__previous_version = value

    def __on_startup(self) -> None:
        """Handle startup."""
        Gtk.Application.do_startup(self)
        Adw.init()
        GtkSource.init()

        GtkSource.LanguageManager.get_default().prepend_search_path(
            "resource:///org/gnome/World/Iotas/gtksourceview-5/language-specs/"
        )
        GtkSource.StyleSchemeManager.get_default().prepend_search_path(
            "resource:///org/gnome/World/Iotas/gtksourceview-5/styles/"
        )

        devel = " (development build)" if const.IS_DEVEL else ""
        logging.info(f"Iotas v{const.VERSION}{devel}")

        self.db_base = Database()
        self.db = NoteDatabase(self.db_base)
        self.__sync_manager = SyncManager(self.db)
        self.__backup_manager = BackupManager(self.db, const.VERSION)

        self.previous_version = self.__config_manager.last_launched_version
        migration_assistant = MigrationAssistant(self.db)
        migration_assistant.migrate()

        load_widgets()
        self.__setup_actions()

    def __on_handle_local_options(self, _obj: GObject.Object, options: GLib.VariantDict) -> int:
        """Handle options, setup logging."""
        options = options.end().unpack()

        # Print paths before logging is setup and app initialised, for clean output
        if "display-backup-path" in options:
            print(BackupManager.get_default_primary_path())
            self.quit()
            return 0
        elif "display-ca-file-path" in options:
            print(NextcloudSyncWorker.CA_CHAIN_FILE)
            self.quit()
            return 0

        self.__debug_session = "debug-session" in options

        loglevel = logging.INFO
        if self.development_mode or self.__debug_session:
            loglevel = logging.DEBUG

            for module in ("urllib3", "gtkspellcheck", "markdown_it", "pypandoc"):
                logging.getLogger(module).setLevel(logging.INFO)

        logging.basicConfig(
            format="%(asctime)s | %(module)s | %(levelname)s | %(message)s",
            datefmt="%H:%M:%S",
            level=loglevel,
        )

        if "create-backup" in options:
            self.register()
            if self.get_property("is-remote"):
                self.activate_action("create-backup")
                logging.info("Requesting backup from already running Iotas")
                logging.info(
                    "Note: backup outcome will be logged from the other process; the return code "
                    "here doesn't reflect the backup result"
                )
                success = True
            else:
                success = self.__backup_manager.create_backup(
                    self.__config_manager.nextcloud_sync_configured,
                    self.__config_manager.backup_note_extension,
                )
            self.quit()
            return int(not success)
        elif "restore-backup" in options:
            self.register()
            if self.get_property("is-remote"):
                logging.error(
                    "Please quit the running instance of Iotas before restoring the backup"
                )
                self.quit()
                success = False
            else:
                success = self.__backup_manager.restore_backup(
                    self.__config_manager.nextcloud_sync_configured
                )
            self.quit()
            return int(not success)
        elif "quit-running" in options:
            self.register()
            if self.get_property("is-remote"):
                self.activate_action("quit")
            else:
                logging.warning("No running instance found")
            self.quit()
            return 0
        elif "new-note" in options:
            # Application needs to be registered to send action if remote instance
            self.register()
            self.activate_action("create-note")
        elif "open-note" in options:
            # Application needs to be registered to send action if remote instance
            self.register()
            note_id = GLib.Variant("u", options["open-note"])
            self.activate_action("open-note", note_id)
        elif "search" in options:
            # Application needs to be registered to send action if remote instance
            self.register()
            term = GLib.Variant("s", options["search"])
            self.activate_action("search-from-cli", term)
        elif "toggle-extended-preferences" in options:
            self.register()
            if self.__config_manager.show_extended_preferences:
                logging.info("Hiding extended preferences")
                self.__config_manager.show_extended_preferences = False
            else:
                logging.info("Showing extended preferences")
                self.__config_manager.show_extended_preferences = True
            self.quit()
            return 0

        # Let default option processing continue
        return -1

    def __on_activate(self) -> None:
        """Handle window activation."""
        if not self.__window:
            self.__create_window()
            assert self.__window  # mypy

        self.__apply_css()
        self.apply_style()
        self.__window.present()

    def __on_quit(self) -> None:
        if self.__window:
            self.__window.cleanup_and_close()

    def __on_create_note(self) -> None:
        self.activate()
        if self.__window:
            self.__window.activate_action("win.create-note-from-cli")

    def __on_create_note_from_uri(self, param: GObject.ParamSpec) -> None:
        self.activate()
        if self.__window:
            self.__window.activate_action("win.create-note-from-uri", param),

    def __on_open_note(self, param: GObject.ParamSpec) -> None:
        self.activate()
        if self.__window:
            self.__window.activate_action("win.open-note", param),

    def __on_search_from_cli(self, param: GObject.ParamSpec) -> None:
        self.activate()
        if self.__window:
            self.__window.activate_action("win.search-from-cli", param),

    def __add_cli_options(self) -> None:
        self.add_main_option(
            "new-note",
            ord("n"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Create a note"),
            None,
        )
        self.add_main_option(
            "create-backup",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Create a backup"),
            None,
        )
        self.add_main_option(
            "restore-backup",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Restore a backup"),
            None,
        )
        self.add_main_option(
            "display-backup-path",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Display backup path"),
            None,
        )
        self.add_main_option(
            "display-ca-file-path",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Display path for custom server SSL CA chain file"),
            None,
        )
        self.add_main_option(
            "toggle-extended-preferences",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Toggle display of extended preferences in UI"),
            None,
        )
        self.add_main_option(
            "quit-running",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Quit any running instance"),
            None,
        )
        self.add_main_option(
            "debug-session",
            ord("d"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            # Translators: Description, CLI option
            _("Enable debug logging and functions"),
            None,
        )
        self.add_main_option(
            "open-note",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.INT,
            # Translators: Description, CLI option
            _("Open note by id"),
            None,
        )
        self.add_main_option(
            "search",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            # Translators: Description, CLI option
            _("Search in notes"),
            None,
        )

    def __setup_actions(self) -> None:
        def add_action(
            name: str,
            method: Callable,
            shortcut: Optional[str] = None,
            parameter_type: Optional[GLib.VariantType] = None,
        ):
            if parameter_type is None:
                action = Gio.SimpleAction.new(name)
            else:
                action = Gio.SimpleAction.new(name, parameter_type)
            self.add_action(action)
            action.connect("activate", method)
            self.__actions[name] = action
            if shortcut is not None:
                self.set_accels_for_action(f"app.{name}", [shortcut])

        add_action("about", lambda _o, _p: self.__show_about_dialog())
        add_action("quit", lambda _o, _p: self.__on_quit(), "<Control>q")
        add_action("settings", lambda _o, _p: self.__show_preferences_dialog(), "<Control>comma")
        add_action(
            "create-note",
            lambda _o, _p: self.__on_create_note(),
        )
        add_action(
            "create-note-from-uri",
            lambda _o, param: self.__on_create_note_from_uri(param),
            None,
            GLib.VariantType("s"),
        )
        add_action(
            "open-note",
            lambda _o, param: self.__on_open_note(param),
            None,
            GLib.VariantType("u"),
        )
        add_action(
            "search-from-cli",
            lambda _o, param: self.__on_search_from_cli(param),
            None,
            GLib.VariantType("s"),
        )
        add_action("create-backup", lambda _o, _p: self.__create_backup())

    def __create_window(self) -> None:
        self.__window = Window(self, self.db, self.__sync_manager)
        self.add_window(self.__window)

    def __apply_css(self) -> None:
        self.__apply_index_category_label_css()

    def __show_about_dialog(self) -> None:
        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/World/Iotas/ui/about_dialog.ui")
        about_dialog = builder.get_object("about_dialog")
        about_dialog.set_application_icon(const.APP_ID)
        about_dialog.set_version(const.VERSION)
        about_dialog.set_developers(["Chris Heywood"])
        about_dialog.present(self.__window)

    def __show_preferences_dialog(self) -> None:
        if self.__preferences_dialog is not None:
            return

        self.__preferences_dialog = PreferencesDialog()
        self.__preferences_dialog.connect(
            "start-onboarding", self.__on_preferences_dialog_start_onboarding
        )
        self.__preferences_dialog.connect("closed", self.__on_preferences_dialog_closed)
        self.__preferences_dialog.present(self.__window)

    def __on_preferences_dialog_start_onboarding(self, dialog: PreferencesDialog) -> None:
        if not self.__preferences_dialog or not self.__window:  # mypy
            return
        self.__preferences_dialog.close()
        self.__window.show_login_dialog()

    def __on_preferences_dialog_closed(self, _dialog: GObject.Object) -> None:
        if not self.__preferences_dialog:  # mypy
            return
        self.__preferences_dialog.disconnect_by_func(self.__on_preferences_dialog_start_onboarding)
        self.__preferences_dialog = None

    def __create_backup(self) -> None:
        logging.info("Running remotely requested backup")
        self.__backup_manager.create_backup(
            self.__config_manager.nextcloud_sync_configured,
            self.__config_manager.backup_note_extension,
        )

    def __apply_index_category_label_css(self) -> None:
        # First pass approach to building this is using a display CSS provider, instead of adding
        # providers to each label. Presumably this is better, but it's a mess.
        style_manager = Adw.StyleManager.get_default()
        category_style = self.__config_manager.index_category_style
        colour_names = {
            "blue": {"light": "blue_2", "dark": "blue_5"},
            "green": {"light": "green_4", "dark": "green_5"},
            "yellow": {"light": "yellow_4", "dark": "yellow_5"},
            "orange": {"light": "orange_2", "dark": "orange_5"},
            "red": {"light": "red_1", "dark": "red_5"},
            "purple": {"light": "purple_2", "dark": "purple_5"},
        }
        if style_manager.get_dark():
            style_name = "dark"
        else:
            style_name = "light"

        css = "box label.index-category-pill {"
        if category_style in ("monochrome", "muted", "none"):
            css += """
              border-color: @insensitive_fg_color;
              border-width: 1px;
              border-style: solid;"""
        else:
            css += f"""
                background-color: @{colour_names[category_style][style_name]};
                color: @light_1;"""
        if category_style == "muted":
            # Matching libadwaita for dimmed/subtitle (non high-contrast, monochrome option is
            # provides for high contrast)
            css += "\nopacity: 0.55;"
        css += "\n}"

        display = Gdk.Display.get_default()
        if self.__index_category_label_css_provider is None:
            self.__index_category_label_css_provider = Gtk.CssProvider()
        else:
            Gtk.StyleContext.remove_provider_for_display(
                display, self.__index_category_label_css_provider
            )
        self.__index_category_label_css_provider.load_from_data(css, -1)
        Gtk.StyleContext.add_provider_for_display(
            display,
            self.__index_category_label_css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )
