"""Data models for MailioReceipt v1."""

from __future__ import annotations

import json
import re
import uuid
from datetime import datetime, timezone
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator

from mailio_receipt.canonicalize import compute_digest


class IntentKind:
    """Intent kind constants for v1 lifecycle events."""

    ACTION_PLANNED = "mailio.workflow.action.planned"
    APPROVAL_REQUESTED = "mailio.workflow.approval.requested"
    APPROVAL_GRANTED = "mailio.workflow.approval.granted"
    APPROVAL_REVOKED = "mailio.workflow.approval.revoked"
    ACTION_EXECUTED = "mailio.workflow.action.executed"
    ACTION_FAILED = "mailio.workflow.action.failed"
    NOTIFY = "mailio.workflow.notify"


# Regex patterns for validation
UUID_PATTERN = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
)
SHA256_HEX_PATTERN = re.compile(r"^[0-9a-f]{64}$", re.IGNORECASE)
SHA256_B64URL_PATTERN = re.compile(r"^[A-Za-z0-9_-]{43}=?$")
ISO8601_DURATION_PATTERN = re.compile(
    r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$"
)


class _Base(BaseModel):
    """Shared config for all receipt models."""

    model_config = ConfigDict(frozen=True)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return self.model_dump(mode="json", exclude_none=True)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Any:
        """Create from dictionary."""
        return cls.model_validate(data)


class Actors(_Base):
    """Identifies participants in an event."""

    issuer: str
    subject: str
    authority: str | None = None
    others: tuple[str, ...] | None = None

    @field_validator("issuer")
    @classmethod
    def issuer_not_empty(cls, v: str) -> str:
        if not v:
            raise ValueError("issuer must not be empty")
        return v

    @field_validator("subject")
    @classmethod
    def subject_not_empty(cls, v: str) -> str:
        if not v:
            raise ValueError("subject must not be empty")
        return v


class Intent(_Base):
    """Describes what the event represents."""

    kind: str
    action: str
    category: str
    scope: dict[str, Any] = Field(default_factory=dict)


class Chain(_Base):
    """Links this event to its predecessor."""

    prev_receipt_digest: str
    prev_receipt_ref: str | None = None
    reason: str | None = None

    @field_validator("prev_receipt_digest")
    @classmethod
    def validate_prev_receipt_digest(cls, v: str) -> str:
        if not SHA256_HEX_PATTERN.match(v) and not SHA256_B64URL_PATTERN.match(v):
            raise ValueError(
                f"prev_receipt_digest must be a valid SHA-256 digest (hex or base64url), got: {v}"
            )
        return v


class ArtifactBinding(_Base):
    """Binds an artifact to the event."""

    name: str
    digest: str
    media_type: str | None = None

    @field_validator("digest")
    @classmethod
    def validate_digest(cls, v: str) -> str:
        if not SHA256_HEX_PATTERN.match(v) and not SHA256_B64URL_PATTERN.match(v):
            raise ValueError(
                f"digest must be a valid SHA-256 digest (hex or base64url), got: {v}"
            )
        return v


class MessageBinding(_Base):
    """Binds a message to the event."""

    message_id: str
    thread_id: str | None = None


class Binding(_Base):
    """Links artifacts and messages to the event."""

    artifacts: tuple[ArtifactBinding, ...] | None = None
    message: MessageBinding | None = None


class SignedMetadata(_Base):
    """Metadata about what was signed."""

    canonicalization: Literal["RFC8785"] = "RFC8785"
    digest: str = ""
    target: Literal["/event"] = "/event"

    @field_validator("digest")
    @classmethod
    def validate_digest(cls, v: str) -> str:
        if v and not SHA256_HEX_PATTERN.match(v) and not SHA256_B64URL_PATTERN.match(v):
            raise ValueError(
                f"digest must be a valid SHA-256 digest (hex or base64url), got: {v}"
            )
        return v


class Signature(_Base):
    """Explicit cryptographic signature over the event."""

    role: Literal["user", "agent", "authority"]
    signer: str
    alg: Literal["ES256", "EdDSA"]
    signed: SignedMetadata
    jws: str

    @field_validator("signer")
    @classmethod
    def signer_not_empty(cls, v: str) -> str:
        if not v:
            raise ValueError("signer must not be empty")
        return v


