# 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, Gtk, Gio, GLib

from meshy.background_portal import BackgroundPortal

log = logging.getLogger(__name__)


class MeshyApplication(Adw.Application):

    def __init__(self, version='0.1.0', **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)
        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
        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
        from urllib.parse import urlparse, parse_qs
        import re

        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
            header = 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
        pkgdatadir = os.path.dirname(os.path.dirname(__file__))
        resource_path = os.path.join(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):
        css = b'''
        .outgoing-message {
            background-color: alpha(@accent_bg_color, 0.15);
            border-color: alpha(@accent_bg_color, 0.3);
        }
        .navigation-sidebar row {
            margin-left: 4px;
            margin-right: 4px;
            padding-left: 4px;
            padding-right: 4px;
        }
        .unread-badge {
            background-color: @accent_bg_color;
            color: @accent_fg_color;
            border-radius: 12px;
            min-width: 12px;
            font-size: 0.8em;
            font-weight: bold;
            padding: 2px 5px 1px;
        }
        .emoji-avatar {
            background-color: alpha(@accent_bg_color, 0.15);
            border-radius: 50%;
            min-width: 32px;
            min-height: 32px;
            font-size: 18px;
        }
        .unread-divider {
            color: @accent_bg_color;
            font-weight: bold;
            font-size: 0.8em;
        }
        .date-divider-line {
            min-height: 1px;
            margin-top: 0;
            margin-bottom: 0;
        }
        .unread-divider-line {
            min-height: 1px;
            margin-top: 0;
            margin-bottom: 0;
            background-color: @accent_bg_color;
        }
        .map-marker-icon {
            color: white;
        }
        .map-marker-badge {
            background-color: rgba(60, 60, 60, 0.85);
            color: white;
            font-size: 8px;
            font-weight: bold;
            min-width: 14px;
            min-height: 14px;
            border-radius: 7px;
            padding: 0;
        }
        .nav-rail {
            background-color: @sidebar_bg_color;
            border-right: 1px solid alpha(@borders, 0.5);
            padding-top: 4px;
            padding-left: 4px;
            padding-right: 3px;
        }
        listview.background > row {
            min-height: 0;
            padding: 0;
            border-radius: 0;
            background: none;
            outline: none;
        }
        .pill {
            border-radius: 16px;
            padding-left: 16px;
            padding-right: 16px;
        }
        .theme-tile {
            border-radius: 12px;
            min-width: 100px;
            min-height: 50px;
            box-shadow: 0 0 0 1px alpha(@borders, 0.5);
        }
        button.flat:checked .theme-tile {
            box-shadow: 0 0 0 2px @accent_bg_color;
        }
        '''
        provider = Gtk.CssProvider()
        provider.load_from_data(css)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            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.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'])

    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', 'stereo'],
            license_type=Gtk.License.GPL_3_0,
            comments=_('A GTK client for MeshCore LoRa mesh network devices'),
            website='https://codeberg.org/sesivany/meshy',
            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)
