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


CAPABILITY_IDENTITY_TRUST_DIRECTORY = "identity_trust.directory"
CAPABILITY_IDENTITY_TRUST_ASSURANCE = "identity_trust.assurance"
IDENTITY_TRUST_CONTRACT_VERSION = "1"

DeviceKeyPurpose = Literal["encryption", "signing", "encryption_and_signing"]
DeviceKeyStatus = Literal["active", "revoked", "expired"]
TrustSubjectKind = Literal[
    "identity",
    "account",
    "function",
    "postbox",
    "external_recipient",
]

_PRIVATE_JWK_FIELDS = frozenset({"d", "p", "q", "dp", "dq", "qi", "oth", "k"})


@dataclass(frozen=True, slots=True)
class DeviceKeyRegistration:
    tenant_id: str
    identity_id: str
    account_id: str
    device_id: str
    key_id: str
    algorithm: str
    public_jwk: Mapping[str, object]
    purpose: DeviceKeyPurpose = "encryption"
    assurance_level: str = "software"
    attestation_ref: str | None = None
    expires_at: datetime | None = None
    idempotency_key: str = ""
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract(self.contract_version)
        for label, value in (
            ("tenant id", self.tenant_id),
            ("identity id", self.identity_id),
            ("account id", self.account_id),
            ("device id", self.device_id),
            ("key id", self.key_id),
            ("algorithm", self.algorithm),
            ("idempotency key", self.idempotency_key),
        ):
            _require_text(value, label)
        if not self.public_jwk or _PRIVATE_JWK_FIELDS & set(self.public_jwk):
            raise ValueError("Only a bounded public JWK may be registered")


@dataclass(frozen=True, slots=True)
class DeviceKeyRef:
    tenant_id: str
    identity_id: str
    account_id: str
    device_id: str
    key_id: str
    algorithm: str
    public_jwk: Mapping[str, object]
    purpose: DeviceKeyPurpose
    assurance_level: str
    status: DeviceKeyStatus
    epoch: int
    registered_at: datetime
    attestation_ref: str | None = None
    expires_at: datetime | None = None
    revoked_at: datetime | None = None
    revocation_reason: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION


@dataclass(frozen=True, slots=True)
class KeyEpochRotationRequest:
    tenant_id: str
    subject_kind: TrustSubjectKind
    subject_id: str
    reason: str
    access_decision_ref: str
    idempotency_key: str
    history_policy: str = "all_retained"
    previous_epoch: int | None = None
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract(self.contract_version)
        for label, value in (
            ("tenant id", self.tenant_id),
            ("subject id", self.subject_id),
            ("reason", self.reason),
            ("access decision reference", self.access_decision_ref),
            ("idempotency key", self.idempotency_key),
        ):
            _require_text(value, label)


@dataclass(frozen=True, slots=True)
class KeyEpochRef:
    tenant_id: str
    subject_kind: TrustSubjectKind
    subject_id: str
    epoch: int
    state: Literal["active", "superseded", "revoked"]
    history_policy: str
    effective_at: datetime
    previous_epoch: int | None = None
    reason: str | None = None
    access_decision_ref: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION


@dataclass(frozen=True, slots=True)
class KeyAccessRequest:
    tenant_id: str
    account_id: str
    device_key_id: str
    subject_kind: TrustSubjectKind
    subject_id: str
    key_epoch: int
    access_decision_ref: str
    purpose: str
    requested_at: datetime
    function_assignment_id: str | None = None
    delegation_id: str | None = None
    resource_ref: str | None = None
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract(self.contract_version)
        if self.key_epoch < 1:
            raise ValueError("Key epoch must be positive")
        for label, value in (
            ("tenant id", self.tenant_id),
            ("account id", self.account_id),
            ("device key id", self.device_key_id),
            ("subject id", self.subject_id),
            ("access decision reference", self.access_decision_ref),
            ("purpose", self.purpose),
        ):
            _require_text(value, label)


