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

from __future__ import annotations

import json
import re
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Literal

from mailio_receipt.canonicalize import compute_digest
from mailio_receipt.errors import ValidationError


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}=?$")
DID_PATTERN = re.compile(r"^did:")
ISO8601_DURATION_PATTERN = re.compile(
    r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$"
)


def _validate_uuid(value: str, field_name: str) -> None:
    """Validate UUID format."""
    if not UUID_PATTERN.match(value):
        raise ValidationError(f"{field_name} must be a valid UUID, got: {value}")


def _validate_did(value: str, field_name: str) -> None:
    """Validate DID or identifier format."""
    if not value:
        raise ValidationError(f"{field_name} must not be empty")


def _validate_sha256_hex(value: str, field_name: str) -> None:
    """Validate SHA-256 digest in hex or base64url format."""
    if not SHA256_HEX_PATTERN.match(value) and not SHA256_B64URL_PATTERN.match(value):
        raise ValidationError(f"{field_name} must be a valid SHA-256 digest (hex or base64url), got: {value}")


def _validate_iso8601_duration(value: str, field_name: str) -> None:
    """Validate ISO8601 duration format."""
    if not ISO8601_DURATION_PATTERN.match(value):
        raise ValidationError(f"{field_name} must be valid ISO8601 duration, got: {value}")


def _to_camel_case(snake_str: str) -> str:
    """Convert snake_case to camelCase."""
    components = snake_str.split("_")
    return components[0] + "".join(x.title() for x in components[1:])


def _to_snake_case(camel_str: str) -> str:
    """Convert camelCase to snake_case."""
    result = []
    for i, char in enumerate(camel_str):
        if char.isupper() and i > 0:
            result.append("_")
        result.append(char.lower())
    return "".join(result)


@dataclass(frozen=True)
class Actors:
    """Identifies participants in an event."""

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

    def __post_init__(self) -> None:
        _validate_did(self.issuer, "issuer")
        _validate_did(self.subject, "subject")
        if self.authority is not None:
            _validate_did(self.authority, "authority")
        if self.others is not None:
            for i, did in enumerate(self.others):
                _validate_did(did, f"others[{i}]")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {
            "issuer": self.issuer,
            "subject": self.subject,
        }
        if self.authority is not None:
            result["authority"] = self.authority
        if self.others is not None:
            result["others"] = list(self.others)
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Actors:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        others = data.get("others")
        return cls(
            issuer=data["issuer"],
            subject=data["subject"],
            authority=data.get("authority"),
            others=tuple(others) if others else None,
        )


