from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable

from govoplan_core.core.access import (
    CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER,
)
from govoplan_core.core.institutional import GovernedContextEnvelope

AutomationInvocationKind = Literal[
    "manual",
    "api",
    "schedule",
    "event",
    "workflow",
    "dependency",
    "retry",
    "backfill",
]
AutomationSubjectKind = Literal["delegated_user", "service_account"]
AUTOMATION_PRINCIPAL_CONTRACT_VERSION = "1"
ACTION_EFFECT_CONTRACT_VERSION = "1"

ActionRiskLevel = Literal["low", "moderate", "high", "critical"]
ActionReversibility = Literal[
    "reversible",
    "compensatable",
    "corrective_only",
    "irreversible",
]
ActionExecutionState = Literal[
    "pending",
    "running",
    "completed",
    "blocked",
    "retryable",
    "quarantined",
    "manual_required",
    "compensation_required",
]
EffectOperation = Literal[
    "created",
    "changed",
    "deleted",
    "sent",
    "notified",
    "locked",
    "retained",
    "external",
]


@dataclass(frozen=True, slots=True)
class EffectDefinition:
    effect_key: str
    owner_module: str
    operation: EffectOperation
    description: str
    resource_types: tuple[str, ...] = ()
    visibility_classification: str = "internal"
    audit_event_types: tuple[str, ...] = ()
    compensation_hint: str | None = None
    contract_version: str = ACTION_EFFECT_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract_version(self.contract_version)
        _require_text(self.effect_key, "Effect key")
        _require_text(self.owner_module, "Effect owner module")
        _require_text(self.description, "Effect description")


@dataclass(frozen=True, slots=True)
class ActionDefinition:
    action_key: str
    owner_module: str
    description: str
    input_schema_ref: str
    required_scopes: tuple[str, ...] = ()
    required_capabilities: tuple[str, ...] = ()
    policy_checks: tuple[str, ...] = ()
    risk_level: ActionRiskLevel = "moderate"
    reversibility: ActionReversibility = "corrective_only"
    expected_effect_keys: tuple[str, ...] = ()
    idempotency_strategy: str = "caller_supplied"
    audit_event_types: tuple[str, ...] = ()
    preview_required: bool = True
    contract_version: str = ACTION_EFFECT_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract_version(self.contract_version)
        _require_text(self.action_key, "Action key")
        _require_text(self.owner_module, "Action owner module")
        _require_text(self.description, "Action description")
        _require_text(self.input_schema_ref, "Action input schema reference")
        _require_text(self.idempotency_strategy, "Action idempotency strategy")
        if any(not value.strip() for value in self.required_scopes):
            raise ValueError("Action scopes must not be empty")
        if any(not value.strip() for value in self.required_capabilities):
            raise ValueError("Action capabilities must not be empty")
        if any(not value.strip() for value in self.expected_effect_keys):
            raise ValueError("Expected effect keys must not be empty")


@dataclass(frozen=True, slots=True)
class ActionExecutionRequest:
    tenant_id: str
    action_key: str
    input: Mapping[str, object]
    idempotency_key: str
    invocation: AutomationInvocation
    institutional_context: GovernedContextEnvelope | None = None
    actor_ref: str | None = None
    preview_ref: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = ACTION_EFFECT_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract_version(self.contract_version)
        _require_text(self.tenant_id, "Action tenant id")
        _require_text(self.action_key, "Action key")
        _require_text(self.idempotency_key, "Action idempotency key")


@dataclass(frozen=True, slots=True)
class EffectPreview:
    effect_key: str
    summary: str
    resource_refs: tuple[str, ...] = ()
    external_system_refs: tuple[str, ...] = ()


@dataclass(frozen=True, slots=True)
class ActionPreview:
    action_key: str
    allowed: bool
    summary: str
    risk_level: ActionRiskLevel
    reversibility: ActionReversibility
    effects: tuple[EffectPreview, ...] = ()
    blockers: tuple[str, ...] = ()
    policy_provenance: tuple[Mapping[str, object], ...] = ()
    preview_ref: str | None = None


@dataclass(frozen=True, slots=True)
class ObservedEffect:
    effect_key: str
    operation: EffectOperation
    resource_ref: str | None = None
    external_system_ref: str | None = None
    audit_event_ref: str | None = None
    summary: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class ActionExecutionResult:
    state: ActionExecutionState
    output: Mapping[str, object] = field(default_factory=dict)
    observed_effects: tuple[ObservedEffect, ...] = ()
    error: str | None = None
    retry_after: datetime | None = None
    manual_instructions: str | None = None
    compensation_action_key: str | None = None
    audit_event_refs: tuple[str, ...] = ()


@runtime_checkable
class ActionEffectProvider(Protocol):
    def action_definitions(self) -> tuple[ActionDefinition, ...]: ...

    def effect_definitions(self) -> tuple[EffectDefinition, ...]: ...

    def preview_action(
        self,
        session: object,
        principal: object,
        *,
        request: ActionExecutionRequest,
    ) -> ActionPreview: ...

    def execute_action(
        self,
        session: object,
        principal: object,
        *,
        request: ActionExecutionRequest,
    ) -> ActionExecutionResult: ...


