"""Background daemon that listens for monitor hotplug events and applies profiles."""

from __future__ import annotations

import asyncio
import logging
import os
import signal
import threading

from .hyprland import HyprlandIPC
from .sway import SwayIPC
from .models import Profile, apply_clamshell, undo_clamshell
from .profile_manager import ProfileManager
from .utils import load_app_settings

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [moniqued] %(levelname)s %(message)s",
)
log = logging.getLogger(__name__)

DEBOUNCE_MS = 500


def _detect_backend() -> HyprlandIPC | SwayIPC | None:
    """Auto-detect the running compositor from environment variables."""
    if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
        return HyprlandIPC()
    if os.environ.get("SWAYSOCK"):
        return SwayIPC()
    return None


class MonitorDaemon:
    """Watches compositor events and auto-applies matching profiles."""

    def __init__(self) -> None:
        self._profile_mgr = ProfileManager()
        self._debounce_handle: asyncio.TimerHandle | None = None
        self._ipc: HyprlandIPC | SwayIPC | None = None
        self._lid_closed: bool | None = None  # None = no lid / not monitored
        self._asyncio_loop: asyncio.AbstractEventLoop | None = None

    async def run(self) -> None:
        log.info("Starting Monique daemon")
        self._asyncio_loop = asyncio.get_event_loop()
        self._start_lid_monitor()

        while True:
            try:
                ipc = _detect_backend()
                if ipc is None:
                    log.warning("No supported compositor detected. Retrying in 5s...")
                    await asyncio.sleep(5)
                    continue

                backend_name = "Hyprland" if isinstance(ipc, HyprlandIPC) else "Sway"
                log.info("Detected %s compositor", backend_name)
                await self._listen(ipc)
            except (ConnectionRefusedError, FileNotFoundError, ConnectionError) as e:
                log.warning("Cannot connect to compositor: %s. Retrying in 5s...", e)
                await asyncio.sleep(5)
            except Exception as e:
                log.error("Unexpected error: %s. Retrying in 5s...", e)
                await asyncio.sleep(5)
            finally:
                if self._debounce_handle:
                    self._debounce_handle.cancel()
                    self._debounce_handle = None

    async def _listen(self, ipc: HyprlandIPC | SwayIPC) -> None:
        self._ipc = ipc
        log.info("Connected to compositor event socket")
        async for event in ipc.connect_event_socket():
            log.info("Monitor event: %s", event)
            self._schedule_apply(ipc)

    def _schedule_apply(self, ipc: HyprlandIPC | SwayIPC) -> None:
        """Debounce monitor events before applying."""
        loop = asyncio.get_event_loop()
        if self._debounce_handle:
            self._debounce_handle.cancel()
        self._debounce_handle = loop.call_later(
            DEBOUNCE_MS / 1000.0,
            lambda: asyncio.ensure_future(self._apply_best_profile(ipc)),
        )

    async def _apply_best_profile(self, ipc: HyprlandIPC | SwayIPC) -> None:
        """Query current monitors, find best profile, and apply it."""
        try:
            monitors = ipc.get_monitors()
            fingerprint = sorted(m.description for m in monitors if m.description)
            log.info("Current fingerprint: %s", fingerprint)

            settings = load_app_settings()
            clamshell = settings.get("clamshell_mode", False)

            profile = self._profile_mgr.find_best_match(fingerprint)
            if profile:
                # Apply clamshell mode if enabled
                if clamshell:
                    profile = Profile.from_dict(profile.to_dict())
                    if self._lid_closed is not False:
                        # Lid closed or unknown → disable internal
                        if apply_clamshell(profile.monitors):
                            log.info("Clamshell: disabled internal display(s)")
                    else:
                        # Lid open → ensure internal is enabled
                        if undo_clamshell(profile.monitors):
                            log.info("Clamshell: lid open, re-enabled internal display(s)")

                # Snapshot workspaces before applying
                ws_snapshot = ipc.get_workspaces()

                log.info("Applying profile: %s", profile.name)
                update_sddm = settings.get("update_sddm", True)
                update_greetd = settings.get("update_greetd", True)
                use_desc = not settings.get("use_port_names", False)
                ipc.apply_profile(
                    profile, update_sddm=update_sddm,
                    update_greetd=update_greetd, use_description=use_desc,
                )

                # Migrate orphaned workspaces
                if settings.get("migrate_workspaces", True):
                    self._migrate_orphaned_workspaces(ipc, profile, ws_snapshot)
            elif clamshell and self._lid_closed is False:
                # No saved profile, lid open → re-enable internal
                if undo_clamshell(monitors):
                    log.info("Clamshell: re-enabled internal display(s)")
                    temp = Profile(name="clamshell-undo", monitors=monitors)
                    update_sddm = settings.get("update_sddm", True)
                    use_desc = not settings.get("use_port_names", False)
                    ipc.apply_profile(
                        temp, update_sddm=update_sddm, use_description=use_desc,
                    )
            else:
                log.info("No matching profile found")
        except Exception as e:
            log.error("Failed to apply profile: %s", e)

    # ── Lid monitoring via UPower D-Bus ─────────────────────────────

    def _start_lid_monitor(self) -> None:
        """Start monitoring lid state via UPower D-Bus in a background thread."""
        try:
            import gi
            gi.require_version("Gio", "2.0")
            from gi.repository import Gio, GLib
        except (ImportError, ValueError):
            log.info("GLib not available, lid monitoring disabled")
            return

        def _run() -> None:
            try:
                bus = Gio.bus_get_sync(Gio.BusType.SYSTEM)

                # Check if lid is present
                result = bus.call_sync(
                    "org.freedesktop.UPower",
                    "/org/freedesktop/UPower",
                    "org.freedesktop.DBus.Properties",
                    "Get",
                    GLib.Variant("(ss)", ("org.freedesktop.UPower", "LidIsPresent")),
                    GLib.VariantType("(v)"),
                    Gio.DBusCallFlags.NONE,
                    -1,
                    None,
                )
                lid_present = result.get_child_value(0).get_variant().get_boolean()
                if not lid_present:
                    log.info("No lid detected, lid monitoring disabled")
                    return

                # Get initial lid state
                result = bus.call_sync(
                    "org.freedesktop.UPower",
                    "/org/freedesktop/UPower",
                    "org.freedesktop.DBus.Properties",
                    "Get",
                    GLib.Variant("(ss)", ("org.freedesktop.UPower", "LidIsClosed")),
                    GLib.VariantType("(v)"),
                    Gio.DBusCallFlags.NONE,
                    -1,
                    None,
                )
                self._lid_closed = result.get_child_value(0).get_variant().get_boolean()
                log.info("Initial lid state: %s", "closed" if self._lid_closed else "open")

                # Subscribe to PropertiesChanged
                bus.signal_subscribe(
                    "org.freedesktop.UPower",
                    "org.freedesktop.DBus.Properties",
                    "PropertiesChanged",
                    "/org/freedesktop/UPower",
                    None,
                    Gio.DBusSignalFlags.NONE,
                    _on_signal,
                    None,
                )

                loop = GLib.MainLoop.new(GLib.MainContext.default(), False)
                loop.run()
            except Exception as e:
                log.warning("Lid monitor failed: %s", e)

        def _on_signal(_conn, _sender, _path, _iface, _signal, params, _user_data):
            iface_name = params.get_child_value(0).get_string()
            if iface_name != "org.freedesktop.UPower":
                return
            changed = params.get_child_value(1)
            lid_val = changed.lookup_value("LidIsClosed", GLib.VariantType("b"))
            if lid_val is None:
                return
            closed = lid_val.get_boolean()
            log.info("Lid state changed: %s", "closed" if closed else "open")
            self._lid_closed = closed
            if self._ipc and self._asyncio_loop:
                self._asyncio_loop.call_soon_threadsafe(
                    self._schedule_apply, self._ipc,
                )

        thread = threading.Thread(target=_run, daemon=True, name="lid-monitor")
        thread.start()

    def _migrate_orphaned_workspaces(
        self,
        ipc: HyprlandIPC | SwayIPC,
        profile,
        ws_snapshot: list[dict],
    ) -> None:
        """Move workspaces from disabled/removed monitors to the primary monitor."""
        enabled_names = {m.name for m in profile.monitors if m.enabled}
        if not enabled_names:
            return

        primary = next(m.name for m in profile.monitors if m.enabled)
        migrated = 0

        for ws in ws_snapshot:
            ws_monitor = ws.get("monitor", "")
            ws_name = str(ws.get("name", ws.get("id", "")))
            if ws_monitor and ws_monitor not in enabled_names:
                try:
                    ipc.move_workspace_to_monitor(ws_name, primary)
                    migrated += 1
                except Exception as e:
                    log.warning("Failed to migrate workspace %s: %s", ws_name, e)

        if migrated:
            log.info("Migrated %d workspace(s) to %s", migrated, primary)


def main() -> None:
    daemon = MonitorDaemon()
    loop = asyncio.new_event_loop()

    # Handle signals for clean shutdown
    for sig in (signal.SIGTERM, signal.SIGINT):
        loop.add_signal_handler(sig, loop.stop)

    try:
        loop.run_until_complete(daemon.run())
    except KeyboardInterrupt:
        pass
    finally:
        loop.close()
        log.info("Daemon stopped")
