"""High-level façade combining the registries with the audit trail."""

from __future__ import annotations

from typing import Any

from django.db import transaction

from simorgh.apps.ai.actions import ActionError, get_action, validate_payload
from simorgh.apps.ai.models import (
    ActionLogStatus,
    AIActionLog,
    AIApproval,
    AISuggestion,
    ApprovalDecision,
    SuggestionKind,
    SuggestionStatus,
)
from simorgh.apps.events.bus import dispatch
from simorgh.apps.organizations.models import OrganizationNode
from simorgh.apps.tenants.models import Tenant
from simorgh.core.audit import record_event


class AIServiceError(RuntimeError):
    """Raised on service-level errors (approval flow, missing prereqs, ...)."""


def _tenant_root(tenant: Tenant) -> OrganizationNode:
    node = (
        OrganizationNode.objects.filter(tenant=tenant)
        .order_by("depth", "pk")
        .first()
    )
    if node is None:
        raise AIServiceError(
            f"tenant {tenant.slug!r} has no organization nodes — bootstrap one first",
        )
    return node


def _audit(action: str, tenant: Tenant, resource_id: Any, after: dict[str, Any]) -> None:
    record_event(
        action,
        resource_type="ai",
        resource_id=resource_id,
        after=after,
        tenant_id=tenant.pk,
    )


# ---------------------------------------------------------------------------
# Suggestions
# ---------------------------------------------------------------------------
@transaction.atomic
def create_suggestion(
    tenant: Tenant,
    *,
    title: str,
    kind: str = SuggestionKind.AGENT_OUTPUT,
    summary: str = "",
    source: str = "",
    proposed_action: str = "",
    payload: dict[str, Any] | None = None,
    metadata: dict[str, Any] | None = None,
    created_by_id: int | None = None,
) -> AISuggestion:
    if kind not in dict(SuggestionKind.choices):
        raise AIServiceError(f"unknown suggestion kind {kind!r}")
    if proposed_action:
        # Fail fast if the proposed action is unknown.
        get_action(proposed_action)
    anchor = _tenant_root(tenant)
    suggestion = AISuggestion.objects.create(
        tenant=tenant,
        organization_node=anchor,
        kind=kind,
        title=title,
        summary=summary,
        source=source,
        proposed_action=proposed_action,
        payload=payload or {},
        metadata=metadata or {},
        created_by_id=created_by_id,
    )
    _audit(
        "ai.suggestion_created",
        tenant,
        str(suggestion.public_id),
        {"kind": kind, "title": title, "source": source},
    )
    dispatch(
        "ai.suggestion_created",
        {
            "tenant_id": tenant.pk,
            "kind": kind,
            "suggestion_id": str(suggestion.public_id),
        },
    )
    return suggestion


@transaction.atomic
def decide_suggestion(
    tenant: Tenant,
    suggestion_id: str,
    *,
    decision: str,
    actor_id: int | None = None,
    note: str = "",
) -> AIApproval:
    if decision not in dict(ApprovalDecision.choices):
        raise AIServiceError(f"unknown decision {decision!r}")
    try:
        suggestion = AISuggestion.objects.get(tenant=tenant, public_id=suggestion_id)
    except AISuggestion.DoesNotExist as exc:
        raise AIServiceError(f"unknown suggestion {suggestion_id!r}") from exc
    if suggestion.status not in {SuggestionStatus.PENDING, SuggestionStatus.APPROVED}:
        raise AIServiceError(
            f"suggestion {suggestion_id!r} is {suggestion.status}; cannot decide",
        )
    anchor = _tenant_root(tenant)
    approval, _created = AIApproval.objects.update_or_create(
        tenant=tenant,
        suggestion=suggestion,
        actor_id=actor_id,
        defaults={
            "organization_node": anchor,
            "decision": decision,
            "note": note,
        },
    )
    if decision == ApprovalDecision.APPROVE:
        suggestion.status = SuggestionStatus.APPROVED
        event_name = "ai.suggestion_approved"
    else:
        suggestion.status = SuggestionStatus.REJECTED
        event_name = "ai.suggestion_rejected"
    suggestion.save(update_fields=("status", "updated_at"))
    _audit(
        event_name,
        tenant,
        str(suggestion.public_id),
        {"decision": decision, "actor_id": actor_id},
    )
    dispatch(
        event_name,
        {
            "tenant_id": tenant.pk,
            "suggestion_id": str(suggestion.public_id),
            "actor_id": actor_id,
        },
    )
    return approval


