"""Pydantic Schemas for Norm API"""
from datetime import datetime
from typing import Optional, List, Dict, Any, Literal, Annotated
from uuid import UUID
from pydantic import (
    BaseModel,
    Field,
    ConfigDict,
    StringConstraints,
    field_validator,
    model_validator,
)


CreatedByRef = Annotated[
    str,
    StringConstraints(pattern=r"^(user|org|system|external):.+$"),
]


# --- Canonical+NL content contracts ---

class NLContent(BaseModel):
    """Natural-language branch for content zones."""
    model_config = ConfigDict(extra="forbid")

    default_lang: str = Field(..., min_length=2)
    texts: Dict[str, str] = Field(default_factory=dict)

    @model_validator(mode="after")
    def validate_default_lang_text(self):
        text = self.texts.get(self.default_lang)
        if text is None:
            raise ValueError("nl.default_lang must exist in nl.texts.")
        if not str(text).strip():
            raise ValueError("nl.texts[default_lang] must be non-empty.")
        return self


def _normalize_string_list(values: List[str]) -> List[str]:
    return [str(v).strip() for v in values if str(v).strip()]


class SubjectCanonical(BaseModel):
    model_config = ConfigDict(extra="forbid")

    roles: List[str] = Field(default_factory=list)
    agents: List[UUID] = Field(default_factory=list)
    capabilities: List[str] = Field(default_factory=list)

    @field_validator("roles", "capabilities")
    @classmethod
    def validate_roles(cls, values: List[str]) -> List[str]:
        return _normalize_string_list(values)

    @model_validator(mode="after")
    def validate_non_empty_subject(self):
        if not self.roles and not self.agents:
            raise ValueError("subject.canonical requires non-empty roles or agents.")
        return self


class ContextCanonical(BaseModel):
    model_config = ConfigDict(extra="forbid")

    regions: List[str] = Field(default_factory=list)
    domains: List[str] = Field(default_factory=list)
    scenarios: List[str] = Field(default_factory=list)

    @field_validator("regions", "domains", "scenarios")
    @classmethod
    def validate_context_lists(cls, values: List[str]) -> List[str]:
        return _normalize_string_list(values)


class ConditionCanonical(BaseModel):
    model_config = ConfigDict(extra="forbid")

    trigger_type: Optional[Literal["event", "state", "threshold", "schedule"]] = None


class ActionCanonical(BaseModel):
    model_config = ConfigDict(extra="forbid")

    modality: Literal["must", "must_not", "may"]
    operations: List[str] = Field(default_factory=list)
    targets: Optional[List[str]] = None
    time_limit: Optional[str] = None
    approval_required: Optional[bool] = None
    tools: Optional[List[str]] = None
    skills: Optional[List[str]] = None

    @field_validator("operations")
    @classmethod
    def validate_operations(cls, values: List[str]) -> List[str]:
        return _normalize_string_list(values)

    @field_validator("targets", "tools", "skills")
    @classmethod
    def validate_targets(cls, values: Optional[List[str]]) -> Optional[List[str]]:
        if values is None:
            return None
        return _normalize_string_list(values)


class ViolationPolicy(BaseModel):
    model_config = ConfigDict(extra="forbid")

    severity: Literal["low", "medium", "high", "critical"]
    responses: List[str] = Field(default_factory=list)

    @field_validator("responses")
    @classmethod
    def validate_responses(cls, values: List[str]) -> List[str]:
        return _normalize_string_list(values)


class CompliancePolicy(BaseModel):
    model_config = ConfigDict(extra="forbid")

    rewards: List[str] = Field(default_factory=list)

    @field_validator("rewards")
    @classmethod
    def validate_rewards(cls, values: List[str]) -> List[str]:
        return _normalize_string_list(values)


class ConsequenceCanonical(BaseModel):
    model_config = ConfigDict(extra="forbid")

    on_violation: Optional[ViolationPolicy] = None
    on_compliance: Optional[CompliancePolicy] = None


class SubjectContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    canonical: SubjectCanonical
    nl: NLContent


class ContextContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    canonical: ContextCanonical
    nl: NLContent


class ConditionContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    canonical: ConditionCanonical
    nl: NLContent


class ActionContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    canonical: ActionCanonical
    nl: NLContent


class ConsequenceContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    canonical: ConsequenceCanonical
    nl: NLContent


class NormTagItem(BaseModel):
    """Norm tag item"""
    model_config = ConfigDict(from_attributes=True)
    tag: str = Field(..., min_length=1, max_length=100)
    tag_category: str = Field(
        ...,
        min_length=1,
        max_length=50,
        description="Free-form tag category (e.g. role/scenario/domain/tool/skill/capability)",
    )


class NormCreate(BaseModel):
    """Schema for creating a new norm"""
    # Identity zone
    title: str = Field(..., min_length=1, max_length=255)
    field_id: Optional[UUID] = Field(None, description="Information field ID")

    # Attribute zone
    norm_type: Literal["obligation", "permission", "prohibition"]
    authority_level: Optional[Literal["legal", "industrial", "organizational", "group"]] = None
    formality_level: Optional[Literal["informal", "formal", "technical"]] = None

    # Content zone
    subject: SubjectContent
    context: ContextContent
    condition: ConditionContent
    action: ActionContent
    consequence: ConsequenceContent

    # Evidence zone
    source: Optional[str] = Field(None, description="Source document/policy (brief)")
    source_details: Optional[Dict[str, Any]] = Field(None, description="Detailed source information")
    case_refs: List[str] = Field(default_factory=list)

    # Audit zone
    created_by: Optional[CreatedByRef] = Field(
        None,
        description="Creator/authority reference. Format: user:<id>|org:<id>|system:<id>|external:<id>",
    )

    # Governance zone
    status: str = Field("draft", description="draft/active/deprecated/archived")
    tags: List[NormTagItem] = Field(default_factory=list, description="Norm tags")

    @model_validator(mode="after")
    def validate_norm_type_action_modality(self):
        expected_by_type = {
            "obligation": "must",
            "prohibition": "must_not",
            "permission": "may",
        }
        expected = expected_by_type[self.norm_type]
        actual = self.action.canonical.modality
        if actual != expected:
            raise ValueError(
                f"action.canonical.modality must be '{expected}' for norm_type '{self.norm_type}'."
            )
        return self


class NormUpdate(BaseModel):
    """Schema for updating an existing norm"""
    # Identity zone
    title: Optional[str] = Field(None, min_length=1, max_length=255)
    field_id: Optional[UUID] = None

    # Attribute zone
    norm_type: Optional[Literal["obligation", "permission", "prohibition"]] = None
    authority_level: Optional[Literal["legal", "industrial", "organizational", "group"]] = None
    formality_level: Optional[Literal["informal", "formal", "technical"]] = None

    # Content zone
    subject: Optional[SubjectContent] = None
    context: Optional[ContextContent] = None
    condition: Optional[ConditionContent] = None
    action: Optional[ActionContent] = None
    consequence: Optional[ConsequenceContent] = None

    # Evidence zone
    source: Optional[str] = None
    source_details: Optional[Dict[str, Any]] = None
    case_refs: Optional[List[str]] = None

    # Audit zone
    created_by: Optional[CreatedByRef] = None

    # Governance zone
    status: Optional[str] = Field(None, description="draft/active/deprecated/archived")
    tags: Optional[List[NormTagItem]] = None

    @model_validator(mode="after")
    def validate_norm_type_action_modality(self):
        if self.norm_type is None or self.action is None:
            return self
        expected_by_type = {
            "obligation": "must",
            "prohibition": "must_not",
            "permission": "may",
        }
        expected = expected_by_type[self.norm_type]
        actual = self.action.canonical.modality
        if actual != expected:
            raise ValueError(
                f"action.canonical.modality must be '{expected}' for norm_type '{self.norm_type}'."
            )
        return self


