"""Generic requirement fulfillment semantics.

Requirements are deliberately broader than documents. A requirement may be
fulfilled by a file, a record, a verified fact, an approval, or an explicit
waiver. Storage and evidence lifecycle remain owned by the existing records
and files contracts.
"""
from __future__ import annotations

from enum import StrEnum
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator


class FulfillmentStatus(StrEnum):
    REQUIRED = "required"
    SUBMITTED = "submitted"
    APPROVED = "approved"
    REJECTED = "rejected"
    WAIVED = "waived"


class RequirementDefinition(BaseModel):
    """Reusable template copied into a case/application as an instance."""

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

    key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    label: str = Field(min_length=1, max_length=200)
    description: str | None = None
    evidence_kind: Literal["file", "record", "fact", "approval", "none"] = "file"
    required: bool = True
    applicability: dict[str, object] | None = None


class RequirementInstance(BaseModel):
    """A definition applied to one opaque subject record."""

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

    definition_key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    subject_entity_key: str = Field(pattern=r"^[a-z][a-z0-9_]{0,62}$")
    subject_record_id: str = Field(min_length=1, max_length=200)
    status: FulfillmentStatus = FulfillmentStatus.REQUIRED
    evidence_entity_key: str | None = None
    evidence_record_id: str | None = None
    reviewed_by: str | None = None
    review_reason: str | None = None

    @model_validator(mode="after")
    def evidence_is_paired(self) -> "RequirementInstance":
        if (self.evidence_entity_key is None) != (self.evidence_record_id is None):
            raise ValueError("evidence_entity_key and evidence_record_id must be paired")
        if self.status in {FulfillmentStatus.APPROVED, FulfillmentStatus.REJECTED} and not self.reviewed_by:
            raise ValueError("reviewed_by is required for approved or rejected requirements")
        if self.status == FulfillmentStatus.WAIVED and not self.review_reason:
            raise ValueError("review_reason is required for waived requirements")
        return self


class RequirementTransition(BaseModel):
    """Explicit state change; callers must persist the actor and reason."""

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

    from_status: FulfillmentStatus
    to_status: FulfillmentStatus
    actor_user_id: str = Field(min_length=1)
    reason: str | None = None

    @model_validator(mode="after")
    def validate_transition(self) -> "RequirementTransition":
        allowed = {
            FulfillmentStatus.REQUIRED: {FulfillmentStatus.SUBMITTED, FulfillmentStatus.WAIVED},
            FulfillmentStatus.SUBMITTED: {FulfillmentStatus.APPROVED, FulfillmentStatus.REJECTED},
            FulfillmentStatus.REJECTED: {FulfillmentStatus.SUBMITTED, FulfillmentStatus.WAIVED},
            FulfillmentStatus.APPROVED: set(),
            FulfillmentStatus.WAIVED: set(),
        }
        if self.to_status not in allowed[self.from_status]:
            raise ValueError(f"invalid requirement transition: {self.from_status} -> {self.to_status}")
        if self.to_status in {FulfillmentStatus.REJECTED, FulfillmentStatus.WAIVED} and not self.reason:
            raise ValueError("reason is required when rejecting or waiving a requirement")
        return self
