from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

RecipeReadinessStatus = Literal["ready", "configuration_only", "partial", "blocked"]


class EntityRoleRequirement(BaseModel):
    model_config = ConfigDict(extra="forbid")
    role: str = Field(min_length=1, max_length=120, pattern=r"^[a-z0-9][a-z0-9_.-]*$")
    required: bool = True


class PresentationExpectations(BaseModel):
    model_config = ConfigDict(extra="forbid")
    expected_surfaces: list[str] = Field(default_factory=list, max_length=32)
    navigation_roles: list[str] = Field(default_factory=list, max_length=16)
    admin_expected: bool = False
    staff_surface_expected: bool = False
    customer_account_expected: bool = False
    content_management_expected: bool = False
    # Presentation-only email defaults. This never admits email.send or any
    # business capability; admitted operations still own whether a message exists.
    email_profile: str | None = Field(default=None, max_length=80, pattern=r"^[a-z0-9][a-z0-9_.-]*$")


class OperationalExpectations(BaseModel):
    model_config = ConfigDict(extra="forbid")
    workflows: list[str] = Field(default_factory=list, max_length=32)
    forms: list[str] = Field(default_factory=list, max_length=32)
    auth_audiences: list[str] = Field(default_factory=list, max_length=16)


class RecipeGap(BaseModel):
    model_config = ConfigDict(extra="forbid")
    kind: str = Field(min_length=1, max_length=80)
    id: str = Field(min_length=1, max_length=160)
    message: str = Field(min_length=1, max_length=500)
    severity: Literal["configuration", "partial", "blocking"] = "blocking"


class RecipeReadiness(BaseModel):
    model_config = ConfigDict(extra="forbid")
    status: RecipeReadinessStatus
    gaps: list[RecipeGap] = Field(default_factory=list)
    checked_catalog_versions: dict[str, str] = Field(default_factory=dict)


class CompositionRecipe(BaseModel):
    """Bounded composition metadata; schemas and execution remain catalog-owned."""

    model_config = ConfigDict(extra="forbid")
    schema_version: Literal["1"] = "1"
    slug: str = Field(min_length=1, max_length=160, pattern=r"^[a-z0-9][a-z0-9_.-]*$")
    name: str = Field(min_length=1, max_length=160)
    description: str = ""
    project_types: list[str] = Field(default_factory=list, max_length=8)
    starter_id: str = Field(min_length=1, max_length=120)
    required_feature_packs: list[str] = Field(default_factory=list, max_length=64)
    optional_feature_packs: list[str] = Field(default_factory=list, max_length=64)
    required_app_blocks: list[str] = Field(default_factory=list, max_length=64)
    optional_app_blocks: list[str] = Field(default_factory=list, max_length=64)
    required_capabilities: list[str] = Field(default_factory=list, max_length=64)
    optional_capabilities: list[str] = Field(default_factory=list, max_length=64)
    entity_roles: list[EntityRoleRequirement] = Field(default_factory=list, max_length=64)
    presentation: PresentationExpectations = Field(default_factory=PresentationExpectations)
    operational: OperationalExpectations = Field(default_factory=OperationalExpectations)
    provider_requirements: list[str] = Field(default_factory=list, max_length=32)
    version: int = Field(default=1, ge=1)
    is_active: bool = True

    @model_validator(mode="after")
    def validate_sets(self) -> CompositionRecipe:
        for required, optional, label in (
            (self.required_feature_packs, self.optional_feature_packs, "feature pack"),
            (self.required_app_blocks, self.optional_app_blocks, "App Block"),
            (self.required_capabilities, self.optional_capabilities, "capability"),
        ):
            if len(set(required)) != len(required) or len(set(optional)) != len(optional):
                raise ValueError(f"duplicate {label} references are not allowed")
            overlap = set(required) & set(optional)
            if overlap:
                raise ValueError(f"{label} cannot be both required and optional: {sorted(overlap)}")
        if len({item.role for item in self.entity_roles}) != len(self.entity_roles):
            raise ValueError("entity role references must be unique")
        return self