# ---------------------------------------------------------------------------
# Action execution
# ---------------------------------------------------------------------------
@transaction.atomic
def execute_action(
    tenant: Tenant,
    action_key: str,
    *,
    payload: dict[str, Any] | None = None,
    actor_id: int | None = None,
    suggestion_id: str | None = None,
) -> AIActionLog:
    """Run an action with validation + audit + suggestion linkage.

    If the action ``requires_approval``, the caller must pass an approved
    ``suggestion_id``. Otherwise the action is logged with
    ``status=PENDING_APPROVAL`` and *not* executed.
    """
    spec = get_action(action_key)
    anchor = _tenant_root(tenant)
    raw_payload = dict(payload or {})

    suggestion = None
    if suggestion_id:
        try:
            suggestion = AISuggestion.objects.get(tenant=tenant, public_id=suggestion_id)
        except AISuggestion.DoesNotExist as exc:
            raise AIServiceError(f"unknown suggestion {suggestion_id!r}") from exc

    if spec.requires_approval and (
        suggestion is None or suggestion.status != SuggestionStatus.APPROVED
    ):
        log = AIActionLog.objects.create(
            tenant=tenant,
            organization_node=anchor,
            action_key=action_key,
            status=ActionLogStatus.PENDING_APPROVAL,
            payload=raw_payload,
            actor_id=actor_id,
            suggestion=suggestion,
        )
        _audit(
            "ai.action_executed",
            tenant,
            action_key,
            {"status": ActionLogStatus.PENDING_APPROVAL, "actor_id": actor_id},
        )
        dispatch(
            "ai.action_executed",
            {
                "tenant_id": tenant.pk,
                "action": action_key,
                "status": ActionLogStatus.PENDING_APPROVAL,
            },
        )
        return log

    try:
        cleaned = validate_payload(spec, raw_payload)
    except ActionError as exc:
        log = AIActionLog.objects.create(
            tenant=tenant,
            organization_node=anchor,
            action_key=action_key,
            status=ActionLogStatus.REJECTED,
            payload=raw_payload,
            error=str(exc),
            actor_id=actor_id,
            suggestion=suggestion,
        )
        _audit(
            "ai.action_executed",
            tenant,
            action_key,
            {"status": ActionLogStatus.REJECTED, "error": str(exc)},
        )
        dispatch(
            "ai.action_executed",
            {
                "tenant_id": tenant.pk,
                "action": action_key,
                "status": ActionLogStatus.REJECTED,
            },
        )
        return log

    try:
        result = spec.handler(**cleaned)
        status_value = ActionLogStatus.SUCCEEDED
        error = ""
    except Exception as exc:
        result = None
        status_value = ActionLogStatus.FAILED
        error = f"{type(exc).__name__}: {exc}"

    log = AIActionLog.objects.create(
        tenant=tenant,
        organization_node=anchor,
        action_key=action_key,
        status=status_value,
        payload=cleaned,
        result=result if isinstance(result, dict) else {"value": result},
        error=error,
        actor_id=actor_id,
        suggestion=suggestion,
    )
    if suggestion is not None and status_value == ActionLogStatus.SUCCEEDED:
        suggestion.status = SuggestionStatus.APPLIED
        suggestion.save(update_fields=("status", "updated_at"))
    _audit(
        "ai.action_executed",
        tenant,
        action_key,
        {"status": status_value, "actor_id": actor_id},
    )
    dispatch(
        "ai.action_executed",
        {
            "tenant_id": tenant.pk,
            "action": action_key,
            "status": status_value,
        },
    )
    return log


def list_suggestions(
    tenant: Tenant,
    *,
    status: str | None = None,
    kind: str | None = None,
    limit: int = 100,
) -> list[AISuggestion]:
    qs = AISuggestion.objects.filter(tenant=tenant)
    if status:
        qs = qs.filter(status=status)
    if kind:
        qs = qs.filter(kind=kind)
    return list(qs[:limit])


def list_action_logs(
    tenant: Tenant,
    *,
    action_key: str | None = None,
    limit: int = 100,
) -> list[AIActionLog]:
    qs = AIActionLog.objects.filter(tenant=tenant)
    if action_key:
        qs = qs.filter(action_key=action_key)
    return list(qs[:limit])