@dataclass(frozen=True, slots=True)
class KeyAccessDecision:
    allowed: bool
    decision_ref: str
    reason: str
    device_key: DeviceKeyRef | None = None
    epoch: KeyEpochRef | None = None
    audit_event_ref: str | None = None
    requirements: tuple[str, ...] = ()
    provenance: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION


@dataclass(frozen=True, slots=True)
class AssuranceCheckRequest:
    tenant_id: str
    account_id: str
    purpose: str
    minimum_level: str
    evidence_ref: str
    evaluated_at: datetime
    maximum_age_seconds: int = 300
    device_key_id: str | None = None
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION

    def __post_init__(self) -> None:
        _validate_contract(self.contract_version)
        if self.maximum_age_seconds < 1:
            raise ValueError("Assurance maximum age must be positive")
        for label, value in (
            ("tenant id", self.tenant_id),
            ("account id", self.account_id),
            ("purpose", self.purpose),
            ("minimum level", self.minimum_level),
            ("evidence reference", self.evidence_ref),
        ):
            _require_text(value, label)


@dataclass(frozen=True, slots=True)
class AssuranceDecision:
    allowed: bool
    reason: str
    assurance_level: str | None = None
    evidence_ref: str | None = None
    verified_at: datetime | None = None
    expires_at: datetime | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION


@runtime_checkable
class IdentityTrustDirectory(Protocol):
    def register_device_key(
        self,
        session: object,
        principal: object,
        *,
        request: DeviceKeyRegistration,
    ) -> DeviceKeyRef: ...

    def revoke_device_key(
        self,
        session: object,
        principal: object,
        *,
        tenant_id: str,
        key_id: str,
        expected_epoch: int,
        reason: str,
    ) -> DeviceKeyRef: ...

    def list_device_keys(
        self,
        session: object,
        principal: object,
        *,
        tenant_id: str,
        account_id: str,
        active_only: bool = True,
    ) -> tuple[DeviceKeyRef, ...]: ...

    def rotate_epoch(
        self,
        session: object,
        principal: object,
        *,
        request: KeyEpochRotationRequest,
    ) -> KeyEpochRef: ...

    def resolve_epoch(
        self,
        session: object,
        *,
        tenant_id: str,
        subject_kind: TrustSubjectKind,
        subject_id: str,
        epoch: int | None = None,
    ) -> KeyEpochRef | None: ...

    def decide_key_access(
        self,
        session: object,
        principal: object,
        *,
        request: KeyAccessRequest,
    ) -> KeyAccessDecision: ...


@runtime_checkable
class IdentityTrustAssurance(Protocol):
    def verify_assurance(
        self,
        session: object,
        principal: object,
        *,
        request: AssuranceCheckRequest,
    ) -> AssuranceDecision: ...


def identity_trust_directory(
    registry: object | None,
) -> IdentityTrustDirectory | None:
    capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_DIRECTORY)
    return capability if isinstance(capability, IdentityTrustDirectory) else None


def identity_trust_assurance(
    registry: object | None,
) -> IdentityTrustAssurance | None:
    capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_ASSURANCE)
    return capability if isinstance(capability, IdentityTrustAssurance) else None


def _capability(registry: object | None, name: str) -> object | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(name)
    ):
        return None
    return registry.capability(name)


def _validate_contract(value: str) -> None:
    if value != IDENTITY_TRUST_CONTRACT_VERSION:
        raise ValueError("Unsupported identity-trust contract version")


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


__all__ = [
    "AssuranceCheckRequest",
    "AssuranceDecision",
    "CAPABILITY_IDENTITY_TRUST_ASSURANCE",
    "CAPABILITY_IDENTITY_TRUST_DIRECTORY",
    "DeviceKeyRef",
    "DeviceKeyRegistration",
    "IdentityTrustAssurance",
    "IdentityTrustDirectory",
    "KeyAccessDecision",
    "KeyAccessRequest",
    "KeyEpochRef",
    "KeyEpochRotationRequest",
    "identity_trust_assurance",
    "identity_trust_directory",
]