class NormResponse(BaseModel):
    """Schema for norm response"""
    model_config = ConfigDict(from_attributes=True)
    
    # Identity zone
    id: UUID
    title: str
    field_id: Optional[UUID] = None  # Information field ID

    # Attribute zone
    norm_type: str
    authority_level: Optional[str] = None
    formality_level: Optional[str] = None

    # Content zone
    subject: Dict[str, Any]
    context: Dict[str, Any]
    condition: Dict[str, Any]
    action: Dict[str, Any]
    consequence: Dict[str, Any]

    # Evidence zone
    source: Optional[str] = None
    source_details: Optional[Dict[str, Any]] = None
    case_refs: List[str]

    # Audit zone
    created_by: Optional[str] = None
    created_at: datetime
    updated_at: datetime

    # Governance zone
    status: str
    version: int
    tags: List[NormTagItem] = Field(default_factory=list)


class NormListResponse(BaseModel):
    """Schema for norm list response"""
    total: int
    items: List[NormResponse]
    page: int = 1
    page_size: int = 20


# --- Norm Version Schemas ---

class NormVersionResponse(BaseModel):
    """Schema for norm version response"""
    model_config = ConfigDict(from_attributes=True)
    
    id: UUID
    norm_id: UUID
    version: int
    snapshot: Dict[str, Any]
    change_note: Optional[str] = None
    created_at: datetime


# --- Norm Relation Schemas ---

class NormRelationCreate(BaseModel):
    """Schema for creating norm relation"""
    target_norm_id: UUID
    relation_type: str  # conflicts_with/derives_from/supersedes/requires
    resolution: Optional[str] = None


class NormRelationResponse(BaseModel):
    """Schema for norm relation response"""
    model_config = ConfigDict(from_attributes=True)
    
    source_norm_id: UUID
    target_norm_id: UUID
    relation_type: str
    resolution: Optional[str] = None


# --- Retrieve Schemas ---

class NormRetrieveRequest(BaseModel):
    """Schema for norm retrieval request"""
    context: Dict[str, Any] = Field(default_factory=dict, description="Task context")
    query_mode: Literal["structured", "semantic", "hybrid"] = Field(
        "hybrid",
        description="Retrieval mode: structured/semantic/hybrid",
    )
    nl_query: Optional[str] = Field(
        None,
        description="Natural-language query for semantic/hybrid retrieval.",
    )
    agent_role: Optional[str] = Field(None, description="Optional agent role")
    agent_ids: Optional[List[UUID]] = Field(
        None,
        description="Optional concrete agent IDs for subject.canonical.agents filtering.",
    )
    tag_filters: Optional[Dict[str, List[str]]] = Field(
        None,
        description="Optional tag filters by category (free-form category keys).",
    )
    field_ids: Optional[List[UUID]] = Field(None, description="Information field IDs to search in")
    top_k: int = Field(5, ge=1, le=50, description="Number of norms to retrieve")

    @model_validator(mode="after")
    def validate_query_mode_with_nl_query(self):
        if self.query_mode == "semantic":
            if not isinstance(self.nl_query, str) or not self.nl_query.strip():
                raise ValueError("nl_query is required when query_mode='semantic'.")
        return self


class NormRetrieveMeta(BaseModel):
    """Schema for retrieval execution metadata."""
    query_mode_effective: Literal["structured", "semantic", "hybrid"] = "hybrid"
    semantic_used: bool = False
    semantic_attempted: bool = False
    semantic_query_source: Literal["nl_query", "context", "none"] = "none"
    sql_candidates: int = 0
    vector_hits: int = 0
    semantic_hits: int = 0
    fusion_strategy: Literal["cache", "structured_only", "semantic_rank", "rrf"] = "rrf"


