"""Action registry: declarative business operations agents may invoke.

Each action wraps a plain Python callable behind a typed schema. The schema
gives:

* Validation before invocation (no random kwargs).
* Auditability — every invocation is logged with the resolved payload.
* Approval gating — actions flagged ``requires_approval`` must be approved
  via the AI audit trail before they execute.

Actions live in-process; modules register them in their own ``ai.py``
module (auto-discovered by Phase 7 onwards).
"""

from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

_DOTTED = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
_PARAM_TYPES = frozenset(
    {"string", "integer", "decimal", "boolean", "date", "datetime", "uuid", "json", "ref"},
)


class ActionError(ValueError):
    """Raised on registration errors or invalid invocations."""


@dataclass(frozen=True)
class ActionParam:
    name: str
    type: str
    required: bool = True
    description: str = ""

    def __post_init__(self) -> None:
        if not self.name.isidentifier():
            raise ActionError(f"invalid param name {self.name!r}")
        if self.type not in _PARAM_TYPES:
            raise ActionError(
                f"param {self.name!r}: unknown type {self.type!r}; "
                f"allowed: {sorted(_PARAM_TYPES)}",
            )


@dataclass(frozen=True)
class ActionSpec:
    key: str                                  # dotted "module.action"
    label_key: str
    handler: Callable[..., Any]
    params: tuple[ActionParam, ...] = ()
    description: str = ""
    permission: str = ""                      # iam codename gating execution
    requires_approval: bool = False
    target_entity: str = ""                   # optional semantic entity key

    def __post_init__(self) -> None:
        if not _DOTTED.match(self.key):
            raise ActionError(f"action key must be 'module.action', got {self.key!r}")
        if not callable(self.handler):
            raise ActionError(f"action {self.key!r}: handler is not callable")
        names = [p.name for p in self.params]
        if len(set(names)) != len(names):
            raise ActionError(f"action {self.key!r}: duplicate param names")

    def param(self, name: str) -> ActionParam:
        for p in self.params:
            if p.name == name:
                return p
        raise ActionError(f"action {self.key!r}: unknown param {name!r}")


_ACTIONS: dict[str, ActionSpec] = {}


def register_action(spec: ActionSpec) -> ActionSpec:
    existing = _ACTIONS.get(spec.key)
    if existing is not None and existing is not spec:
        raise ActionError(
            f"action {spec.key!r} already registered with a different handler",
        )
    _ACTIONS[spec.key] = spec
    return spec


def get_action(key: str) -> ActionSpec:
    try:
        return _ACTIONS[key]
    except KeyError as exc:
        raise ActionError(f"unknown action {key!r}") from exc


def list_actions() -> list[ActionSpec]:
    return sorted(_ACTIONS.values(), key=lambda s: s.key)


def reset_for_tests() -> None:
    _ACTIONS.clear()


def validate_payload(spec: ActionSpec, payload: dict[str, Any]) -> dict[str, Any]:
    """Return a sanitised copy of ``payload`` or raise :class:`ActionError`."""
    cleaned: dict[str, Any] = {}
    declared = {p.name for p in spec.params}
    unknown = set(payload) - declared
    if unknown:
        raise ActionError(
            f"action {spec.key!r}: unknown params {sorted(unknown)}",
        )
    for param in spec.params:
        if param.name in payload:
            cleaned[param.name] = payload[param.name]
        elif param.required:
            raise ActionError(
                f"action {spec.key!r}: missing required param {param.name!r}",
            )
    return cleaned


def serialize_action(spec: ActionSpec) -> dict:
    return {
        "key": spec.key,
        "label_key": spec.label_key,
        "description": spec.description,
        "permission": spec.permission,
        "requires_approval": spec.requires_approval,
        "target_entity": spec.target_entity,
        "params": [
            {
                "name": p.name,
                "type": p.type,
                "required": p.required,
                "description": p.description,
            }
            for p in spec.params
        ],
    }