@dataclass(frozen=True, slots=True)
class AutomationInvocation:
    kind: AutomationInvocationKind = "manual"
    trigger_ref: str | None = None
    delivery_ref: str | None = None
    event_id: str | None = None
    event_type: str | None = None
    correlation_id: str | None = None
    causation_id: str | None = None
    scheduled_for: datetime | None = None
    requested_by: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class AutomationPrincipalRequest:
    tenant_id: str
    authorization_ref: str
    grant_scopes: tuple[str, ...]
    account_id: str | None = None
    membership_id: str | None = None
    service_account_id: str | None = None
    subject_kind: AutomationSubjectKind = "delegated_user"
    context: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = AUTOMATION_PRINCIPAL_CONTRACT_VERSION

    def __post_init__(self) -> None:
        if (
            self.contract_version
            != AUTOMATION_PRINCIPAL_CONTRACT_VERSION
        ):
            raise ValueError(
                "Unsupported automation-principal contract version"
            )
        if not self.tenant_id.strip():
            raise ValueError("Automation tenant id is required")
        if not self.authorization_ref.strip():
            raise ValueError(
                "Automation authorization artifact reference is required"
            )
        if self.subject_kind == "delegated_user":
            if (
                not self.account_id
                or not self.membership_id
                or self.service_account_id is not None
            ):
                raise ValueError(
                    "Delegated-user automation requires account and "
                    "membership references only"
                )
        elif (
            not self.service_account_id
            or self.account_id is not None
            or self.membership_id is not None
        ):
            raise ValueError(
                "Service-account automation requires only a service-account "
                "reference"
            )
        if any(
            not scope.strip()
            for scope in self.grant_scopes
        ):
            raise ValueError("Automation grant scopes must not be empty")

    @classmethod
    def delegated_user(
        cls,
        *,
        tenant_id: str,
        account_id: str,
        membership_id: str,
        authorization_ref: str,
        grant_scopes: tuple[str, ...],
        context: Mapping[str, object] | None = None,
    ) -> AutomationPrincipalRequest:
        return cls(
            tenant_id=tenant_id,
            account_id=account_id,
            membership_id=membership_id,
            authorization_ref=authorization_ref,
            grant_scopes=grant_scopes,
            context=context or {},
        )

    @classmethod
    def service_account(
        cls,
        *,
        tenant_id: str,
        service_account_id: str,
        authorization_ref: str,
        grant_scopes: tuple[str, ...],
        context: Mapping[str, object] | None = None,
    ) -> AutomationPrincipalRequest:
        return cls(
            tenant_id=tenant_id,
            service_account_id=service_account_id,
            subject_kind="service_account",
            authorization_ref=authorization_ref,
            grant_scopes=grant_scopes,
            context=context or {},
        )


@dataclass(frozen=True, slots=True)
class AutomationPrincipalResolution:
    allowed: bool
    principal: object | None = None
    reason: str | None = None
    granted_scopes: tuple[str, ...] = ()
    missing_scopes: tuple[str, ...] = ()
    provenance: Mapping[str, object] = field(default_factory=dict)


@runtime_checkable
class AutomationPrincipalProvider(Protocol):
    def resolve_automation_principal(
        self,
        session: object,
        *,
        request: AutomationPrincipalRequest,
    ) -> AutomationPrincipalResolution:
        ...


def automation_principal_provider(
    registry: object | None,
) -> AutomationPrincipalProvider | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not registry.has_capability(
            CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER
        )
    ):
        return None
    capability = registry.capability(
        CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER
    )
    return (
        capability
        if isinstance(capability, AutomationPrincipalProvider)
        else None
    )


def action_effect_provider(
    registry: object | None,
    capability_name: str,
) -> ActionEffectProvider | None:
    name = capability_name.strip()
    if (
        not name
        or registry is None
        or not hasattr(registry, "has_capability")
        or not registry.has_capability(name)
    ):
        return None
    capability = registry.capability(name)
    return (
        capability
        if isinstance(capability, ActionEffectProvider)
        else None
    )


def _validate_contract_version(value: str) -> None:
    if value != ACTION_EFFECT_CONTRACT_VERSION:
        raise ValueError("Unsupported action/effect contract version")


def _require_text(value: str, label: str) -> None:
    if not value.strip():
        raise ValueError(f"{label} is required")


__all__ = [
    "ACTION_EFFECT_CONTRACT_VERSION",
    "CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER",
    "AUTOMATION_PRINCIPAL_CONTRACT_VERSION",
    "ActionDefinition",
    "ActionEffectProvider",
    "ActionExecutionRequest",
    "ActionExecutionResult",
    "ActionExecutionState",
    "AutomationInvocation",
    "AutomationInvocationKind",
    "AutomationPrincipalProvider",
    "AutomationPrincipalRequest",
    "AutomationPrincipalResolution",
    "AutomationSubjectKind",
    "ActionPreview",
    "ActionReversibility",
    "ActionRiskLevel",
    "EffectDefinition",
    "EffectOperation",
    "EffectPreview",
    "ObservedEffect",
    "action_effect_provider",
    "automation_principal_provider",
]
