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
import iotas.const as 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_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


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

    def __init__(self, window: Gtk.Window) -> None:
        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) -> None:
        super().__init__(
            *args,
            application_id=self.application_id,
            flags=Gio.ApplicationFlags.HANDLES_OPEN,
            resource_base_path="/org/gnome/World/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: Gtk.CssProvider = None
        self.__light_css_provider: Gtk.CssProvider = None
        self.__dark_css_provider: Gtk.CssProvider = None
        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.__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)

        iotas.config_manager.settings.connect(
            f"changed::{iotas.config_manager.INDEX_CATEGORY_STYLE}",
            lambda _o, _k: self.__apply_index_category_label_css(),
        )

    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 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()
        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

    @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()

        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 = iotas.config_manager.get_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(
                    iotas.config_manager.nextcloud_sync_configured(),
                    iotas.config_manager.get_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(
                    iotas.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 iotas.config_manager.get_show_extended_preferences():
                logging.info("Hiding extended preferences")
                iotas.config_manager.set_show_extended_preferences(False)
            else:
                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) -> None:
        """Handle window activation."""
        if not self.__window:
            self.__create_window()

        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])

        def on_create_note(action: Gio.SimpleAction, param: GLib.Variant) -> None:
            self.activate()
            if self.__window:
                self.__window.activate_action("win.create-note-from-cli")

        def on_create_note_from_uri(action: Gio.SimpleAction, param: GLib.Variant) -> None:
            self.activate()
            if self.__window:
                self.__window.activate_action("win.create-note-from-uri", param),

        def on_open_note(action: Gio.SimpleAction, param: GLib.Variant) -> None:
            self.activate()
            if self.__window:
                self.__window.activate_action("win.open-note", param),

        def on_search_from_cli(action: Gio.SimpleAction, param: GLib.Variant) -> None:
            self.activate()
            if self.__window:
                self.__window.activate_action("win.search-from-cli", param),

        add_action("about", lambda _o, _p: self.__show_about_dialog())
        add_action(
            "quit",
            lambda _o, _p: self.__window.close() if self.__window else None,
            "<Control>q",
        )
        add_action("settings", lambda _o, _p: self.__show_preferences_dialog(), "<Control>comma")
        add_action(
            "shortcuts",
            lambda _o, _p: KeyboardShortcutsWindow(self.__window).present(),
            "<Control>question",
        )
        add_action("create-note", on_create_note)
        add_action(
            "create-note-from-uri",
            on_create_note_from_uri,
            None,
            GLib.VariantType("s"),
        )
        add_action(
            "open-note",
            on_open_note,
            None,
            GLib.VariantType("u"),
        )
        add_action(
            "search-from-cli",
            on_search_from_cli,
            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:
        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 + 1,
            )

        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) -> None:
        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/World/Iotas/about_dialog.ui")
        about_dialog = builder.get_object("AboutDialog")
        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("closed", self.__on_preferences_dialog_closed)
        self.__preferences_dialog.present(self.__window)

    def __on_preferences_dialog_closed(self, _dialog: GObject.Object) -> None:
        self.__preferences_dialog = None

    def __create_backup(self) -> None:
        logging.info("Running remotely requested backup")
        self.__backup_manager.create_backup(
            iotas.config_manager.nextcloud_sync_configured(),
            iotas.config_manager.get_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 = 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,
        )

    @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
