"""System-event hooks — the SDK-side contract for OS/app events reaching plugins.

The desktop app already tracks these events internally (``ForegroundWatcher``
polls the foreground process for auto-switch profiles; the device/serial layer
notices connect/disconnect; Windows notifies wake-from-sleep; ``mixlar://``
deep links arrive via the OS URL-protocol handler) but as of this SDK version
none of that is wired to plugins — this module is the documented seam for
closing that gap, not a claim that wiring already exists.

**Delivery model.** These events are delivered by the HOST, not synthesized by
the SDK. The desktop app is expected to call :func:`broadcast` (or
:func:`dispatch` for a single plugin) from its own event sources, passing every
loaded plugin instance and the event name/kwargs. A plugin author subclasses
:class:`SystemEventsMixin` and overrides the ``on_*`` hook(s) it cares about;
everything else stays a no-op.

Host integration
-----------------
Full wiring lives in ``PC Software/mixlar_mini.py`` — this is the map of
exactly where each call belongs (none of these call sites exist yet; adding
them is what turns this contract on):

- ``APP_LAUNCH`` / ``APP_TERMINATE`` — inside ``ForegroundWatcher``'s poll loop,
  wherever it currently diffs the foreground process for auto-switch profiles.
  Call ``broadcast(plugins, APP_LAUNCH, exe=exe_name)`` when a new foreground
  exe is first seen, and ``APP_TERMINATE`` when a tracked exe disappears.
- ``SYSTEM_WAKE`` — hook the Windows sleep/wake notification (e.g. a
  ``WM_POWERBROADCAST`` / ``PBT_APMRESUMEAUTOMATIC`` handler, or
  ``QSystemTrayIcon``-adjacent OS event) and call
  ``broadcast(plugins, SYSTEM_WAKE)`` on resume.
- ``DEVICE_CONNECT`` / ``DEVICE_DISCONNECT`` — in the serial port scanner /
  ``CDCReader`` connection lifecycle (where the app already detects the M1X
  attaching/detaching over USB), call ``broadcast(plugins, DEVICE_CONNECT)`` /
  ``broadcast(plugins, DEVICE_DISCONNECT)``.
- ``DEEP_LINK`` — in the ``mixlar://`` URL-protocol handler (single-instance
  activation / custom scheme entry point), parse the incoming URL with
  :func:`parse_deep_link` and call
  ``broadcast(plugins, DEEP_LINK, url=raw_url)`` (or route directly to the
  matched plugin using the parsed ``plugin_id``).

Every dispatch is crash-isolated: a plugin raising from a hook never takes
down the host loop or other plugins.
"""
from __future__ import annotations

from typing import Any, Dict, Iterable, Tuple
from urllib.parse import parse_qsl, urlparse

# ── Event name constants ──────────────────────────────────────────────────────
APP_LAUNCH = "app_launch"
APP_TERMINATE = "app_terminate"
SYSTEM_WAKE = "system_wake"
DEVICE_CONNECT = "device_connect"
DEVICE_DISCONNECT = "device_disconnect"
DEEP_LINK = "deep_link"

#: Maps an event name to the mixin method name that handles it.
_HANDLER_NAMES: Dict[str, str] = {
    APP_LAUNCH: "on_app_launch",
    APP_TERMINATE: "on_app_terminate",
    SYSTEM_WAKE: "on_system_wake",
    DEVICE_CONNECT: "on_device_connect",
    DEVICE_DISCONNECT: "on_device_disconnect",
    DEEP_LINK: "on_deep_link",
}


class SystemEventsMixin:
    """Mixin: no-op overridable hooks for host-delivered system events.

    Mix in alongside :class:`mixlar.plugin.MixlarPlugin`::

        class MyPlugin(SystemEventsMixin, MixlarPlugin):
            def on_device_connect(self):
                ...

    Override only the hooks you need — the rest stay harmless no-ops.
    """

    def on_app_launch(self, exe: str) -> None:
        """A tracked foreground app started (``exe`` is the process name)."""

    def on_app_terminate(self, exe: str) -> None:
        """A tracked foreground app exited."""

    def on_system_wake(self) -> None:
        """The OS resumed from sleep."""

    def on_device_connect(self) -> None:
        """The M1X device attached over USB serial."""

    def on_device_disconnect(self) -> None:
        """The M1X device detached / the serial link dropped."""

    def on_deep_link(self, url: str) -> None:
        """A ``mixlar://...`` deep link was opened (raw URL, unparsed)."""


def dispatch(plugin: Any, event: str, **kwargs: Any) -> None:
    """Call the hook on ``plugin`` matching ``event``, if it has one.

    Crash-isolated: never raises, even if ``plugin`` lacks the hook, the hook
    signature doesn't match, or the hook itself throws. Unknown ``event``
    names are silently ignored (forward-compatible with future event types).
    """
    handler_name = _HANDLER_NAMES.get(event)
    if not handler_name:
        return
    try:
        handler = getattr(plugin, handler_name, None)
        if handler is None:
            return
        handler(**kwargs)
    except Exception:
        pass


def broadcast(plugins: Iterable[Any], event: str, **kwargs: Any) -> None:
    """:func:`dispatch` ``event`` to every plugin in ``plugins``."""
    for plugin in plugins:
        dispatch(plugin, event, **kwargs)


def parse_deep_link(url: str) -> Tuple[str, Dict[str, str]]:
    """Parse a ``mixlar://<plugin_id>/<action>?k=v`` deep link.

    Returns ``(plugin_id, params)`` where ``params`` is the query string as a
    flat dict, with the path's action segment (if present) stashed under the
    ``"action"`` key. Malformed input degrades gracefully — an empty/unparsable
    URL returns ``("", {})`` rather than raising.
    """
    try:
        parsed = urlparse(url)
        plugin_id = parsed.netloc
        params: Dict[str, str] = dict(parse_qsl(parsed.query))
        action = parsed.path.strip("/")
        if action:
            params["action"] = action
        return plugin_id, params
    except Exception:
        return "", {}


__all__ = [
    "APP_LAUNCH", "APP_TERMINATE", "SYSTEM_WAKE",
    "DEVICE_CONNECT", "DEVICE_DISCONNECT", "DEEP_LINK",
    "SystemEventsMixin", "dispatch", "broadcast", "parse_deep_link",
]
