"""OS-level notification listener — cross-platform.

Monitors desktop notifications and fires matching NotificationColorStripStream
instances when new notifications appear.  Sources with os_listener=True are
monitored.

Supported platforms:
- **Windows**: polls toast notifications via winrt UserNotificationListener
  (falls back to winsdk if winrt packages are not installed)
- **Linux**: monitors org.freedesktop.Notifications via D-Bus (dbus-next)
"""

import asyncio
import collections
import json
import platform
import threading
import time
from pathlib import Path
from typing import Dict, List, Optional, Set

from wled_controller.utils import get_logger

logger = get_logger(__name__)

_POLL_INTERVAL = 0.5  # seconds between polls (Windows only)
_HISTORY_FILE = Path("data/notification_history.json")
_HISTORY_MAX = 50

# Module-level singleton for dependency access
_instance: Optional["OsNotificationListener"] = None


def get_os_notification_listener() -> Optional["OsNotificationListener"]:
    """Return the global OsNotificationListener instance (or None)."""
    return _instance


# ── Platform backends ──────────────────────────────────────────────────

def _import_winrt_notifications():
    """Try to import WinRT notification APIs: winrt first, then winsdk fallback.

    Returns (UserNotificationListener, UserNotificationListenerAccessStatus,
    NotificationKinds, backend_name) or raises ImportError.
    """
    # Preferred: lightweight winrt packages (~1MB total)
    try:
        from winrt.windows.ui.notifications.management import (
            UserNotificationListener,
            UserNotificationListenerAccessStatus,
        )
        from winrt.windows.ui.notifications import NotificationKinds
        return UserNotificationListener, UserNotificationListenerAccessStatus, NotificationKinds, "winrt"
    except ImportError as e:
        logger.debug("winrt notification packages not available, trying winsdk: %s", e)
        pass

    # Fallback: winsdk (~35MB, may already be installed)
    from winsdk.windows.ui.notifications.management import (
        UserNotificationListener,
        UserNotificationListenerAccessStatus,
    )
    from winsdk.windows.ui.notifications import NotificationKinds
    return UserNotificationListener, UserNotificationListenerAccessStatus, NotificationKinds, "winsdk"


class _WindowsBackend:
    """Polls Windows toast notifications via winrt (preferred) or winsdk."""

    def __init__(self, on_notification):
        self._on_notification = on_notification
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._seen_ids: Set[int] = set()

    @staticmethod
    def probe() -> bool:
        """Return True if this backend can run on the current system."""
        if platform.system() != "Windows":
            return False
        try:
            UNL, AccessStatus, _NK, backend = _import_winrt_notifications()
            listener = UNL.current
            status = listener.get_access_status()
            if status != AccessStatus.ALLOWED:
                logger.warning(
                    f"OS notification listener: access denied (status={status}). "
                    "Enable notification access in Windows Settings > Privacy > Notifications."
                )
                return False
            logger.info(f"OS notification listener: using {backend} backend")
            return True
        except ImportError:
            logger.info(
                "OS notification listener: neither winrt nor winsdk installed, skipping. "
                "Install with: pip install winrt-Windows.UI.Notifications winrt-Windows.UI.Notifications.Management"
            )
            return False
        except Exception as e:
            logger.warning(f"OS notification listener: Windows init error: {e}")
            return False

    def start(self) -> None:
        self._running = True
        self._thread = threading.Thread(
            target=self._poll_loop,
            name="os-notif-listener",
            daemon=True,
        )
        self._thread.start()

    def stop(self) -> None:
        self._running = False
        if self._thread:
            self._thread.join(timeout=3.0)
            self._thread = None

    def _poll_loop(self) -> None:
        UNL, _AccessStatus, NotificationKinds, _backend = _import_winrt_notifications()
        listener = UNL.current
        loop = asyncio.new_event_loop()

        async def _get_notifications():
            return await listener.get_notifications_async(NotificationKinds.TOAST)

        # Seed with current notifications so we don't fire on startup
        try:
            initial = loop.run_until_complete(_get_notifications())
            for n in initial:
                self._seen_ids.add(n.id)
            logger.debug(f"OS notification listener: seeded {len(self._seen_ids)} existing notifications")
        except Exception as e:
            logger.warning(f"OS notification listener: seed error: {e}")

        try:
            while self._running:
                try:
                    notifications = loop.run_until_complete(_get_notifications())
                    current_ids = set()
                    for n in notifications:
                        current_ids.add(n.id)
                        if n.id not in self._seen_ids:
                            self._seen_ids.add(n.id)
                            app_name = self._extract_app_name(n)
                            self._on_notification(app_name)

                    # Prune seen IDs that are no longer in the notification center
                    self._seen_ids &= current_ids

                except Exception as e:
                    logger.debug(f"OS notification listener poll error: {e}")

                time.sleep(_POLL_INTERVAL)
        finally:
            loop.close()
            self._running = False

    @staticmethod
    def _extract_app_name(notification) -> Optional[str]:
        try:
            return notification.app_info.display_info.display_name
        except (OSError, AttributeError):
            try:
                return notification.app_info.app_user_model_id
            except (OSError, AttributeError):
                return None


