# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

import logging

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gdk, Gio, GLib, Gtk
from meshy.background_portal import BackgroundPortal

log = logging.getLogger(__name__)


class MeshyApplication(Adw.Application):
    def __init__(self, version="26.07", **kwargs):
        super().__init__(
            application_id="page.codeberg.sesivany.Meshy",
            flags=Gio.ApplicationFlags.HANDLES_OPEN,
            **kwargs,
        )
        self.version = version
        self._window = None
        self._pending_uri = None  # URI received before window is ready
        self._hidden_start = False
        self._background_held = False
        self._bg_portal = BackgroundPortal()

        self.add_main_option(
            "debug",
            ord("d"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            "Enable debug logging",
            None,
        )
        self.add_main_option(
            "hidden",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            "Start without showing the window (used for autostart)",
            None,
        )

    def do_handle_local_options(self, options):
        if options.contains("debug"):
            logging.basicConfig(level=logging.DEBUG, format="%(name)s %(levelname)s %(message)s")
        else:
            logging.basicConfig(level=logging.WARNING)
            # On macOS, also log to file for troubleshooting (console output hidden when launched via Finder)
            from meshy.platforms import is_macos

            if is_macos():
                import os

                log_file = os.path.expanduser("~/Library/Logs/Meshy/debug.log")
                os.makedirs(os.path.dirname(log_file), exist_ok=True)
                file_handler = logging.FileHandler(log_file, mode="w")
                file_handler.setLevel(logging.DEBUG)
                file_handler.setFormatter(
                    logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
                )
                logging.getLogger().addHandler(file_handler)
                logging.getLogger().setLevel(logging.DEBUG)
        self._hidden_start = options.contains("hidden")
        return -1  # continue normal startup

    def do_activate(self):
        if not self._window:
            from meshy.window import MeshyWindow

            self._window = MeshyWindow(application=self)
            settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy")
            if settings.get_boolean("run-in-background"):
                self._enable_background_mode()
        if self._hidden_start:
            self._hidden_start = False
            return

        # Activate NSApplication on macOS before presenting window
        from meshy.platforms import is_macos

        if is_macos():
            from meshy.platforms.macos.app_activation import activate_macos_app

            activate_macos_app()

        self._window.present()
        # Process any URI that arrived before the window was created
        if self._pending_uri:
            uri = self._pending_uri
            self._pending_uri = None
            GLib.idle_add(lambda: self._handle_meshcore_uri(uri) or False)

    def do_open(self, files, n_files, hint):
        """Handle meshcore:// URIs opened from the system."""
        self.do_activate()
        for f in files:
            uri = f.get_uri()
            if uri and uri.startswith("meshcore://"):
                if self._window and self._window.is_connected:
                    self._handle_meshcore_uri(uri)
                else:
                    self._pending_uri = uri

    def _handle_meshcore_uri(self, uri):
        """Process a meshcore:// URI -- import contact or channel."""
        if not self._window:
            return
        import re
        from urllib.parse import parse_qs, urlparse

        type_labels = {1: _("Chat"), 2: _("Repeater"), 3: _("Room"), 4: _("Sensor")}

        # Raw hex format: meshcore://<hex> — no slashes, no query params
        hex_data = uri[len("meshcore://") :].strip("/")
        if re.fullmatch(r"[0-9a-fA-F]+", hex_data):
            try:
                contact_frame = bytes.fromhex(hex_data)
                name, contact_type = self._parse_advert_name(contact_frame)
                type_label = type_labels.get(contact_type, _("Unknown"))
                if name:
                    body = _("Name: {name}\nType: {type}").format(name=name, type=type_label)
                else:
                    body = _("Type: {type}\nKey: {key}…").format(
                        type=type_label, key=hex_data[4:36]
                    )
                self._show_import_dialog(
                    _("Import Contact"),
                    body,
                    lambda: (
                        self._window.import_contact(contact_frame),
                        self._window.show_toast(_("Contact {}").format(name or _("imported"))),
                    ),
                )
            except ValueError:
                self._window.show_toast(_("Invalid meshcore:// link"))
            return

        parsed = urlparse(uri)
        params = parse_qs(parsed.query)

        # meshcore://channel/add?name=...&secret=...
        full_path = f"{parsed.netloc}{parsed.path}".rstrip("/")
        if full_path == "channel/add" and "secret" in params:
            ch_name = params.get("name", [""])[0]
            secret_hex = params["secret"][0].strip()
            try:
                secret = bytes.fromhex(secret_hex)
            except ValueError:
                self._window.show_toast(_("Invalid channel secret in link"))
                return
            if len(secret) != 16:
                self._window.show_toast(_("Invalid channel secret length"))
                return
            self._show_import_dialog(
                _("Add Channel"),
                _("Name: {name}\nType: Private").format(name=ch_name or _("Unnamed")),
                lambda: self._window.add_channel_from_qr(ch_name, secret),
            )
            return

        # meshcore://contact/add?name=...&public_key=...&type=...
        if "public_key" in params:
            pub_key_hex = params["public_key"][0].strip()
            name = params.get("name", [""])[0]
            try:
                contact_type = int(params.get("type", ["1"])[0])
            except ValueError:
                contact_type = 1
            try:
                pub_key = bytes.fromhex(pub_key_hex)
            except ValueError:
                self._window.show_toast(_("Invalid public key in link"))
                return
            if len(pub_key) != 32:
                self._window.show_toast(_("Invalid public key length"))
                return
            if self._window.is_contact_known(pub_key_hex):
                self._window.show_toast(
                    _("{} is already in your list").format(name or _("Contact"))
                )
                return
            type_label = type_labels.get(contact_type, _("Chat"))
            self._show_import_dialog(
                _("Add {} Contact").format(type_label),
                _("Name: {name}\nPublic Key: {key}…").format(
                    name=name or _("Unknown"), key=pub_key_hex[:16]
                ),
                lambda: (
                    self._window.add_contact(pub_key, contact_type, name or "Unknown"),
                    self._window.show_toast(_("Contact {} added").format(name or pub_key_hex[:16])),
                ),
            )
            return

        self._window.show_toast(_("Unrecognized meshcore:// link"))

    @staticmethod
    def _parse_advert_name(frame: bytes) -> tuple:
        """Extract name and type from a raw advert packet frame.

        Returns (name, adv_type) or ('', 0) if parsing fails.
        Frame layout: header(1) + path_len(1) + path(var) + pubkey(32)
                      + timestamp(4) + signature(64) + appdata(flags + ...)
        """
        try:
            i = 0
            frame[i]
            i += 1
            path_len_byte = frame[i]
            i += 1
            hop_count = path_len_byte & 0x3F
            hash_size = ((path_len_byte >> 6) & 0x03) + 1
            i += hop_count * hash_size  # skip path
            i += 32  # pub key
            i += 4  # timestamp
            i += 64  # signature
            if i >= len(frame):
                return ("", 0)
            flags = frame[i]
            i += 1
            adv_type = flags & 0x0F
            if flags & 0x10:  # has latlon
                i += 8
            if flags & 0x20:  # has feat1
                i += 2
            if flags & 0x40:  # has feat2
                i += 2
            if flags & 0x80:  # has name
                name = frame[i:].decode("utf-8", errors="replace").rstrip("\x00")
                return (name, adv_type)
            return ("", adv_type)
        except (IndexError, ValueError):
            return ("", 0)

    def _show_import_dialog(self, heading, body, on_confirm):
        """Show a confirmation dialog before importing."""
        dialog = Adw.AlertDialog(heading=heading, body=body)
        dialog.add_response("cancel", _("Cancel"))
        dialog.add_response("add", _("Add"))
        dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
        dialog.connect("response", lambda d, r: on_confirm() if r == "add" else None)
        dialog.present(self._window)

    def do_startup(self):
        # Register GResource before chaining up so GTK can auto-discover
        # gtk/help-overlay.ui during Gtk.Application.do_startup().
        self._load_gresource()
        Adw.Application.do_startup(self)
        self._setup_icon_theme()
        self._load_css()
        self._setup_actions()
        self._apply_saved_theme()

    def _load_gresource(self):
        """Load the bundled GResource file."""
        import os

        import meshy as _meshy_pkg

        resource_path = os.path.join(_meshy_pkg.pkgdatadir, "meshy.gresource")
        if os.path.exists(resource_path):
            resource = Gio.Resource.load(resource_path)
            Gio.resources_register(resource)

    def _setup_icon_theme(self):
        """Add custom icon resource path (requires display, call after startup)."""
        icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
        icon_theme.add_resource_path("/page/codeberg/sesivany/Meshy/icons")

    def _load_css(self):
        from meshy.platforms import is_macos

        # Load base CSS from resource
        provider = Gtk.CssProvider()
        provider.load_from_resource("/page/codeberg/sesivany/Meshy/style.css")
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )

        # Add macOS-specific CSS for traffic light buttons
        if is_macos():
            macos_css = b"""
        /* macOS traffic light button spacing */
        .macos-window .nav-rail {
            /* Add space at top for traffic lights (28px buttons + padding) */
            padding-top: 40px;
        }
        .macos-window #sidebar_header {
            /* Add left padding to avoid traffic light overlap */
            padding-left: 80px;
        }
        .macos-window #connection_header {
            /* Connection screen header also needs spacing */
            padding-left: 80px;
        }
        """
            macos_provider = Gtk.CssProvider()
            macos_provider.load_from_data(macos_css)
            Gtk.StyleContext.add_provider_for_display(
                Gdk.Display.get_default(),
                macos_provider,
                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
            )

    def _request_background_portal(self):
        settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy")
        autostart = settings.get_boolean("autostart")
        self._bg_portal.request_background(
            _("Receiving messages in the background"),
            autostart,
            ["meshy", "--hidden"] if autostart else [],
            self._on_background_response,
        )

    def _enable_background_mode(self):
        if not self._background_held:
            self.hold()
            self._background_held = True
        if self._window:
            self._window.set_background_mode(True)
        self._request_background_portal()

    def _disable_background_mode(self):
        if self._background_held:
            self.release()
            self._background_held = False
        if self._window:
            self._window.set_background_mode(False)
        self._bg_portal.request_background(
            _("Receiving messages in the background"),
            False,
            [],
            lambda bg, auto: None,
        )
        self._bg_portal.clear_status()

    def _on_background_response(self, bg_granted, autostart_granted):
        log.info("Background portal response: bg=%s autostart=%s", bg_granted, autostart_granted)
        if not bg_granted and self._background_held:
            self.release()
            self._background_held = False
            if self._window:
                self._window.set_background_mode(False)

    def update_background_mode(self, enabled):
        if enabled:
            self._enable_background_mode()
        else:
            self._disable_background_mode()

    def update_autostart(self, enabled):
        if not enabled:
            self._bg_portal.request_background(
                _("Receiving messages in the background"),
                False,
                [],
                self._on_background_response,
            )
        elif self._background_held:
            self._request_background_portal()

    def _do_full_quit(self, *_args):
        if self._window:
            self._window.shutdown()
        if self._background_held:
            self.release()
            self._background_held = False
        self._bg_portal.clear_status()
        self.quit()

    def _setup_actions(self):
        quit_action = Gio.SimpleAction.new("quit", None)
        quit_action.connect("activate", self._do_full_quit)
        self.add_action(quit_action)
        self.set_accels_for_action("win.quit", ["<primary>q"])

        about_action = Gio.SimpleAction.new("about", None)
        about_action.connect("activate", self._on_about)
        self.add_action(about_action)

        import meshy as _meshy_pkg

        if _meshy_pkg.SHORTCUTS_DIALOG_ENABLED:
            self.set_accels_for_action("win.show-help-overlay", ["<primary>question"])
        self.set_accels_for_action("win.disconnect", ["<primary>d"])
        self.set_accels_for_action("win.help", ["F1"])
        self.set_accels_for_action("win.navigate-device", ["<primary>1"])
        self.set_accels_for_action("win.navigate-contacts", ["<primary>2"])
        self.set_accels_for_action("win.navigate-channels", ["<primary>3"])
        self.set_accels_for_action("win.navigate-map", ["<primary>4"])
        self.set_accels_for_action("win.navigate-settings", ["<primary>5"])
        self.set_accels_for_action("win.search", ["<primary>f"])
        self.set_accels_for_action("win.new-message", ["<primary>n"])
        self.set_accels_for_action("win.navigate-prev", ["<Alt>Up"])
        self.set_accels_for_action("win.navigate-next", ["<Alt>Down"])
        self.set_accels_for_action("win.navigate-prev-unread", ["<Alt><Shift>Up"])
        self.set_accels_for_action("win.navigate-next-unread", ["<Alt><Shift>Down"])
        self.set_accels_for_action("win.custom-tiles", ["<primary>m"])

    def _apply_saved_theme(self):
        from meshy.theme_manager import ThemeManager

        ThemeManager.get_default().apply_from_settings()

    def _on_about(self, action, param):
        about = Adw.AboutDialog(
            application_name="Meshy",
            application_icon="page.codeberg.sesivany.Meshy",
            version=self.version,
            developer_name="Jiri Eischmann",
            developers=[
                "Jiri Eischmann",
                "Jaroslav Řezník",
                "stereo",
                "vanous",
                "Moritz Bitsch",
                "Petr Menšík",
                "Manuel Traut",
                "Lukáš Viktora",
                "clauwn",
            ],
            translator_credits=_(
                "gallegonovato — Spanish\n"
                "lebeno — French, Dutch\n"
                "bjawebos — German\n"
                "Manuel Traut — German\n"
                "lejun — French\n"
                "LievenBlancke — Dutch\n"
                "sesivany — Czech"
            ),
            license_type=Gtk.License.GPL_3_0,
            comments=_("A GTK client for MeshCore LoRa mesh network devices"),
            website="https://meshy-app.org/",
            support_url="https://meshy-app.org/docs/",
            issue_url="https://codeberg.org/sesivany/meshy/issues",
            copyright="\u00a9 2026 Jiri Eischmann",
        )
        about.add_legal_section(
            "libshumate",
            "\u00a9 libshumate contributors",
            Gtk.License.LGPL_2_1,
            None,
        )
        about.add_legal_section(
            "zbar",
            "\u00a9 zbar contributors",
            Gtk.License.LGPL_2_1,
            None,
        )
        about.add_legal_section(
            "protobuf-c",
            "\u00a9 protobuf-c contributors",
            Gtk.License.BSD,
            None,
        )
        about.add_legal_section(
            "PyCryptodome",
            "\u00a9 PyCryptodome contributors",
            Gtk.License.BSD,
            None,
        )
        about.add_legal_section(
            "pyzbar",
            "\u00a9 pyzbar contributors",
            Gtk.License.MIT_X11,
            None,
        )
        about.add_legal_section(
            "pySerial",
            "\u00a9 pySerial contributors",
            Gtk.License.BSD,
            None,
        )
        about.add_legal_section(
            "Segno",
            "\u00a9 Segno contributors",
            Gtk.License.BSD,
            None,
        )
        about.add_legal_section(
            _("Adwaita Symbolic Icons"),
            "\u00a9 GNOME Project",
            Gtk.License.LGPL_3_0,
            None,
        )
        about.present(self._window)
