"""Immutable installed composition contracts and deterministic snapshots."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field

from src.domain.app_blocks.composition_resolve import ResolvedComposition
from src.domain.app_blocks.catalogs import canonical_feature_pack
from src.domain.codegen.generation_plan import GenerationPlanV1


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

    requested_id: str
    canonical_id: str
    version: str


class InstalledCompositionSnapshot(BaseModel):
    """The contract a generated project was actually installed from."""

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

    schema_version: str = "installed_composition.v1"
    project_id: UUID
    recipe_id: str | None = None
    recipe_version: int | None = Field(default=None, ge=1)
    starter_id: str | None = None
    starter_version: str | None = None
    feature_packs: tuple[InstalledFeaturePack, ...] = ()
    app_blocks: tuple[str, ...] = ()
    capability_contract_versions: dict[str, str] = Field(default_factory=dict)
    auth_contract: dict[str, Any] | None = None
    auth_contract_version: str | None = None
    runtime_contract_version: str | None = None
    reaction_contract_version: str | None = None
    generation_plan_hash: str | None = None
    generated_artifact_version: str | None = None
    created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))


def snapshot_from_generation_plan(
    project_id: UUID,
    plan: GenerationPlanV1,
    *,
    resolved: ResolvedComposition | None = None,
    starter_version: str | None = None,
    generated_artifact_version: str | None = None,
) -> InstalledCompositionSnapshot:
    """Build a stable snapshot from the already-frozen GenerationPlan."""

    composition = plan.composition
    return InstalledCompositionSnapshot(
        project_id=project_id,
        recipe_id=(composition.recipe_ids[0] if composition.recipe_ids else None),
        recipe_version=(
            composition.recipe_versions.get(composition.recipe_ids[0])
            if composition.recipe_ids
            else None
        ),
        starter_id=composition.starter_id or (resolved.starter_id if resolved else None),
        starter_version=starter_version,
        feature_packs=tuple(
            InstalledFeaturePack(
                requested_id=pack_id,
                canonical_id=canonical_feature_pack(pack_id),
                version=plan.provenance.feature_pack_catalog_version,
            )
            for pack_id in composition.feature_pack_ids
        ),
        app_blocks=tuple(composition.app_block_ids),
        capability_contract_versions={
            capability: plan.provenance.cloud_capability_catalog_version
            for capability in composition.cloud_capability_ids
        },
        auth_contract=(
            plan.runtime.auth_contract.model_dump(mode="json")
            if plan.runtime.auth_contract is not None
            else None
        ),
        auth_contract_version=(
            plan.runtime.auth_contract.contract_version
            if plan.runtime.auth_contract is not None
            else None
        ),
        runtime_contract_version=plan.provenance.schema_version,
        generation_plan_hash=plan.provenance.plan_hash,
        generated_artifact_version=generated_artifact_version,
    )


def snapshot_diff(
    installed: InstalledCompositionSnapshot,
    current: InstalledCompositionSnapshot,
) -> dict[str, Any]:
    """Return a deterministic field-level diff for reconciliation planning."""

    left = installed.model_dump(mode="json")
    right = current.model_dump(mode="json")
    return {
        key: {"installed": left.get(key), "current": right.get(key)}
        for key in sorted(set(left) | set(right))
        if key not in {"project_id", "created_at"} and left.get(key) != right.get(key)
    }