class _LinuxBackend:
    """Monitors D-Bus org.freedesktop.Notifications for new notifications."""

    def __init__(self, on_notification):
        self._on_notification = on_notification
        self._running = False
        self._thread: Optional[threading.Thread] = None

    @staticmethod
    def probe() -> bool:
        """Return True if this backend can run on the current system."""
        if platform.system() != "Linux":
            return False
        try:
            import dbus_next  # noqa: F401
            return True
        except ImportError:
            logger.info("OS notification listener: dbus-next not installed, skipping")
            return False

    def start(self) -> None:
        self._running = True
        self._thread = threading.Thread(
            target=self._run_loop,
            name="os-notif-listener",
            daemon=True,
        )
        self._thread.start()

    def stop(self) -> None:
        self._running = False
        if self._thread:
            self._thread.join(timeout=3.0)
            self._thread = None

    def _run_loop(self) -> None:
        """Run the async D-Bus monitor in a dedicated event loop."""
        loop = asyncio.new_event_loop()
        try:
            loop.run_until_complete(self._monitor())
        except Exception as e:
            if self._running:
                logger.error(f"OS notification listener D-Bus error: {e}")
        finally:
            loop.close()
            self._running = False

    async def _monitor(self) -> None:
        from dbus_next.aio import MessageBus
        from dbus_next import BusType, Message, MessageType

        bus = await MessageBus(bus_type=BusType.SESSION).connect()

        # Use BecomeMonitor to eavesdrop on Notify method calls.
        # This sees calls from apps to the notification daemon with the app_name.
        rule = (
            "type='method_call',"
            "interface='org.freedesktop.Notifications',"
            "member='Notify'"
        )
        try:
            msg = Message(
                destination="org.freedesktop.DBus",
                path="/org/freedesktop/DBus",
                interface="org.freedesktop.DBus.Monitoring",
                member="BecomeMonitor",
                signature="asu",
                body=[[rule], 0],
            )
            await bus.call(msg)
        except Exception:
            # Fallback: AddMatch (works on most desktop sessions without special permissions)
            try:
                msg = Message(
                    destination="org.freedesktop.DBus",
                    path="/org/freedesktop/DBus",
                    interface="org.freedesktop.DBus",
                    member="AddMatch",
                    signature="s",
                    body=[rule],
                )
                await bus.call(msg)
            except Exception as e:
                logger.warning(f"OS notification listener: D-Bus match failed: {e}")

        def _on_message(msg):
            if msg.message_type != MessageType.METHOD_CALL:
                return
            if msg.member != "Notify":
                return
            try:
                # Notify args: app_name, replaces_id, app_icon, summary, body, ...
                args = msg.body
                app_name = args[0] if args else None
                self._on_notification(app_name)
            except Exception as e:
                logger.debug(f"OS notification listener: D-Bus parse error: {e}")

        bus.add_message_handler(_on_message)

        logger.info("OS notification listener: D-Bus monitor active")

        # Keep running until stopped
        while self._running:
            await asyncio.sleep(0.5)

        bus.disconnect()


# ── Main listener class ───────────────────────────────────────────────

class OsNotificationListener:
    """Monitors OS notifications and fires notification CSS streams."""

    def __init__(self, color_strip_store, color_strip_stream_manager):
        self._css_store = color_strip_store
        self._css_stream_manager = color_strip_stream_manager
        self._available = False
        self._backend = None
        # Recent notification history (thread-safe deque, newest first)
        self._history: collections.deque = collections.deque(maxlen=_HISTORY_MAX)
        self._load_history()

    @property
    def available(self) -> bool:
        """Whether a platform backend is active and listening."""
        return self._available

    def start(self) -> None:
        global _instance
        _instance = self

        # Try platform backends in order
        for backend_cls in (_WindowsBackend, _LinuxBackend):
            if backend_cls.probe():
                self._backend = backend_cls(on_notification=self._on_new_notification)
                self._backend.start()
                self._available = True
                logger.info(f"OS notification listener started ({backend_cls.__name__})")
                return

        logger.info("OS notification listener: no supported platform backend available")

    def stop(self) -> None:
        if self._backend:
            self._backend.stop()
            self._backend = None
        self._save_history()
        logger.info("OS notification listener stopped")

    @property
    def recent_history(self) -> List[Dict]:
        """Return recent notification history (newest first)."""
        return list(self._history)

    def _load_history(self) -> None:
        """Load persisted notification history from disk."""
        try:
            if _HISTORY_FILE.exists():
                data = json.loads(_HISTORY_FILE.read_text(encoding="utf-8"))
                if isinstance(data, list):
                    for entry in data[:_HISTORY_MAX]:
                        self._history.append(entry)
                    logger.info(f"Loaded {len(self._history)} notification history entries")
        except Exception as exc:
            logger.warning(f"Failed to load notification history: {exc}")

    def _save_history(self) -> None:
        """Persist notification history to disk."""
        try:
            _HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
            _HISTORY_FILE.write_text(
                json.dumps(list(self._history), ensure_ascii=False),
                encoding="utf-8",
            )
        except Exception as exc:
            logger.warning(f"Failed to save notification history: {exc}")

    def _on_new_notification(self, app_name: Optional[str]) -> None:
        """Handle a new OS notification — fire matching streams."""
        from wled_controller.storage.color_strip_source import NotificationColorStripSource

        fired = 0
        filtered = 0
        try:
            sources = self._css_store.get_all_sources()
        except Exception:
            sources = []

        for source in sources:
            if not isinstance(source, NotificationColorStripSource):
                continue
            if not source.os_listener:
                continue

            streams = self._css_stream_manager.get_streams_by_source_id(source.id)
            for stream in streams:
                if hasattr(stream, "fire"):
                    if stream.fire(app_name=app_name):
                        fired += 1
                    else:
                        filtered += 1

        entry = {
            "app": app_name,
            "time": time.time(),
            "fired": fired,
            "filtered": filtered,
        }
        self._history.appendleft(entry)
        self._save_history()

        logger.info(f"OS notification captured: app={app_name!r}, fired={fired}, filtered={filtered}")
