"""Automation Action Registry.

Each module that wants to expose automation actions must call
``register_action(ActionSpec(...))`` from its ``AppConfig.ready()`` or from
an ``automations.py`` module (auto-discovered at startup).

The executor never imports module code by name at runtime — it only calls
``get_action(key)`` which returns the pre-registered callable, keeping
dispatch fully explicit and testable.

Usage
-----
::

    from simorgh.apps.automation.registry import ActionSpec, register_action

    register_action(ActionSpec(
        key="notifications.send_notification",
        label="Send Notification",
        module="notifications",
        params_schema={
            "type": "object",
            "properties": {
                "template": {"type": "string"},
                "recipient_id": {"type": "integer"},
            },
            "required": ["template", "recipient_id"],
        },
        handler=_send_notification_handler,
    ))
"""

from __future__ import annotations

import re
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from threading import Lock
from typing import Any

_KEY_RE = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")


class AutomationRegistryError(RuntimeError):
    """Raised for registry misuse (duplicate keys, bad key format, etc.)."""


@dataclass(frozen=True)
class ActionSpec:
    """Descriptor for a single automation action.

    Attributes
    ----------
    key:
        Dot-separated identifier, e.g. ``"notifications.send_notification"``.
        Must match the pattern ``module.action_name``.
    label:
        Human-readable label shown in the UI no-code builder.
    module:
        Name of the owning module (for grouping in the UI).
    params_schema:
        JSON Schema dict describing the ``params`` object that the executor
        will pass to ``handler``.  Used for frontend validation and docs.
    handler:
        Callable with signature ``(context: ActionContext) -> None``.
        Must be **idempotent** — the executor may call it more than once on
        partial retry.
    """

    key: str
    label: str
    module: str
    params_schema: dict[str, Any] = field(default_factory=dict)
    handler: Callable[["ActionContext"], None] = field(default=lambda _ctx: None)

    def __post_init__(self) -> None:
        if not _KEY_RE.match(self.key):
            raise AutomationRegistryError(
                f"ActionSpec key must be 'module.action_name' format, got {self.key!r}"
            )


@dataclass
class ActionContext:
    """Runtime context passed to every action handler.

    Attributes
    ----------
    tenant_id:
        PK of the tenant the rule belongs to.
    rule_id:
        PK of the ``AutomationRule`` being executed.
    trigger_event:
        Name of the event that triggered the rule, e.g. ``"crm.lead.created"``.
    trigger_payload:
        Raw event payload dict.
    params:
        Action-specific params extracted from ``AutomationRule.actions[n].params``.
    actor_id:
        User PK who triggered the event, or ``None`` for scheduled/system events.
    """

    tenant_id: int
    rule_id: int
    trigger_event: str
    trigger_payload: dict[str, Any]
    params: dict[str, Any]
    actor_id: int | None = None


# ---------------------------------------------------------------------------
# Global registry
# ---------------------------------------------------------------------------

_registry: dict[str, ActionSpec] = {}
_lock = Lock()


def register_action(spec: ActionSpec) -> None:
    """Register an action spec.  Idempotent for identical re-registration.

    Raises ``AutomationRegistryError`` if a different spec is already
    registered under the same key (detects accidental double-registration of
    different handlers).
    """
    with _lock:
        existing = _registry.get(spec.key)
        if existing is not None and existing is not spec:
            raise AutomationRegistryError(
                f"Action {spec.key!r} is already registered with a different spec. "
                "Check that register_action() is not called twice for the same key."
            )
        _registry[spec.key] = spec


def get_action(key: str) -> ActionSpec:
    """Return the registered ``ActionSpec`` for *key*.

    Raises ``KeyError`` if the action has not been registered.
    """
    try:
        return _registry[key]
    except KeyError:
        raise KeyError(f"No action registered for key {key!r}") from None


def list_actions() -> list[ActionSpec]:
    """Return all registered action specs, ordered by key."""
    with _lock:
        return sorted(_registry.values(), key=lambda s: s.key)


def list_actions_for_module(module: str) -> list[ActionSpec]:
    """Return specs for a single owning module."""
    return [s for s in list_actions() if s.module == module]


def iter_actions() -> Iterator[ActionSpec]:
    with _lock:
        yield from _registry.values()


def reset_registry_for_tests() -> None:
    """Clear the registry — for use in isolated unit tests only."""
    with _lock:
        _registry.clear()
