from gettext import gettext as _
import gi

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

import logging
from typing import Callable, Optional

from iotas.backup_manager import BackupManager
from iotas import const
import iotas.config_manager
from iotas.database import Database
from iotas.migration_assistant import MigrationAssistant
from iotas.note_database import NoteDatabase
from iotas.preferences_window import PreferencesWindow
from iotas.nextcloud_sync_worker import NextcloudSyncWorker
from iotas.sync_manager import SyncManager
from iotas.widgets import load_widgets
from iotas.window import Window


@Gtk.Template(resource_path="/org/gnome/gitlab/cheywood/Iotas/ui/keyboard_shortcuts_window.ui")
class KeyboardShortcutsWindow(Gtk.ShortcutsWindow):
    __gtype_name__ = "KeyboardShortcutsWindow"

    def __init__(self, window: Gtk.Window):
        super().__init__()
        self.set_transient_for(window)


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

    def __init__(self, *args):
        super().__init__(
            *args,
            application_id=self.application_id,
            flags=Gio.ApplicationFlags.FLAGS_NONE,
            resource_base_path="/org/gnome/gitlab/cheywood/Iotas",
            register_session=True,
        )

        self.__add_cli_options()

        self.__base_css_resource = "{}/ui/base_style.css".format(self.props.resource_base_path)
        self.__light_css_resource = "{}/ui/light_style.css".format(self.props.resource_base_path)
        self.__dark_css_resource = "{}/ui/dark_style.css".format(self.props.resource_base_path)
        self.__high_contrast_css_resource = "{}/ui/high_contrast_style.css".format(
            self.props.resource_base_path
        )
        self.__base_css_provider = None
        self.__light_css_provider = None
        self.__dark_css_provider = None
        self.__index_category_label_css_provider = None
        style_manager = Adw.StyleManager.get_default()
        style_manager.connect("notify::dark", self.__on_style_change)
        style_manager.connect("notify::high-contrast", self.__on_style_change)

        self.cursors = {}
        self.__actions = {}
        self.__backup_manager = None

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

        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.INDEX_CATEGORY_STYLE}", self.__category_style_changed
        )

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

        self.__sync_manager.sign_out()
        self.db.delete_all_clean_synced_notes()
        iotas.config_manager.set_nextcloud_username("")
        self.__window.close()

    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()
        iotas.config_manager.set_nextcloud_prune_threshold(0)
        self.quit()

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

    def __on_startup(self, _obj: GObject.Object) -> None:
        """Handle startup."""
        Gtk.Application.do_startup(self)
        Adw.init()

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

        migration_assistant = MigrationAssistant(self.db)
        migration_assistant.handle_version_migration()

        load_widgets()

        self.__create_window()

        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.PRIMARY_BACKUP_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"):
                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"):
                # Translators: Log message
                msg = _("Please quit the running instance of Iotas before creating the backup")
                logging.error(msg)
                self.quit()
                success = False
            else:
                success = self.__backup_manager.create_backup()
            self.quit()
            return int(not success)
        elif "restore-backup" in options:
            self.register()
            if self.get_property("is-remote"):
                # Translators: Log message
                msg = _("Please quit the running instance of Iotas before restoring the backup")
                logging.error(msg)
                self.quit()
                success = False
            else:
                success = self.__backup_manager.restore_backup()
            self.quit()
            return int(not success)
        elif "quit-running" in options:
            self.register()
            if self.get_property("is-remote"):
                self.activate_action("quit-running")
            else:
                # Translators: Log message
                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()
            if self.get_property("is-remote"):
                self.activate_action("create-note-wrapper")
            else:
                self.__window.activate_action("win.create-note-from-cli")
        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"])
            if self.get_property("is-remote"):
                self.activate_action("open-note-wrapper", note_id)
            else:
                self.__window.activate_action("win.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"])
            if self.get_property("is-remote"):
                self.activate_action("search-from-cli-wrapper", term)
            else:
                self.__window.activate_action("win.search-from-cli", term)
        elif "toggle-extended-preferences" in options:
            self.register()
            if iotas.config_manager.get_show_extended_preferences():
                # Translators: Log message
                logging.info(_("Hiding extended preferences"))
                iotas.config_manager.set_show_extended_preferences(False)
            else:
                # Translators: Log message
                logging.info(_("Showing extended preferences"))
                iotas.config_manager.set_show_extended_preferences(True)
            self.quit()
            return 0

        # Let default option processing continue
        return -1

    def __on_activate(self, _obj: GObject.Object) -> None:
        """Handle window activation."""
        self.__apply_css()
        Application.apply_style()
        self.__window.present()

    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", self.__show_about_dialog)
        add_action("quit", self.__on_quit_shortcut, "<Control>q")
        add_action("settings", self.__show_preferences_window, "<Control>comma")
        add_action("shortcuts", self.__show_shortcuts_window, "<Control>question")
        add_action("create-note-wrapper", self.__on_create_note_wrapper)
        add_action("open-note-wrapper", self.__on_open_note_wrapper, None, GLib.VariantType("u"))
        add_action("search-from-cli-wrapper", self.__on_search_wrapper, None, GLib.VariantType("s"))
        add_action(
            "set-style-variant",
            self.__set_style_variant,
            parameter_type=GLib.VariantType("s"),
        )
        add_action("quit-running", self.__on_quit_running)

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

    def __apply_css(self) -> None:
        if self.__base_css_resource is None:
            return

        display = Gdk.Display.get_default()

        if self.__base_css_provider is None:
            self.__base_css_provider = Gtk.CssProvider()
            self.__base_css_provider.load_from_resource(self.__base_css_resource)
            self.__light_css_provider = Gtk.CssProvider()
            self.__light_css_provider.load_from_resource(self.__light_css_resource)
            self.__dark_css_provider = Gtk.CssProvider()
            self.__dark_css_provider.load_from_resource(self.__dark_css_resource)
            self.__high_contrast_css_provider = Gtk.CssProvider()
            self.__high_contrast_css_provider.load_from_resource(self.__high_contrast_css_resource)
        else:
            Gtk.StyleContext.remove_provider_for_display(display, self.__base_css_provider)
            Gtk.StyleContext.remove_provider_for_display(display, self.__light_css_provider)
            Gtk.StyleContext.remove_provider_for_display(display, self.__dark_css_provider)
            Gtk.StyleContext.remove_provider_for_display(display, self.__high_contrast_css_provider)

        style_manager = Adw.StyleManager.get_default()

        if style_manager.get_high_contrast():
            Gtk.StyleContext.add_provider_for_display(
                display, self.__high_contrast_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
            )

        style_provider = (
            self.__dark_css_provider if style_manager.get_dark() else self.__light_css_provider
        )
        Gtk.StyleContext.add_provider_for_display(
            display, style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        Gtk.StyleContext.add_provider_for_display(
            display, self.__base_css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )
        self.__apply_index_category_label_css()

    def __show_about_dialog(self, _action: Gio.SimpleAction, _param: GLib.Variant) -> None:
        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/gitlab/cheywood/Iotas/about_dialog.ui")
        about_dialog = builder.get_object("AboutDialog")
        about_dialog.set_transient_for(self.__window)
        about_dialog.present()

    def __show_shortcuts_window(self, _action: Gio.SimpleAction, _param: GLib.Variant) -> None:
        win = KeyboardShortcutsWindow(self.__window)
        win.present()

    def __show_preferences_window(self, _action: Gio.SimpleAction, _param: GLib.Variant) -> None:
        PreferencesWindow().present()

    def __set_style_variant(self, _action: Gio.SimpleAction, new_style: str) -> None:
        iotas.config_manager.set_style(new_style.get_string())
        Application.apply_style()

    def __on_style_change(
        self,
        _obj: GObject.Object,
        _value: GObject.ParamSpec,
    ) -> None:
        self.__apply_css()

    def __on_quit_shortcut(self, _window: Gtk.Window, _action_param: GLib.Variant) -> None:
        self.__window.close()

    def __on_quit_running(
        self,
        _obj: GObject.Object,
        _value: GObject.ParamSpec,
    ) -> None:
        self.quit()

    def __on_create_note_wrapper(self, _action: Gio.SimpleAction, _param: GLib.Variant) -> None:
        self.__window.activate_action("win.create-note-from-cli")

    def __on_open_note_wrapper(self, _action: Gio.SimpleAction, param: GLib.Variant) -> None:
        self.__window.activate_action("win.open-note", param)

    def __on_search_wrapper(self, _action: Gio.SimpleAction, param: GLib.Variant) -> None:
        self.__window.activate_action("win.search-from-cli", param)

    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 = iotas.config_manager.get_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,
        )

    def __category_style_changed(self, _obj: Gio.Settings, _key: str) -> None:
        self.__apply_index_category_label_css()

    def __destroy_window_and_quit(self) -> None:
        self.__window.close()

    @staticmethod
    def apply_style() -> None:
        """Apply style preference."""
        manager = Adw.StyleManager.get_default()
        style = iotas.config_manager.get_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