class NormRetrieveResponse(BaseModel):
    """Schema for norm retrieval response"""
    norms: List[NormResponse]
    scores: List[float]
    total: int
    usage_token: str
    cache_hit: bool = False
    meta: NormRetrieveMeta = Field(default_factory=NormRetrieveMeta)


class ComplianceCheckRequest(BaseModel):
    """Schema for action compliance check request."""
    usage_token: str = Field(..., min_length=1, max_length=100)
    action_description: str = Field(..., min_length=1)
    norm_ids: Optional[List[UUID]] = None


class ComplianceViolation(BaseModel):
    """One detected compliance violation."""
    norm_id: UUID
    violation_type: str
    message: str


class ComplianceCheckResponse(BaseModel):
    """Schema for action compliance check result."""
    compliant: bool
    violations: List[ComplianceViolation] = Field(default_factory=list)


class NormOutcomeReportRequest(BaseModel):
    """Schema for norm usage outcome reporting."""
    usage_token: str = Field(..., min_length=1, max_length=100)
    norm_id: UUID
    outcome: str = Field(..., description="complied/violated/exception/inapplicable")
    details: Dict[str, Any] = Field(default_factory=dict)
    agent_id: Optional[str] = None
    project_id: Optional[str] = None
    task_context: Optional[str] = None


class NormOutcomeReportResponse(BaseModel):
    """Schema for norm usage outcome report ack."""
    id: UUID
    usage_token: str
    norm_id: UUID
    outcome: str
    created_at: datetime


# --- API Key Schemas ---

class APIKeyIssueRequest(BaseModel):
    """Schema for issuing a runtime API key."""
    agent_id: UUID
    name: Optional[str] = None
    expires_at: Optional[datetime] = None
    scopes: List[str] = Field(default_factory=list)


class APIKeyIssueResponse(BaseModel):
    """Schema for API key issue/rotate response."""
    key_id: str
    agent_id: UUID
    name: Optional[str] = None
    api_key: str
    key_preview: str
    scopes: List[str]
    is_active: bool
    expires_at: Optional[datetime] = None
    created_at: datetime


class APIKeyListItem(BaseModel):
    """Schema for API key list item."""
    key_id: str
    agent_id: Optional[UUID] = None
    name: Optional[str] = None
    key_preview: str
    scopes: List[str]
    is_active: bool
    expires_at: Optional[datetime] = None
    created_at: datetime


class APIKeyResolveRequest(BaseModel):
    """Schema for resolving API key metadata from plaintext key."""
    api_key: str = Field(..., min_length=8)


class APIKeyResolveResponse(BaseModel):
    """Schema for resolved API key metadata."""
    key_id: str
    agent_id: UUID
    scopes: List[str]
    is_active: bool
    expires_at: Optional[datetime] = None


# --- Role Suggestion Schemas ---

class RoleCandidate(BaseModel):
    """One suggested role candidate for retrieval."""
    role: str
    score: float
    norm_count: int
    matched_contexts: List[str] = Field(default_factory=list)


class RoleSuggestRequest(BaseModel):
    """Schema for role suggestion request."""
    context: Dict[str, Any] = Field(default_factory=dict, description="Task context")
    field_ids: Optional[List[UUID]] = Field(None, description="Optional information field IDs")
    top_k: int = Field(5, ge=1, le=20, description="Number of candidate roles")


class RoleSuggestResponse(BaseModel):
    """Schema for role suggestion response."""
    candidates: List[RoleCandidate]
    recommended_role: Optional[str] = None


class RetrievalPlanResponse(BaseModel):
    """Schema for retrieval planning response."""
    candidates: List[RoleCandidate]
    recommended_role: Optional[str] = None
    role_is_global: bool = False
    suggested_tag_filters: Dict[str, List[str]] = Field(default_factory=dict)


# --- Field Schemas ---

