"""Machine-readable App Block contracts. No Cloud execution details."""

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator

from src.domain.starter_library.schemas import StarterContractRequirement

DependencyKind = Literal["primitive", "feature_pack", "app_block"]
BlockStatus = Literal["implemented", "planned", "experimental"]
CapabilityLifecycle = Literal[
    "stable", "preview", "internal", "reserved", "compatibility", "deprecated"
]
CapabilityReadiness = Literal["implemented", "partial", "unimplemented"]
GapType = Literal[
    "block_missing",
    "feature_pack_missing",
    "primitive_missing",
    "configuration_required",
    "unsupported",
]
GapSeverity = Literal["build_blocking", "runtime_blocking", "optional"]
MissingConfigurationBehavior = Literal["degraded", "disabled", "block_install"]


class DependencyRef(BaseModel):
    model_config = ConfigDict(extra="forbid")
    kind: DependencyKind
    id: str = Field(min_length=1)
    min_version: str | None = None


class ProvidedCapability(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    description: str | None = None


class ProviderBinding(BaseModel):
    model_config = ConfigDict(extra="forbid")
    capability: str = Field(min_length=1)
    providers: list[str] = Field(default_factory=list)
    configuration_required: bool = False
    missing_configuration_behavior: MissingConfigurationBehavior = "degraded"


class SlotConstraint(BaseModel):
    model_config = ConfigDict(extra="forbid")
    required: bool = False
    editable: bool = True
    max_items: int | None = None
    min_items: int | None = None
    allowed_types: list[str] = Field(default_factory=list)
    description: str | None = None


PresentationKind = Literal[
    "default",
    "status_grouping",
    "split_list_detail",
    "compact_dashboard",
    "kanban_grouping",
    "relationship_view",
    "portal_read_only",
    "staff_table",
    "calendar",
]


class AppBlockSlot(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    type: str
    description: str
    component_contract: str | None = None
    presentation: PresentationKind | None = None
    group_by_field: str | None = None
    query_source: str | None = None
    query_params: dict[str, str] | None = None
    read_only: bool = False
    constraints: SlotConstraint = Field(default_factory=SlotConstraint)

    @field_validator("query_params")
    @classmethod
    def static_query_params_only(cls, value: dict[str, str] | None) -> dict[str, str] | None:
        if value is None:
            return value
        for key, raw in value.items():
            if "{{" in raw or "}}" in raw:
                raise ValueError(
                    f"query_params.{key} must be a static default, not an interpolation"
                )
        return value


class BlockBinding(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    target: str
    protected: bool = True


class ProtectedContract(BaseModel):
    model_config = ConfigDict(extra="forbid")
    persistence: bool = True
    auth: bool = True
    bff: bool = True
    events: bool = True
    allowed_fields: bool = True
    abuse_controls: bool = True


class AppBlockEvent(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    description: str | None = None


class ThemeContract(BaseModel):
    model_config = ConfigDict(extra="forbid")
    use_project_tokens: bool = True
    allowed_overrides: list[str] = Field(
        default_factory=lambda: ["spacing", "typography", "radius", "surface", "accent"]
    )


class CompositionFallback(BaseModel):
    model_config = ConfigDict(extra="forbid")
    supported: bool = False
    blocks: list[str] = Field(default_factory=list)
    feature_packs: list[str] = Field(default_factory=list)


class AppBlockManifest(BaseModel):
    model_config = ConfigDict(extra="forbid")
    schema_version: Literal[1] = 1
    kind: Literal["app_block"] = "app_block"
    id: str
    version: str
    family: str
    title: str
    description: str
    status: BlockStatus = "implemented"
    lifecycle: CapabilityLifecycle = "stable"
    readiness: CapabilityReadiness = "implemented"
    owner: str | None = None
    replacement: str | None = None
    provides: list[ProvidedCapability] = Field(default_factory=list)
    requires: list[DependencyRef] = Field(default_factory=list)
    optional_requires: list[DependencyRef] = Field(default_factory=list)
    provider_bindings: list[ProviderBinding] = Field(default_factory=list)
    slots: list[AppBlockSlot] = Field(default_factory=list)
    bindings: list[BlockBinding] = Field(default_factory=list)
    protected: ProtectedContract = Field(default_factory=ProtectedContract)
    events: list[AppBlockEvent] = Field(default_factory=list)
    theme: ThemeContract = Field(default_factory=ThemeContract)
    starter_contract: StarterContractRequirement | None = None
    tags: list[str] = Field(default_factory=list)
    composition_fallback: CompositionFallback | None = None

    @property
    def implemented(self) -> bool:
        return (
            self.status == "implemented"
            and self.readiness == "implemented"
            and self.lifecycle in ("stable", "preview")
        )


class ProductIntent(BaseModel):
    """Normalized discovery facts. Never includes App Block IDs."""

    model_config = ConfigDict(extra="forbid")
    purpose: str = ""
    first_build_goal: str = ""
    actor_names: list[str] = Field(default_factory=list)
    workflow_names: list[str] = Field(default_factory=list)
    workflow_descriptions: list[str] = Field(default_factory=list)
    entity_names: list[str] = Field(default_factory=list)
    entity_descriptions: list[str] = Field(default_factory=list)
    entity_lifecycles: list[str] = Field(default_factory=list)
    archetypes: list[str] = Field(default_factory=list)
    target_audience: list[str] = Field(default_factory=list)
    access_scopes: list[str] = Field(default_factory=list)
    requirement_lines: list[str] = Field(default_factory=list)
    source_prompt: str = ""
    original_intent: str = ""
    domain: str = ""
    requested_product_shape: str = ""
    form_names: list[str] = Field(default_factory=list)
    authentication_required: bool = False
    staff_review: bool = False


class ConfigurationRequirement(BaseModel):
    model_config = ConfigDict(extra="forbid")
    capability: str
    providers: list[str] = Field(default_factory=list)
    behavior: MissingConfigurationBehavior = "degraded"


class AppBlockGap(BaseModel):
    model_config = ConfigDict(extra="forbid")
    type: GapType
    id: str
    severity: GapSeverity
    affects: list[str] = Field(default_factory=list)
    message: str = ""


class OptionalRequirementStatus(BaseModel):
    """Non-blocking enhancement observation. Never an install instruction."""

    model_config = ConfigDict(extra="forbid")
    kind: DependencyKind
    id: str
    source_id: str
    status: Literal["satisfied", "unavailable"]


class PlannedBlock(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    version: str
    reason: str
    slots: dict[str, Any] = Field(default_factory=dict)
    dependencies: list[DependencyRef] = Field(default_factory=list)
    protected_bindings: list[str] = Field(default_factory=list)
    optional_requirements: list[OptionalRequirementStatus] = Field(
        default_factory=list,
        exclude=True,
    )


class AppBlockPlan(BaseModel):
    """Version-1 diagnostic snapshot, not a runtime readiness or install grant.

    A plan is valid iff ``unsatisfiable`` is false. A returned object can contain
    blocking gaps so Discovery can explain unresolved hard requirements.
    """

    model_config = ConfigDict(extra="forbid")
    schema_version: Literal["1"] = "1"
    requested_capabilities: list[str] = Field(default_factory=list)
    blocks: list[PlannedBlock] = Field(default_factory=list)
    required_feature_packs: list[str] = Field(default_factory=list)
    required_primitives: list[str] = Field(default_factory=list)
    configuration_requirements: list[ConfigurationRequirement] = Field(default_factory=list)
    gaps: list[AppBlockGap] = Field(default_factory=list)
    runtime_dependencies: list[str] = Field(default_factory=list)
    starter_contracts: list[str] = Field(default_factory=list)
    collection_recipe_ids: list[str] = Field(default_factory=list)
    adoption: Literal["authoritative", "advisory", "omitted"] = "advisory"
    matched_recipe_id: str | None = None
    composition_recipe_version: int | None = Field(default=None, ge=1)
    starter_id: str | None = None
    feature_pack_ids: list[str] = Field(default_factory=list)
    forbidden_capabilities: list[str] = Field(default_factory=list)
    product_archetype: str | None = None
    unsupported_assumptions: list[str] = Field(default_factory=list)

    @property
    def unsatisfiable(self) -> bool:
        return any(item.severity == "build_blocking" for item in self.gaps)
