"""Pydantic v2 models for domain_policy.yaml.

Loader 가 yaml → DomainPolicy 로 파싱. Schema 위반 시 ValidationError → 배포 차단.
"""

from __future__ import annotations

from typing import Literal

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


# documents.category enum (migration 143 + 152)
UICategory = Literal["document", "library", "news", "memo", "audio", "video", "law"]

SelfDeclareSemantics = Literal["additive_trigger_only"]


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

    description: str
    suggested_ui_category: UICategory
    high_impact: bool = False
    default_risk_flags: tuple[str, ...] = ()
    deep_summary_risk_flags: tuple[str, ...] = ()
    keywords: tuple[str, ...] = ()
    note: str | None = None


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

    name: str
    description: str
    suggested_ui_category: UICategory
    high_impact: bool = False
    default_risk_flags: tuple[str, ...] = ()
    requires_human_review: bool = True


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

    description: str
    requires_26b: bool
    synthesis_directive: str | None = None
    output_mask_required: bool = False

    @field_validator("synthesis_directive")
    @classmethod
    def _directive_length(cls, v: str | None) -> str | None:
        if v is not None and len(v) > 500:
            raise ValueError("synthesis_directive must be <= 500 chars")
        return v


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

    id: str
    description: str
    applies_when_subject_in: tuple[str, ...]
    detection_patterns: tuple[str, ...] = ()


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

    confidence_threshold: float = Field(ge=0.0, le=1.0)
    context_char_cap_4b: int = Field(gt=0)
    context_char_cap_26b: int = Field(gt=0)
    escalate_on_multi_doc_count: int = Field(ge=1)


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

    min: float | None = None
    max: float | None = None


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

    required_event_fields: tuple[str, ...]
    health_ranges: dict[str, HealthRange]


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

    version: int
    last_updated: str
    scope: tuple[str, ...]
    self_declare_semantics: SelfDeclareSemantics

    subject_domains: dict[str, SubjectDomain]
    fallback_domain: FallbackDomain
    risk_flags: dict[str, RiskFlag]
    forbidden_for_4b: tuple[ForbiddenRule, ...]
    escalation: Escalation
    observability: Observability

    @model_validator(mode="after")
    def _cross_reference_check(self) -> "DomainPolicy":
        """Cross-field validation — yaml 내부 일관성."""
        known_flags = set(self.risk_flags.keys())

        # 1. 모든 subject_domain.default_risk_flags 가 risk_flags 에 정의돼 있어야 함
        for name, dom in self.subject_domains.items():
            for flag in (*dom.default_risk_flags, *dom.deep_summary_risk_flags):
                if flag not in known_flags:
                    raise ValueError(
                        f"subject_domain '{name}' references unknown risk_flag '{flag}'"
                    )

        for flag in self.fallback_domain.default_risk_flags:
            if flag not in known_flags:
                raise ValueError(
                    f"fallback_domain references unknown risk_flag '{flag}'"
                )

        # 2. forbidden_for_4b.applies_when_subject_in 의 도메인이 subject_domains 에 있어야 함
        known_domains = set(self.subject_domains.keys())
        for rule in self.forbidden_for_4b:
            for dom_name in rule.applies_when_subject_in:
                if dom_name not in known_domains:
                    raise ValueError(
                        f"forbidden rule '{rule.id}' references unknown subject_domain '{dom_name}'"
                    )

        return self
