"""Strict public contracts for manifests, proposals, and decisions."""

from __future__ import annotations

import re
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, Literal

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    field_validator,
    model_validator,
)

_ID_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._:/+-]{2,127}$"
_RUN_ID_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$"
_ACTION_PATTERN = r"^[a-z][a-z0-9_.-]{2,63}(?:\.\*)?$"
_SHA_PATTERN = r"^[a-f0-9]{64}$"


def _utc(value: datetime) -> datetime:
    if value.tzinfo is None or value.utcoffset() is None:
        raise ValueError("timestamp must include a timezone")
    return value.astimezone(UTC)


class Effect(StrEnum):
    ALLOW = "allow"
    DENY = "deny"


class Decision(StrEnum):
    ALLOW = "ALLOW"
    DENY = "DENY"
    PAUSE = "PAUSE"


class ScopeRule(BaseModel):
    """One typed authority rule.

    Resource patterns are exact URIs or a URI ending in ``/**``. HTTP(S)
    patterns may use a single left-most ``*.`` host wildcard. No other glob
    syntax is accepted.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)

    rule_id: str = Field(pattern=_ID_PATTERN)
    effect: Effect
    action: str = Field(pattern=_ACTION_PATTERN)
    resource: str = Field(min_length=3, max_length=2048)
    methods: tuple[str, ...] = ()
    required_attributes: dict[str, Any] = Field(default_factory=dict)
    allow_private_network: bool = False
    resolved_ip_cidrs: tuple[str, ...] = ()
    max_uses: int | None = Field(default=None, ge=1, le=1_000_000)

    @field_validator("methods")
    @classmethod
    def canonical_methods(cls, value: tuple[str, ...]) -> tuple[str, ...]:
        methods = tuple(item.strip().upper() for item in value)
        if len(methods) != len(set(methods)):
            raise ValueError("methods must be unique")
        if any(not method or not method.replace("-", "").isalnum() for method in methods):
            raise ValueError("invalid HTTP method")
        return methods

    @field_validator("required_attributes")
    @classmethod
    def json_attributes(cls, value: dict[str, Any]) -> dict[str, Any]:
        def check(item: Any) -> None:
            if item is None or isinstance(item, (str, int, bool)):
                return
            if isinstance(item, list):
                for child in item:
                    check(child)
                return
            if isinstance(item, dict) and all(isinstance(key, str) for key in item):
                for child in item.values():
                    check(child)
                return
            raise ValueError("required_attributes must contain deterministic JSON values")

        check(value)
        return value

    @model_validator(mode="after")
    def require_exact_shell_argv(self) -> ScopeRule:
        if (
            self.effect == Effect.ALLOW
            and self.action in {"shell.exec", "shell.*"}
            and (
                not isinstance(self.required_attributes.get("argv_sha256"), str)
                or not re.fullmatch(
                    _SHA_PATTERN,
                    self.required_attributes["argv_sha256"],
                )
            )
        ):
            raise ValueError("shell allow rules must bind the exact canonical argv_sha256")
        return self


class ScopeManifest(BaseModel):
    """A principal-signed authority grant for exactly one run."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    schema_version: Literal["1.0"] = "1.0"
    mandate_id: str = Field(pattern=_ID_PATTERN)
    run_id: str = Field(pattern=_RUN_ID_PATTERN)
    principal: str = Field(pattern=_ID_PATTERN)
    agent_id: str = Field(pattern=_ID_PATTERN)
    objective: str = Field(min_length=10, max_length=2000)
    issued_at: datetime
    not_before: datetime
    expires_at: datetime
    nonce: str = Field(pattern=r"^[A-Za-z0-9_-]{16,128}$")
    rules: tuple[ScopeRule, ...] = Field(min_length=1, max_length=256)
    pause_on_scope_uncertainty: bool = True
    ticket_ttl_seconds: int = Field(default=30, ge=1, le=300)
    claim_boundary: str = Field(min_length=20, max_length=2000)

    @field_validator("issued_at", "not_before", "expires_at")
    @classmethod
    def timestamps_are_utc(cls, value: datetime) -> datetime:
        return _utc(value)

    @model_validator(mode="after")
    def validate_manifest(self) -> ScopeManifest:
        if self.not_before < self.issued_at:
            raise ValueError("not_before cannot precede issued_at")
        if self.expires_at <= self.not_before:
            raise ValueError("expires_at must follow not_before")
        rule_ids = [rule.rule_id for rule in self.rules]
        if len(rule_ids) != len(set(rule_ids)):
            raise ValueError("rule ids must be unique")
        if not any(rule.effect == Effect.ALLOW for rule in self.rules):
            raise ValueError("manifest must contain at least one allow rule")
        return self


class SignatureBlock(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    algorithm: Literal["Ed25519"] = "Ed25519"
    key_id: str = Field(pattern=_ID_PATTERN)
    signer_fingerprint: str = Field(pattern=_SHA_PATTERN)
    payload_sha256: str = Field(pattern=_SHA_PATTERN)
    signature_b64: str = Field(min_length=80, max_length=128)


class SignedEnvelope(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    envelope_version: Literal["1.0"] = "1.0"
    payload: dict[str, Any]
    signature: SignatureBlock


class ActionProposal(BaseModel):
    """An action the agent wants an adapter to execute."""

    model_config = ConfigDict(extra="forbid", frozen=True)

    request_id: str = Field(pattern=_ID_PATTERN)
    run_id: str = Field(pattern=_RUN_ID_PATTERN)
    action: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,63}$")
    resource: str = Field(min_length=3, max_length=4096)
    method: str | None = Field(default=None, min_length=1, max_length=32)
    attributes: dict[str, Any] = Field(default_factory=dict)
    justification: str | None = Field(default=None, max_length=4000)

    @field_validator("method")
    @classmethod
    def normalize_method(cls, value: str | None) -> str | None:
        return value.strip().upper() if value is not None else None


class ActionDecision(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    decision: Decision
    request_id: str
    run_id: str
    action: str
    normalized_resource: str | None
    request_sha256: str = Field(pattern=_SHA_PATTERN)
    reasons: tuple[str, ...]
    matched_rule_id: str | None = None
    uncertainty_signals: tuple[str, ...] = ()
    ticket: str | None = None
    ledger_head_sha256: str | None = Field(default=None, pattern=_SHA_PATTERN)


class RunStart(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    run_id: str
    mandate_id: str
    manifest_sha256: str = Field(pattern=_SHA_PATTERN)
    attestation: str
    expires_at: datetime
    ledger_head_sha256: str = Field(pattern=_SHA_PATTERN)


class AuditPacket(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    packet_version: Literal["1.0"] = "1.0"
    run_id: str
    mandate_id: str
    manifest_sha256: str = Field(pattern=_SHA_PATTERN)
    generated_at: datetime
    ledger_head_sha256: str = Field(pattern=_SHA_PATTERN)
    event_count: int = Field(ge=1)
    decision_counts: dict[str, int]
    claim_boundary: str