class FieldCreate(BaseModel):
    """Schema for creating an information field."""
    name: str = Field(..., min_length=1, max_length=255)
    description: Optional[str] = None
    org_id: Optional[UUID] = None
    status: Literal["active", "inactive", "archived"] = "active"


class FieldUpdate(BaseModel):
    """Schema for partially updating an information field."""
    name: Optional[str] = Field(None, min_length=1, max_length=255)
    description: Optional[str] = None
    org_id: Optional[UUID] = None
    status: Optional[Literal["active", "inactive", "archived"]] = None


class FieldResponse(BaseModel):
    """Schema for information field response."""
    model_config = ConfigDict(from_attributes=True)

    id: UUID
    name: Optional[str] = None
    org_id: Optional[UUID] = None
    description: Optional[str] = None
    status: str
    created_at: datetime
    updated_at: datetime


# --- Extraction Schemas ---


class ExtractionFromTextRequest(BaseModel):
    """Request for inline natural-language norm extraction."""

    text: str = Field(..., min_length=1, description="Policy or regulation text to extract norms from.")
    default_lang: str = Field("zh-CN", min_length=2, description="Primary language for nl.texts.")
    field_id: Optional[UUID] = Field(
        None, description="Optional default information field ID for imported norms"
    )
    created_by: Optional[CreatedByRef] = None
    max_norms: int = Field(30, ge=1, le=50)
    default_authority_level: Optional[
        Literal["legal", "industrial", "organizational", "group"]
    ] = "legal"
    default_formality_level: Optional[Literal["informal", "formal", "technical"]] = "formal"
    submitter_user_id: Optional[str] = Field(
        None,
        max_length=256,
        description="Optional end-user id for source_details (no auth subsystem yet).",
    )


class ExtractionImportIssue(BaseModel):
    """One candidate norm that was not imported."""

    index: int = Field(..., ge=0)
    detail: str = Field(..., description="Validation or import error detail.")


class ExtractionFromTextResponse(BaseModel):
    """Result of POST /extraction/from-text."""

    session_id: UUID
    artifact_relative_path: str
    norm_ids: List[UUID] = Field(default_factory=list)
    imported_count: int = 0
    issues: List[ExtractionImportIssue] = Field(default_factory=list)


class ExtractionExtractQueuedResponse(BaseModel):
    """HTTP 202: batch document extraction scheduled in background."""

    status: Literal["queued"] = "queued"
    batch_id: UUID
    document_id: UUID
    message: str = "Extraction scheduled; poll GET /extraction/batches/{batch_id} for document status."


class ExtractionBatchCreate(BaseModel):
    """Create an extraction batch (upload container)."""

    title: Optional[str] = Field(None, max_length=255)
    field_id: Optional[UUID] = Field(
        None, description="Default information field ID for norms in this batch"
    )
    created_by: Optional[CreatedByRef] = None
    options: Dict[str, Any] = Field(default_factory=dict)


class ExtractionDocumentResponse(BaseModel):
    """One uploaded file in a batch (no extracted_text body)."""

    model_config = ConfigDict(from_attributes=True)

    id: UUID
    batch_id: UUID
    original_filename: str
    content_type: Optional[str] = None
    size_bytes: int
    storage_relative_path: str
    source_file_url: Optional[str] = None
    parse_status: str
    extraction_status: str
    error_message: Optional[str] = None
    created_at: datetime
    updated_at: datetime


class ExtractionBatchResponse(BaseModel):
    """Extraction batch summary."""

    model_config = ConfigDict(from_attributes=True)

    id: UUID
    title: Optional[str] = None
    field_id: Optional[UUID] = None
    status: str
    options: Dict[str, Any] = Field(default_factory=dict)
    created_by: Optional[str] = None
    created_at: datetime
    updated_at: datetime


class ExtractionBatchDetailResponse(ExtractionBatchResponse):
    """Batch with documents."""

    documents: List[ExtractionDocumentResponse] = Field(default_factory=list)
