"""Cross-platform theme manager — light / dark / system.

Detects and tracks the OS theme using the *darkdetect* library.
Notifies registered callbacks when the effective theme changes.

Usage::

    # At app startup
    theme_mgr = ThemeManager.setup(config)
    theme_mgr.add_callback(my_on_theme_change)

    # Get current effective theme
    theme = theme_mgr.get_current_theme()   # 'light' or 'dark'

    # Change theme preference
    theme_mgr.set_theme("dark")             # or 'light' / 'system'
"""

from __future__ import annotations

import logging
import threading
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..config import Config

log = logging.getLogger(__name__)

VALID_THEMES = {"light", "dark", "system"}
_THEMES_DIR = Path(__file__).parent.parent.parent / "assets" / "themes"


class ThemeManager:
    """Manages application theme with live system-theme tracking."""

    _instance: ThemeManager | None = None

    def __init__(self, config: Config) -> None:
        self._config = config
        self._callbacks: list[Callable[[str], None]] = []
        self._lock = threading.Lock()
        self._current_effective: str = "light"
        self._listener_started = False
        self._init()

    # ------------------------------------------------------------------ #
    # Singleton
    # ------------------------------------------------------------------ #

    @classmethod
    def setup(cls, config: Config) -> ThemeManager:
        """Create (or replace) the singleton ThemeManager."""
        cls._instance = cls(config)
        return cls._instance

    @classmethod
    def get_instance(cls) -> ThemeManager:
        """Return existing singleton. Raises if setup() was not called."""
        if cls._instance is None:
            raise RuntimeError("ThemeManager not initialized — call setup() first")
        return cls._instance

    # ------------------------------------------------------------------ #
    # Public API
    # ------------------------------------------------------------------ #

    def get_current_theme(self) -> str:
        """Return the effective theme: 'light' or 'dark' (never 'system')."""
        return self._current_effective

    def set_theme(self, theme: str) -> None:
        """Set the theme preference and apply immediately.

        Args:
            theme: One of 'light', 'dark', 'system'.
        """
        if theme not in VALID_THEMES:
            log.warning("Unknown theme '%s' — falling back to 'system'", theme)
            theme = "system"

        self._config.theme = theme
        self._config.save()

        if theme == "system":
            self._start_system_listener()

        effective = self._compute_effective()
        self._apply_and_notify(effective)

    def add_callback(self, cb: Callable[[str], None]) -> None:
        """Subscribe to theme change events.

        The callback is called with 'light' or 'dark' from any thread.
        For Qt widgets, wrap with a pyqtSignal or QTimer.singleShot.
        """
        with self._lock:
            self._callbacks.append(cb)

    def remove_callback(self, cb: Callable[[str], None]) -> None:
        """Unsubscribe a previously registered callback."""
        with self._lock:
            try:
                self._callbacks.remove(cb)
            except ValueError:
                pass

    def apply_theme(self) -> None:
        """Re-fire callbacks with the current effective theme."""
        self._apply_and_notify(self._current_effective)

    def load_qss(self, theme: str | None = None) -> str:
        """Load and return a QSS stylesheet for the given theme.

        Falls back to the current effective theme if *theme* is None.
        Returns an empty string if the file is not found.
        """
        if theme is None:
            theme = self._current_effective
        qss_path = _THEMES_DIR / f"{theme}.qss"
        try:
            return qss_path.read_text(encoding="utf-8")
        except FileNotFoundError:
            log.warning("QSS file not found: %s", qss_path)
            return ""
        except OSError as exc:
            log.warning("Cannot read QSS file %s: %s", qss_path, exc)
            return ""

    # ------------------------------------------------------------------ #
    # Private
    # ------------------------------------------------------------------ #

    def _init(self) -> None:
        self._current_effective = self._compute_effective()
        if self._config.theme == "system":
            self._start_system_listener()

    def _compute_effective(self) -> str:
        """Map the config theme preference to an effective 'light' or 'dark'."""
        theme = self._config.theme
        if theme == "light":
            return "light"
        if theme == "dark":
            return "dark"
        # theme == "system" — query OS
        return self._get_system_theme()

    def _get_system_theme(self) -> str:
        """Query the current OS theme via darkdetect. Defaults to 'light'."""
        try:
            import darkdetect  # type: ignore[import]

            result = darkdetect.theme()
            if result and result.lower() == "dark":
                return "dark"
        except Exception as exc:
            log.debug("darkdetect unavailable: %s", exc)
        return "light"

    def _start_system_listener(self) -> None:
        """Start the darkdetect background listener (idempotent)."""
        if self._listener_started:
            return
        try:
            import darkdetect  # type: ignore[import]

            def _safe_listener(callback) -> None:  # type: ignore[no-untyped-def]
                try:
                    darkdetect.listener(callback)
                except Exception as exc:
                    log.debug("darkdetect listener stopped: %s", exc)

            thread = threading.Thread(
                target=_safe_listener,
                args=(self._on_system_theme_change,),
                daemon=True,
                name="theme-listener",
            )
            thread.start()
            self._listener_started = True
            log.debug("System theme listener started")
        except Exception as exc:
            log.debug("Could not start theme listener: %s", exc)

    def _on_system_theme_change(self, theme: str) -> None:
        """Called by darkdetect listener when the OS theme changes."""
        if self._config.theme != "system":
            return
        effective = "dark" if theme.lower() == "dark" else "light"
        with self._lock:
            if effective == self._current_effective:
                return
        log.info("System theme changed: %s", effective)
        self._apply_and_notify(effective)

    def _apply_and_notify(self, effective: str) -> None:
        """Update stored theme and fire all registered callbacks."""
        with self._lock:
            self._current_effective = effective
            callbacks = list(self._callbacks)

        log.debug("Theme applied: %s (%d callbacks)", effective, len(callbacks))
        for cb in callbacks:
            try:
                cb(effective)
            except Exception as exc:
                log.warning("Theme callback raised an exception: %s", exc)