@dataclass(frozen=True)
class Intent:
    """Describes what the event represents."""

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

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        return {
            "kind": self.kind,
            "action": self.action,
            "category": self.category,
            "scope": self.scope,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Intent:
        """Create from dictionary."""
        return cls(
            kind=data["kind"],
            action=data["action"],
            category=data["category"],
            scope=data.get("scope", {}),
        )


@dataclass(frozen=True)
class Chain:
    """Links this event to its predecessor."""

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

    def __post_init__(self) -> None:
        _validate_sha256_hex(self.prev_receipt_digest, "prev_receipt_digest")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {
            "prevReceiptDigest": self.prev_receipt_digest,
        }
        if self.prev_receipt_ref is not None:
            result["prevReceiptRef"] = self.prev_receipt_ref
        if self.reason is not None:
            result["reason"] = self.reason
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Chain:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        return cls(
            prev_receipt_digest=data.get("prevReceiptDigest") or data.get("prev_receipt_digest", ""),
            prev_receipt_ref=data.get("prevReceiptRef") or data.get("prev_receipt_ref"),
            reason=data.get("reason"),
        )


@dataclass(frozen=True)
class ArtifactBinding:
    """Binds an artifact to the event."""

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

    def __post_init__(self) -> None:
        _validate_sha256_hex(self.digest, "digest")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {
            "name": self.name,
            "digest": self.digest,
        }
        if self.media_type is not None:
            result["mediaType"] = self.media_type
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ArtifactBinding:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        return cls(
            name=data["name"],
            digest=data["digest"],
            media_type=data.get("mediaType") or data.get("media_type"),
        )


@dataclass(frozen=True)
class MessageBinding:
    """Binds a message to the event."""

    message_id: str
    thread_id: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {
            "messageId": self.message_id,
        }
        if self.thread_id is not None:
            result["threadId"] = self.thread_id
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MessageBinding:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        return cls(
            message_id=data.get("messageId") or data.get("message_id", ""),
            thread_id=data.get("threadId") or data.get("thread_id"),
        )


@dataclass(frozen=True)
class Binding:
    """Links artifacts and messages to the event."""

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

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {}
        if self.artifacts is not None:
            result["artifacts"] = [a.to_dict() for a in self.artifacts]
        if self.message is not None:
            result["message"] = self.message.to_dict()
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Binding:
        """Create from dictionary."""
        artifacts = data.get("artifacts")
        message = data.get("message")
        return cls(
            artifacts=tuple(ArtifactBinding.from_dict(a) for a in artifacts) if artifacts else None,
            message=MessageBinding.from_dict(message) if message else None,
        )


@dataclass(frozen=True)
class SignedMetadata:
    """Metadata about what was signed."""

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

    def __post_init__(self) -> None:
        if self.digest:
            _validate_sha256_hex(self.digest, "digest")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "canonicalization": self.canonicalization,
            "digest": self.digest,
            "target": self.target,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SignedMetadata:
        """Create from dictionary."""
        return cls(
            canonicalization=data.get("canonicalization", "RFC8785"),
            digest=data.get("digest", ""),
            target=data.get("target", "/event"),
        )


@dataclass(frozen=True)
class Signature:
    """Explicit cryptographic signature over the event."""

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

    def __post_init__(self) -> None:
        _validate_did(self.signer, "signer")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "role": self.role,
            "signer": self.signer,
            "alg": self.alg,
            "signed": self.signed.to_dict(),
            "jws": self.jws,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> Signature:
        """Create from dictionary."""
        return cls(
            role=data["role"],
            signer=data["signer"],
            alg=data["alg"],
            signed=SignedMetadata.from_dict(data["signed"]),
            jws=data["jws"],
        )


@dataclass(frozen=True)
class MailioAuditEvent:
    """The semantic content of a receipt."""

    type: Literal["MailioAuditEvent"] = "MailioAuditEvent"
    v: Literal["v1"] = "v1"
    id: str = ""
    flow_id: str = ""
    ts: str = ""
    actors: Actors = field(default_factory=lambda: Actors(issuer="did:web:x", subject="did:web:x"))
    intent: Intent = field(default_factory=lambda: 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

    def __post_init__(self) -> None:
        if self.id:
            _validate_uuid(self.id, "id")
        if self.flow_id:
            _validate_uuid(self.flow_id, "flow_id")
        if self.ttl is not None:
            _validate_iso8601_duration(self.ttl, "ttl")

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        result: dict[str, Any] = {
            "type": self.type,
            "v": self.v,
            "id": self.id,
            "flowId": self.flow_id,
            "ts": self.ts,
            "actors": self.actors.to_dict(),
            "intent": self.intent.to_dict(),
        }
        if self.chain is not None:
            result["chain"] = self.chain.to_dict()
        if self.binding is not None:
            result["binding"] = self.binding.to_dict()
        if self.ttl is not None:
            result["ttl"] = self.ttl
        if self.evidence is not None:
            result["evidence"] = self.evidence
        if self.meta is not None:
            result["meta"] = self.meta
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MailioAuditEvent:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        chain = data.get("chain")
        binding = data.get("binding")
        return cls(
            type=data.get("type", "MailioAuditEvent"),
            v=data.get("v", "v1"),
            id=data.get("id", ""),
            flow_id=data.get("flowId") or data.get("flow_id", ""),
            ts=data.get("ts", ""),
            actors=Actors.from_dict(data["actors"]),
            intent=Intent.from_dict(data["intent"]),
            chain=Chain.from_dict(chain) if chain else None,
            binding=Binding.from_dict(binding) if binding else None,
            ttl=data.get("ttl"),
            evidence=data.get("evidence"),
            meta=data.get("meta"),
        )


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,
    )


@dataclass(frozen=True)
class MailioReceipt:
    """The top-level container for a signed event."""

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

    def __post_init__(self) -> None:
        if self.event_digest:
            _validate_sha256_hex(self.event_digest, "event_digest")
        if not self.signatures:
            raise ValidationError("signatures must contain at least one signature")

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

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary with camelCase keys."""
        return {
            "type": self.type,
            "v": self.v,
            "event": self.event.to_dict(),
            "eventDigest": self.event_digest,
            "signatures": [s.to_dict() for s in self.signatures],
        }

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

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MailioReceipt:
        """Create from dictionary (accepts both camelCase and snake_case)."""
        return cls(
            type=data.get("type", "MailioReceipt"),
            v=data.get("v", "v1"),
            event=MailioAuditEvent.from_dict(data["event"]),
            event_digest=data.get("eventDigest") or data.get("event_digest", ""),
            signatures=tuple(Signature.from_dict(s) for s in data["signatures"]),
        )

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