"""``MixlarPlugin`` — the typed base class you subclass to build a plugin.

Import it and subclass it::

    from mixlar import MixlarPlugin

    class MyPlugin(MixlarPlugin):
        plugin_id = "my_plugin"
        ...

**Runtime identity.** Inside the running desktop app the live
``mixlar_mini.MixlarPlugin`` is re-exported here verbatim, so a plugin authored
against the SDK subclasses the *same* class the app's loader checks with
``issubclass`` — it loads with zero changes and ``push_widget_data`` reaches the
real serial link. Off-app (your editor, ``mixlar-sdk emulate``, tests) you get the
faithful standalone class below, which routes pushes through
:class:`mixlar.registry.PluginRegistry` so the emulator can capture them.

This is the "single source of truth" seam from the SDK blueprint: the same
import works in both worlds, and IDEs can finally resolve the class, its methods,
and their signatures.
"""
from __future__ import annotations

import sys as _sys
from typing import Any, Dict, List, Optional, Tuple

from .registry import PluginRegistry

# QSettings-ish object the host passes to on_load; typed loosely on purpose.
Settings = Any


class _StandaloneMixlarPlugin:
    """Base class for Mixlar Control plugins (off-app standalone edition).

    Behaviourally identical to ``mixlar_mini.MixlarPlugin``. Override the
    metadata attributes and the hooks you need. Inside the app this class is
    replaced by the live one (see module docstring).
    """

    # ── Metadata (override in subclass) ──────────────────────────────────────
    plugin_id: str = ""
    plugin_name: str = "Unnamed Plugin"
    plugin_version: str = "0.0.0"
    plugin_author: str = ""
    plugin_description: str = ""
    plugin_icon: str = "fa5s.puzzle-piece"
    plugin_icon_color: str = ""
    plugin_icons_dir: str = ""

    #: Pair with a bundled device widget (``widgets/<widget_id>/``). Pairing
    #: namespaces every ``push_widget_data`` key as ``<widget_id>.<key>``.
    widget_id: str = ""

    # ── Lifecycle ────────────────────────────────────────────────────────────
    def on_load(self, settings: Settings) -> None:
        """Called once at startup with the host settings object."""
        self._settings = settings

    def on_unload(self) -> None:
        """Called on app shutdown / warm-reload."""

    # ── Macro methods ────────────────────────────────────────────────────────
    def get_macro_actions(self) -> List[Tuple[str, str]]:
        """Return ``[(action_id, display_name), ...]``."""
        return []

    def get_macro_action_groups(self) -> List[Tuple[str, List[Tuple[str, str]]]]:
        """Return ``[("Group", [(id, name), ...]), ...]`` for a tabbed grid."""
        return []

    def get_macro_action_icons(self) -> Dict[str, str]:
        """Return ``{action_id: "fa5s.icon"}``."""
        return {}

    def get_macro_step_defaults(self) -> Dict[str, Any]:
        """Return a dict of default fields for new steps."""
        return {}

    def execute_macro_step(self, step: Dict[str, Any]) -> None:
        """Run the macro action (called from a background thread)."""

    def describe_macro_step(self, step: Dict[str, Any]) -> str:
        """Return a human-readable summary for the step list."""
        return self.plugin_name

    def build_macro_editor(self, step, row, editor_lay, upd, upd_refresh, lbl):
        """Add custom Qt editor widgets below the action selector."""

    def prime_client(self, settings: Settings) -> None:
        """Pre-init any clients on the main thread before macro execution."""

    # ── Slider methods ───────────────────────────────────────────────────────
    def get_slider_mode(self) -> Optional[Tuple[str, str]]:
        """Return ``(mode_id, display_name)`` or ``None``."""
        return None

    def build_slider_config_ui(self, idx, parent_layout, settings) -> None:
        """Build the config panel for this slider mode."""

    def save_slider_config(self, idx) -> Dict[str, Any]:
        """Return a dict of settings to persist."""
        return {}

    def handle_slider_value(self, idx: int, value: int, pct: float) -> None:
        """Handle slider movement (0-100 value, 0.0-1.0 pct)."""

    def get_slider_color(self, idx) -> Optional[str]:
        """Return a hex color string or ``None`` for default."""
        return None

    # ── Plugin settings page ─────────────────────────────────────────────────
    def build_settings_ui(self, layout, settings) -> None:
        """Build a plugin settings panel (Qt). See also settings.schema.json."""

    def has_settings_ui(self) -> bool:
        """True if this plugin overrides :meth:`build_settings_ui`."""
        return type(self).build_settings_ui is not _StandaloneMixlarPlugin.build_settings_ui

    # ── Custom device widgets ────────────────────────────────────────────────
    def on_widget_event(self, widget_id: str, element_id: str,
                        action: str) -> None:
        """A custom-widget interactive element fired on the device.

        ``action`` is ``"press"`` | ``"hold"`` (buttons) or ``"on"`` | ``"off"``
        (toggles). Broadcast to every plugin — filter by the ids you own.
        """

    def on_widget_shown(self, widget_id: str) -> None:
        """Your paired widget came on screen — push full state, start timers."""

    def on_widget_hidden(self, widget_id: str) -> None:
        """Your paired widget left the screen — go quiet, stop timers."""

    def widget_bundle_dir(self) -> Optional[str]:
        """Path to ``<plugin dir>/widgets/<widget_id>/`` or ``None``."""
        import os
        if not self.widget_id:
            return None
        src = getattr(self, "_mixlar_source", "")
        if not src:
            return None
        d = os.path.join(os.path.dirname(src), "widgets", self.widget_id)
        return d if os.path.isfile(os.path.join(d, "widget.json")) else None

    def widget_bundle_dirs(self) -> List[Tuple[str, str]]:
        """All bundled device widgets: ``[(widget_id, dir), ...]``."""
        import os
        src = getattr(self, "_mixlar_source", "")
        base = os.path.dirname(src) if src else ""
        out: List[Tuple[str, str]] = []
        for w in getattr(self, "_manifest_widgets", []) or []:
            wid = w.get("id", "") if isinstance(w, dict) else ""
            if not wid or not base:
                continue
            d = os.path.join(base, "widgets", wid)
            if os.path.isfile(os.path.join(d, "widget.json")):
                out.append((wid, d))
        if not out:
            d = self.widget_bundle_dir()
            if d:
                out.append((self.widget_id, d))
        return out

    def push_widget_data(self, key, value, widget: Optional[str] = None) -> bool:
        """Push a live data point to the device's custom-widget layer.

        Any element in your paired widget with ``"bind": "<key>"`` refreshes.
        Keys are auto-namespaced by ``widget_id`` (pass ``widget=`` to feed
        another widget you own, or a ``_``-prefixed key for shared feeds).
        No-op (returns ``False``) when no host sender is attached. Thread-safe.
        """
        try:
            sender = PluginRegistry.instance()._wdata_sender
        except AttributeError:
            return False
        if sender is None:
            return False
        try:
            key = str(key).strip().replace(",", "_")
            wid = widget if widget is not None else self.widget_id
            if wid and not key.startswith("_"):
                key = f"{wid}.{key}"
            key = key[:47]
            value = str(value).replace("\n", " ").replace(",", " ")[:47]
            return bool(sender(f"WDATA,{key},{value}\n"))
        except Exception:
            return False

    def push_widget_image(self, key, source, w, h,
                          widget: Optional[str] = None) -> bool:
        """Stream a runtime image (album art, avatar) to an ``img`` element.

        Decoded, resized to ``w×h``, sent RGB565. BLOCKS on the transfer — call
        from a worker thread. No-op with no sender. Standalone/off-app this uses
        Pillow if available; inside the app the native fast path is used.
        """
        try:
            sender = PluginRegistry.instance()._wimg_sender
        except AttributeError:
            return False
        if sender is None:
            return False
        try:
            key = str(key).strip().replace(",", "_")
            wid = widget if widget is not None else self.widget_id
            if wid and not key.startswith("_"):
                key = f"{wid}.{key}"
            key = key[:47]
            w = max(1, min(int(w), 240))
            h = max(1, min(int(h), 140))
            from .imaging import source_to_rgb565
            rgb565 = source_to_rgb565(source, w, h)
            if not rgb565:
                return False
            return bool(sender(key, rgb565, w, h))
        except Exception:
            return False

    # ── Settings helpers ─────────────────────────────────────────────────────
    def _pget(self, key: str, default: Any = "") -> Any:
        """Read a namespaced setting (``plugin_<id>_<key>``)."""
        try:
            s = getattr(self, "_settings", None)
            if s is not None and hasattr(s, "value"):
                return s.value(f"plugin_{self.plugin_id}_{key}", default)
        except Exception:
            pass
        return getattr(self, "_pstore", {}).get(
            f"plugin_{self.plugin_id}_{key}", default)

    def _pset(self, key: str, value: Any) -> None:
        """Write a namespaced setting (``plugin_<id>_<key>``)."""
        try:
            s = getattr(self, "_settings", None)
            if s is not None and hasattr(s, "setValue"):
                s.setValue(f"plugin_{self.plugin_id}_{key}", value)
                return
        except Exception:
            pass
        if not hasattr(self, "_pstore"):
            self._pstore: Dict[str, Any] = {}
        self._pstore[f"plugin_{self.plugin_id}_{key}"] = value


# Inside the live app, BE the app's class so issubclass() checks pass and pushes
# use the real link. Off-app, use the standalone base above. The app may be
# `mixlar_mini` or `__main__` (frozen/entry) — _host.find_app() handles both.
from ._host import find_app as _find_app  # noqa: E402
_app = _find_app()
if _app is not None:  # pragma: no cover - only true inside the desktop app
    MixlarPlugin = _app.MixlarPlugin  # type: ignore[misc,assignment]
else:
    MixlarPlugin = _StandaloneMixlarPlugin


__all__ = ["MixlarPlugin"]
