from __future__ import annotations

import argparse
import faulthandler
import logging
import os
import sys
from pathlib import Path

# A hard crash (segfault) in the GTK/GDK/X11 C layer normally leaves no
# trace at all - no Python traceback, just the process disappearing. This
# makes faulthandler dump a Python-level stack for fatal signals (SIGSEGV
# included) straight to stderr before the process dies, which is otherwise
# the only way to point at *where* such a crash happened.
faulthandler.enable()

# Force XWayland instead of the native Wayland backend: the overlay hints in
# x11_overlay.py (always-on-top, click-through) rely on X11/EWMH, since
# GNOME's Mutter doesn't implement the wlr-layer-shell Wayland protocol.
os.environ["GDK_BACKEND"] = "x11"

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gio, GLib, Gtk  # noqa: E402

from . import i18n
from .config import load_config, load_language, profile_path, resolve_active_profile, set_active_profile
from .drag import ModifierWatcher
from .hotkeys import HotkeyListener
from .overlay import OverlayWindow
from .settings_window import SettingsWindow

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
logger = logging.getLogger("titletimer")


def _sync_dark_theme() -> None:
    """Make our GTK chrome (the settings window) follow GNOME's system
    dark-mode toggle.

    Plain GTK4 doesn't do this on its own for a non-Flatpak app: it would
    need GTK_USE_PORTAL=1 plus a working xdg-desktop-portal round-trip,
    which is unreliable outside a sandbox. Reading
    org.gnome.desktop.interface color-scheme directly via GSettings is
    simpler and doesn't depend on the portal at all - verified live: it
    reports 'prefer-dark' correctly even when
    Gtk.Settings:gtk-application-prefer-dark-theme still defaults to
    False. No-ops on non-GNOME desktops where that schema isn't installed.
    """
    schema_source = Gio.SettingsSchemaSource.get_default()
    if schema_source is None or schema_source.lookup("org.gnome.desktop.interface", True) is None:
        return

    interface_settings = Gio.Settings.new("org.gnome.desktop.interface")
    gtk_settings = Gtk.Settings.get_default()

    def apply(*_args) -> None:
        gtk_settings.set_property(
            "gtk-application-prefer-dark-theme",
            interface_settings.get_string("color-scheme") == "prefer-dark",
        )

    apply()
    interface_settings.connect("changed::color-scheme", apply)
    # Gio.Settings has no other owner once this function returns - keep a
    # reference alive on gtk_settings (itself a process-wide singleton) so
    # the "changed" connection isn't silently dropped by GC.
    gtk_settings._titletimer_interface_settings = interface_settings


def _resolve_config_path(args: argparse.Namespace) -> tuple[Path, str | None]:
    """Returns (config path to load, active profile name or None).

    `--config` bypasses the character-profile system entirely and points
    straight at a file (useful for testing or a one-off config); the
    profile name is then None, which tells SettingsWindow to hide the
    profile-management UI since there's no profile concept in play.
    """
    if args.config is not None:
        return args.config, None
    profile_name = resolve_active_profile()
    set_active_profile(profile_name)
    return profile_path(profile_name), profile_name


def _run_settings(config, config_path: Path, profile_name: str | None) -> int:
    app = Gtk.Application(application_id="dev.korintimer.settings")

    def on_activate(app: Gtk.Application) -> None:
        _sync_dark_theme()
        window = SettingsWindow(app, config, config_path, profile_name=profile_name)
        app.add_window(window)
        window.present()

    app.connect("activate", on_activate)
    return app.run(None)


def main() -> int:
    parser = argparse.ArgumentParser(prog="titletimer")
    parser.add_argument(
        "--config",
        type=Path,
        default=None,
        help="Direct path to a config.json, bypassing the character-profile "
        "system (default: last-active profile under "
        "~/.config/titletimer/profiles/)",
    )
    parser.add_argument(
        "--settings",
        action="store_true",
        help="Open only the settings editor, without the overlay.",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Log every raw key event (device, keycode, held set) - noisy, "
        "for diagnosing hotkeys that don't register.",
    )
    args = parser.parse_args()

    if args.debug:
        logging.getLogger("titletimer").setLevel(logging.DEBUG)

    i18n.set_language(load_language())

    config_path, profile_name = _resolve_config_path(args)
    try:
        config = load_config(config_path)
    except (FileNotFoundError, ValueError) as exc:
        logger.error(str(exc))
        return 1

    if args.settings:
        return _run_settings(config, config_path, profile_name)

    app = Gtk.Application(application_id="dev.korintimer.overlay")

    def on_activate(app: Gtk.Application) -> None:
        _sync_dark_theme()
        try:
            overlay_window = OverlayWindow(config, config_path)
        except RuntimeError as exc:
            logger.error(str(exc))
            app.quit()
            return
        app.add_window(overlay_window)
        overlay_window.present()

        def on_hotkey(held: frozenset[str], pressed_key: str) -> None:
            GLib.idle_add(overlay_window.handle_hotkey, held, pressed_key)

        listener = HotkeyListener(on_hotkey)
        try:
            listener.start()
        except RuntimeError as exc:
            logger.error(str(exc))
            app.quit()
            return

        modifier_watcher = ModifierWatcher(overlay_window.on_alt_change)
        try:
            modifier_watcher.start()
        except RuntimeError as exc:
            logger.error(str(exc))
            app.quit()
            return

        overlay_window._hotkey_listener = listener  # keep a reference alive
        overlay_window._modifier_watcher = modifier_watcher  # keep a reference alive

        # Settings opens right alongside the overlay - and closing it quits
        # the whole app, since that's otherwise the only way to stop an
        # overlay with no window chrome and no way to click through to it.
        def on_settings_closed(_window) -> bool:
            app.quit()
            return False  # don't block the window from actually closing

        settings_window = SettingsWindow(
            app,
            config,
            config_path,
            profile_name=profile_name,
            on_saved=overlay_window.apply_config,
        )
        app.add_window(settings_window)
        settings_window.present()
        settings_window.connect("close-request", on_settings_closed)

    app.connect("activate", on_activate)
    return app.run(None)


if __name__ == "__main__":
    sys.exit(main())
