"""Ergonomic macro-action declaration for :class:`mixlar.plugin.MixlarPlugin`.

Authoring macro actions by hand means overriding four parallel methods
(``get_macro_actions``, ``get_macro_action_groups``, ``get_macro_action_icons``,
``execute_macro_step``) and keeping an ``if/elif`` dispatch in sync with them —
see ``PC Software/Plugin Examples/Bambu Studio/bambu_studio.py`` for what that
looks like at scale. This module lets you declare each action once, next to its
handler, with the :func:`action` decorator and the :class:`MacroActions` mixin::

    from mixlar import MixlarPlugin
    from mixlar.macros import MacroActions, action

    class MyPlugin(MacroActions, MixlarPlugin):
        plugin_id = "my_plugin"

        @action("mute", "Mute Mic", icon="fa5s.microphone-slash", group="Audio")
        def _mute(self, step):
            ...

The mixin reads ``step["<plugin_id>_action"]`` to dispatch — the exact key
convention the app's built-in editor writes (see ``http_trigger_action`` in
``PC Software/Plugin Examples/HttpTrigger/HttpTrigger.py``) — falling back to a
plain ``"action"`` key for callers that don't namespace it. Nothing here is
required: override any of the four methods directly on your subclass and it
wins over the decorator-driven behaviour (MRO makes your override the last
word).
"""
from __future__ import annotations

from typing import Any, Callable, Dict, List, Optional, Tuple

#: Attribute name used to stash action metadata on the decorated function.
_META_ATTR = "_mixlar_action"


class ActionMeta:
    """Metadata recorded by :func:`action` on the wrapped method."""

    __slots__ = ("id", "name", "icon", "group")

    def __init__(self, id: str, name: str, icon: Optional[str],
                 group: Optional[str]) -> None:
        self.id = id
        self.name = name
        self.icon = icon
        self.group = group or "General"


def action(id: str, name: str, icon: Optional[str] = None,
           group: Optional[str] = None) -> Callable[[Callable], Callable]:
    """Mark a :class:`MacroActions` method as a macro action handler.

    The decorated method IS the handler: it is called as ``self.<method>(step)``
    when the step's action id matches. ``icon`` is an optional ``fa5s.*``
    (Font Awesome) name; ``group`` buckets the action into a tab in the macro
    builder (defaults to ``"General"``).
    """
    def _wrap(fn: Callable) -> Callable:
        setattr(fn, _META_ATTR, ActionMeta(id, name, icon, group))
        return fn
    return _wrap


class MacroActions:
    """Mixin: auto-implement the macro-action quartet from ``@action`` methods.

    Mix in ahead of :class:`mixlar.plugin.MixlarPlugin` (or the app's live
    equivalent) so this class's methods take priority in the MRO::

        class MyPlugin(MacroActions, MixlarPlugin): ...

    Scans the class (once, cached) for methods carrying ``@action`` metadata
    and derives:

    - ``get_macro_actions()`` — ``[(id, name), ...]`` in declaration order.
    - ``get_macro_action_groups()`` — actions bucketed by ``group=``.
    - ``get_macro_action_icons()`` — ``{id: icon}`` for actions that gave one.
    - ``execute_macro_step(step)`` — dispatches to the matching method by
      reading ``step["<plugin_id>_action"]`` (falling back to ``step["action"]``).

    A plugin can still override any of these four methods directly — that
    override simply shadows the mixin's version, no special-casing needed.
    """

    # ── introspection (cached per concrete class) ────────────────────────────
    @classmethod
    def _mixlar_collect_actions(cls) -> List[Tuple[str, ActionMeta, str]]:
        """Return ``[(action_id, meta, method_name), ...]`` in MRO/definition order."""
        cache_attr = "_mixlar_actions_cache"
        cached = cls.__dict__.get(cache_attr)
        if cached is not None:
            return cached
        seen: Dict[str, Tuple[str, ActionMeta, str]] = {}
        # Walk from most-derived to least-derived so subclass overrides win,
        # but keep first-seen (most-derived) ordering.
        for klass in cls.__mro__:
            for attr_name, attr_val in vars(klass).items():
                meta = getattr(attr_val, _META_ATTR, None)
                if isinstance(meta, ActionMeta) and meta.id not in seen:
                    seen[meta.id] = (meta.id, meta, attr_name)
        result = list(seen.values())
        setattr(cls, cache_attr, result)
        return result

    # ── the quartet ───────────────────────────────────────────────────────────
    def get_macro_actions(self) -> List[Tuple[str, str]]:
        return [(aid, meta.name) for aid, meta, _ in self._mixlar_collect_actions()]

    def get_macro_action_groups(self) -> List[Tuple[str, List[Tuple[str, str]]]]:
        groups: Dict[str, List[Tuple[str, str]]] = {}
        order: List[str] = []
        for aid, meta, _ in self._mixlar_collect_actions():
            if meta.group not in groups:
                groups[meta.group] = []
                order.append(meta.group)
            groups[meta.group].append((aid, meta.name))
        return [(g, groups[g]) for g in order]

    def get_macro_action_icons(self) -> Dict[str, str]:
        return {
            aid: meta.icon
            for aid, meta, _ in self._mixlar_collect_actions()
            if meta.icon
        }

    def execute_macro_step(self, step: Dict[str, Any]) -> None:
        plugin_id = getattr(self, "plugin_id", "")
        action_id = step.get(f"{plugin_id}_action") if plugin_id else None
        if action_id is None:
            action_id = step.get("action")
        if not action_id:
            return
        for aid, _meta, method_name in self._mixlar_collect_actions():
            if aid == action_id:
                getattr(self, method_name)(step)
                return


__all__ = ["action", "ActionMeta", "MacroActions"]