class MailioAuditEvent(_Base):
    """The semantic content of a receipt."""

    type: Literal["MailioAuditEvent"] = "MailioAuditEvent"
    v: Literal["v1"] = "v1"
    id: str = ""
    flow_id: str = ""
    ts: str = ""
    actors: Actors = Actors(issuer="did:web:x", subject="did:web:x")
    intent: Intent = Intent(kind="", action="", category="")
    chain: Chain | None = None
    binding: Binding | None = None
    ttl: str | None = None
    evidence: dict[str, Any] | None = None
    meta: dict[str, Any] | None = None

    @field_validator("id")
    @classmethod
    def validate_id(cls, v: str) -> str:
        if v and not UUID_PATTERN.match(v):
            raise ValueError(f"id must be a valid UUID, got: {v}")
        return v

    @field_validator("flow_id")
    @classmethod
    def validate_flow_id(cls, v: str) -> str:
        if v and not UUID_PATTERN.match(v):
            raise ValueError(f"flow_id must be a valid UUID, got: {v}")
        return v

    @field_validator("ttl")
    @classmethod
    def validate_ttl(cls, v: str | None) -> str | None:
        if v is not None and not ISO8601_DURATION_PATTERN.match(v):
            raise ValueError(f"ttl must be valid ISO8601 duration, got: {v}")
        return v


def create_event(
    *,
    flow_id: str,
    actors: Actors,
    intent: Intent,
    chain: Chain | None = None,
    binding: Binding | None = None,
    ttl: str | None = None,
    evidence: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    event_id: str | None = None,
    timestamp: datetime | None = None,
) -> MailioAuditEvent:
    """
    Create a MailioAuditEvent with sensible defaults.

    Args:
        flow_id: Workflow instance ID (UUID string)
        actors: Participants in this event
        intent: What this event represents
        chain: Optional link to previous receipt
        binding: Optional artifact/message bindings
        ttl: Optional ISO8601 duration for expiration
        meta: Optional arbitrary metadata
        event_id: Optional event ID (UUID v4 generated if None)
        timestamp: Optional timestamp (UTC now if None)

    Returns:
        MailioAuditEvent ready for signing
    """
    if event_id is None:
        event_id = str(uuid.uuid4())
    if timestamp is None:
        timestamp = datetime.now(timezone.utc)

    ts = timestamp.isoformat()

    return MailioAuditEvent(
        type="MailioAuditEvent",
        v="v1",
        id=event_id,
        flow_id=flow_id,
        ts=ts,
        actors=actors,
        intent=intent,
        chain=chain,
        binding=binding,
        ttl=ttl,
        evidence=evidence,
        meta=meta,
    )


class MailioReceipt(_Base):
    """The top-level container for a signed event."""

    type: Literal["MailioReceipt"] = "MailioReceipt"
    v: Literal["v1"] = "v1"
    event: MailioAuditEvent
    event_digest: str = ""
    signatures: tuple[Signature, ...] = ()

    @field_validator("event_digest")
    @classmethod
    def validate_event_digest(cls, v: str) -> str:
        if v and not SHA256_HEX_PATTERN.match(v) and not SHA256_B64URL_PATTERN.match(v):
            raise ValueError(
                f"event_digest must be a valid SHA-256 digest (hex or base64url), got: {v}"
            )
        return v

    @field_validator("signatures")
    @classmethod
    def validate_signatures(cls, v: tuple[Signature, ...]) -> tuple[Signature, ...]:
        if not v:
            raise ValueError("signatures must contain at least one signature")
        return v

    @property
    def digest(self) -> str:
        """Compute digest of entire receipt (for chaining)."""
        return compute_digest(self.to_dict())

    def to_json(self) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict(), indent=2)

    @classmethod
    def from_json(cls, json_str: str) -> MailioReceipt:
        """Create from JSON string."""
        data = json.loads(json_str)
        return cls.model_validate(data)
