"""Approval — human-in-the-loop gate for AI agents."""

from __future__ import annotations

import functools
import inspect
from typing import Any, Callable, Literal

import httpx

from primitif._models import PaginatedList
from primitif._transport import (
    configure_logging,
    request_with_retries,
)
from primitif.approval.exceptions import (
    ApprovalAuthError,
    ApprovalNetworkError,
    ApprovalError,
    ApprovalNotFoundError,
    ApprovalRateLimitError,
    ApprovalServerError,
    ApprovalValidationError,
)
from primitif.approval.models import ApprovalRequest

logger = configure_logging("approval")

_EXC_MAP = {
    401: ApprovalAuthError,
    404: ApprovalNotFoundError,
    422: ApprovalValidationError,
    429: ApprovalRateLimitError,
}


class ApprovalAdmin:
    """Approval operations — create, check, and list approval requests.

    Not instantiated directly. Access via ``Primitif().approval``.
    """

    def __init__(self, client: httpx.Client, max_retries: int) -> None:
        """Initialize ApprovalAdmin (internal -- use ``Primitif().approval`` instead).

        Args:
            client: Pre-configured httpx.Client with auth headers.
            max_retries: Number of automatic retries on transient errors.
        """
        self._client = client
        self._max_retries = max_retries
        self._registry: dict[str, Callable] = {}

    def _request(self, method: str, path: str, **kwargs) -> dict:
        return request_with_retries(
            self._client,
            method,
            path,
            self._max_retries,
            exc_map=_EXC_MAP,
            server_exc=ApprovalServerError,
            base_exc=ApprovalError,
            connection_exc=ApprovalNetworkError,
            logger=logger,
            **kwargs,
        )

    def require_approval(
        self,
        action: str | None = None,
        *,
        requested_by: str | None = None,
        callback_url: str | None = None,
    ) -> Callable:
        """Decorator that gates a tool behind human approval.

        Wraps a function so that calling it creates an approval request
        instead of executing it.  The function is also registered by action
        name so :meth:`dispatch` can execute it when the webhook fires.

        Args:
            action: Human-readable action name shown to the approver.
                Defaults to the function name with underscores replaced
                by spaces (``send_invoice`` → ``"Send invoice"``).
            requested_by: Optional agent/service identifier attached to
                the approval request.
            callback_url: Optional webhook URL for direct callback when
                the request is decided.

        Returns:
            A decorator.  The decorated function returns an
            :class:`ApprovalRequest` (status ``"pending"``) on every call.

        The original unwrapped function is available as
        ``wrapped.original`` for manual invocation.

        Example::

            @p.approval.require_approval()
            def send_invoice(client, amount):
                billing.send(client, amount)

            result = send_invoice("Acme", 45000)   # ApprovalRequest
            result.approval_url                     # link for the human

        Raises:
            ValueError: If another function is already registered with the
                same action name.
        """

        def decorator(fn: Callable) -> Callable:
            resolved_action = action or fn.__name__.replace("_", " ").capitalize()

            if resolved_action in self._registry:
                raise ValueError(
                    f"Action {resolved_action!r} is already registered"
                )
            self._registry[resolved_action] = fn

            @functools.wraps(fn)
            def wrapper(*args: Any, **kwargs: Any) -> ApprovalRequest:
                sig = inspect.signature(fn)
                bound = sig.bind(*args, **kwargs)
                bound.apply_defaults()
                context = dict(bound.arguments)

                return self.create_request(
                    resolved_action,
                    context=context,
                    requested_by=requested_by,
                    callback_url=callback_url,
                )

            wrapper.original = fn  # type: ignore[attr-defined]
            return wrapper

        return decorator

    def dispatch(self, event: dict) -> Any:
        """Execute the tool registered for an approved webhook event.

        The event should be verified with :func:`verify_webhook` before
        calling this method.  Accepts both payload shapes:

        * **Rich** (platform webhook) — contains ``action`` and
          ``context``; the handler is looked up and called directly.
        * **Minimal** (callback_url) — contains only ``request_id``
          and ``status``; the full record is fetched via
          :meth:`get_request` first.

        Args:
            event: The parsed webhook JSON body.

        Returns:
            The return value of the executed tool, or ``None`` if the
            event status is not ``"approved"``.

        Raises:
            ApprovalError: If no handler is registered for the action.

        Example::

            @app.post("/webhooks/approval")
            def handle():
                event = verify_webhook(...)
                p.approval.dispatch(event)
                return "", 200
        """
        if event.get("status") != "approved":
            return None

        # Minimal payload — fetch full record from API
        if "action" not in event and "request_id" in event:
            req = self.get_request(event["request_id"])
            action = req.action
            ctx = req.context or {}
        else:
            action = event.get("action", "")
            ctx = event.get("context") or {}

        fn = self._registry.get(action)
        if fn is None:
            raise ApprovalError(
                message=f"No handler registered for action {action!r}",
                status_code=0,
            )

        # Validate context keys match the function's expected parameters
        # to prevent untrusted webhook payloads from injecting arguments.
        sig = inspect.signature(fn)
        allowed_keys = set(sig.parameters.keys())
        actual_keys = set(ctx.keys())
        unexpected = actual_keys - allowed_keys
        if unexpected:
            raise ApprovalError(
                message=f"Unexpected keys in context for {action!r}: {unexpected}",
                status_code=0,
            )

        return fn(**ctx)

    def create_request(
        self,
        action: str,
        *,
        context: dict[str, Any] | None = None,
        callback_url: str | None = None,
        requested_by: str | None = None,
    ) -> ApprovalRequest:
        """Create an approval request. Returns immediately with a pending status.

        The agent moves on. No polling. No blocking.
        A webhook fires when a decision is made.

        Args:
            action: Human-readable description of the action awaiting
                approval (e.g. ``"Send contract to Acme"``).
            context: Arbitrary JSON dict passed to the approver and
                included in the webhook payload. Defaults to None.
            callback_url: Optional URL for a direct webhook callback when
                the request is decided. Defaults to None.
            requested_by: Optional agent or service identifier. Defaults to None.

        Returns:
            ApprovalRequest with ``id``, ``status`` (``"pending"``),
            ``approval_url``, and ``expires_at``.

        Raises:
            ApprovalValidationError: If the payload is invalid.
            ApprovalAuthError: If the API key is invalid.

        Example:
            >>> req = p.approval.create_request(
            ...     "Send contract",
            ...     context={"client": "Acme", "amount": 50000},
            ... )
            >>> print(req.approval_url)  # share with the human
        """
        payload: dict[str, Any] = {"action": action}
        if context is not None:
            payload["context"] = context
        if callback_url is not None:
            payload["callback_url"] = callback_url
        if requested_by is not None:
            payload["requested_by"] = requested_by

        data = self._request("POST", "/v1/requests", json=payload)
        return ApprovalRequest(**data)

    def get_request(self, request_id: str) -> ApprovalRequest:
        """Get the current status of an approval request.

        Args:
            request_id: The approval request ID.

        Returns:
            ApprovalRequest with the current ``status`` (``"pending"``,
            ``"approved"``, ``"rejected"``, or ``"expired"``).

        Raises:
            ApprovalNotFoundError: If the request does not exist.
            ApprovalAuthError: If the API key is invalid.

        Example:
            >>> req = p.approval.get_request("apr_abc123")
            >>> print(req.status)
        """
        data = self._request("GET", f"/v1/requests/{request_id}")
        return ApprovalRequest(**data)

    def list_requests(
        self,
        *,
        status: Literal["pending", "approved", "rejected", "expired"] | None = None,
        limit: int = 20,
        cursor: str | None = None,
    ) -> PaginatedList[ApprovalRequest]:
        """List approval requests, optionally filtered by status.

        Args:
            status: Filter by status. One of ``"pending"``, ``"approved"``,
                ``"rejected"``, or ``"expired"``. Omit to list all.
            limit: Maximum number of requests per page. Defaults to 20.
            cursor: Pagination cursor from a previous response.

        Returns:
            PaginatedList[ApprovalRequest] supporting iteration and
            ``.auto_paging_iter()``.

        Raises:
            ApprovalAuthError: If the API key is invalid.

        Example:
            >>> pending = p.approval.list_requests(status="pending")
            >>> for req in pending:
            ...     print(req.action, req.approval_url)
        """
        params: dict[str, Any] = {"limit": limit}
        if status is not None:
            params["status"] = status
        if cursor is not None:
            params["cursor"] = cursor
        data = self._request("GET", "/v1/requests", params=params)
        raw = data.get("data") or data.get("items") or []
        items = [ApprovalRequest(**r) for r in raw]
        next_cursor = data.get("next_cursor")
        result = PaginatedList(
            items=items, cursor=next_cursor, has_more=next_cursor is not None
        )
        result._fetch_fn = self.list_requests
        result._fetch_kwargs = {"status": status, "limit": limit}
        return result
