"""Deterministic resolution of the minimum architecture context for Codegen."""

from __future__ import annotations

import hashlib
import json
from typing import Any, Literal
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, model_validator

from src.domain.app_blocks.catalogs import canonical_feature_pack
from src.domain.app_blocks.job_packs import get_job_pack
from src.domain.app_blocks.registry import AppBlockRegistry, load_default_registry
from src.domain.codegen.generation_plan import (
    GenerationPlanV1,
    validate_generation_plan_catalog,
)
from src.domain.feature_packs.normalize import load_pack_contract
from src.domain.starter_library.catalog import get_contract


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

    id: str
    kind: Literal["starter_contract", "feature_pack", "app_block", "recipe", "primitive"]
    status: Literal["admitted"] = "admitted"
    contract: dict[str, Any] = Field(default_factory=dict)


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

    id: str
    availability: Literal[
        "native",
        "provider_required",
        "external_system_required",
        "degraded_only",
        "unsupported",
    ]
    providers: list[str] = Field(default_factory=list)


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

    schema_version: Literal["resolved_generation_context.v1"] = (
        "resolved_generation_context.v1"
    )
    plan_hash: str
    starter_contracts: list[ResolvedAsset] = Field(default_factory=list)
    feature_packs: list[ResolvedAsset] = Field(default_factory=list)
    app_blocks: list[ResolvedAsset] = Field(default_factory=list)
    recipes: list[ResolvedAsset] = Field(default_factory=list)
    primitives: list[ResolvedAsset] = Field(default_factory=list)
    cloud_capabilities: list[ResolvedCapability] = Field(default_factory=list)
    provider_requirements: list[dict[str, Any]] = Field(default_factory=list)
    required_constraints: list[str] = Field(default_factory=list)
    forbidden_constraints: list[str] = Field(default_factory=list)


class CodegenRequestV1(BaseModel):
    """Single internal Codegen input contract; compatibility is non-authoritative."""

    model_config = ConfigDict(extra="forbid")

    schema_version: Literal["codegen_request.v1"] = "codegen_request.v1"
    generation_id: UUID | None = None
    project_id: UUID
    generation_plan: dict[str, Any]
    resolved_context: ResolvedGenerationContext
    resolved_context_hash: str | None = None
    application_request: dict[str, Any] = Field(default_factory=dict)
    generation_constraints: dict[str, list[str]] = Field(default_factory=dict)
    execution_mode: Literal["legacy", "plan_first_shadow", "plan_first"] = (
        "plan_first_shadow"
    )
    compatibility: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def hashes_match_authoritative_inputs(self) -> CodegenRequestV1:
        plan_hash = (
            self.generation_plan.get("provenance", {}).get("plan_hash")
            if isinstance(self.generation_plan, dict)
            else None
        )
        if plan_hash and plan_hash != self.resolved_context.plan_hash:
            raise ValueError("generation_plan_hash_mismatch")
        if self.resolved_context_hash and self.resolved_context_hash != _context_hash(
            self.resolved_context
        ):
            raise ValueError("resolved_context_hash_mismatch")
        return self


class GenerationContextResolutionError(ValueError):
    """Raised when a validated plan cannot be materialized into required assets."""


PLAN_FIRST_INSTRUCTION = """Implement the supplied validated GenerationPlan using the resolved
P2HS composition context.

The supplied architecture selection is authoritative. Use only the admitted
components and capabilities provided. Honor all required and forbidden
constraints. Generate application-specific UI, fields, routes, copy,
presentation, and permitted glue.

Do not invent P2HS primitives, App Blocks, Feature Packs, Cloud APIs, provider
semantics, persistence authorities, or private backend substitutes. Unsupported
semantics may only be omitted or implemented through an explicitly declared
degraded path.
"""


def resolve_generation_context(
    plan: GenerationPlanV1,
    *,
    registry: AppBlockRegistry | None = None,
) -> ResolvedGenerationContext:
    """Resolve only the admitted assets referenced by ``plan``."""

    registry = registry or load_default_registry()
    validate_generation_plan_catalog(plan, registry=registry)

    starter_contracts = [
        ResolvedAsset(
            id=contract_id,
            kind="starter_contract",
            contract=get_contract(contract_id).model_dump(mode="json"),
        )
        for contract_id in plan.runtime.starter_contract_ids
    ]
    feature_packs = [
        _resolve_feature_pack(pack_id)
        for pack_id in plan.composition.feature_pack_ids
    ]
    app_blocks = [
        ResolvedAsset(
            id=block_id,
            kind="app_block",
            contract=registry.require(block_id).model_dump(mode="json"),
        )
        for block_id in plan.composition.app_block_ids
    ]
    recipes = [
        ResolvedAsset(id=recipe_id, kind="recipe", contract={"id": recipe_id})
        for recipe_id in plan.composition.recipe_ids
    ]
    primitives = [
        ResolvedAsset(
            id=primitive_id,
            kind="primitive",
            contract={"id": primitive_id, "authority": "P2HS Cloud"},
        )
        for primitive_id in plan.composition.primitive_ids
    ]
    requirement_by_id = {
        item.id: item for item in plan.provider_requirements
    }
    cloud_capabilities = [
        ResolvedCapability(
            id=capability_id,
            availability=_availability(requirement_by_id.get(capability_id)),
            providers=(requirement_by_id.get(capability_id).providers
                       if requirement_by_id.get(capability_id) else []),
        )
        for capability_id in plan.composition.cloud_capability_ids
    ]
    return ResolvedGenerationContext(
        plan_hash=plan.provenance.plan_hash or "",
        starter_contracts=starter_contracts,
        feature_packs=feature_packs,
        app_blocks=app_blocks,
        recipes=recipes,
        primitives=primitives,
        cloud_capabilities=cloud_capabilities,
        provider_requirements=[item.model_dump(mode="json") for item in plan.provider_requirements],
        required_constraints=list(plan.constraints.required_ids),
        forbidden_constraints=(
            list(plan.constraints.forbidden_capabilities)
            + list(plan.constraints.forbidden_implementations)
        ),
    )


def build_codegen_request(
    *,
    project_id: UUID,
    plan: GenerationPlanV1,
    resolved_context: ResolvedGenerationContext,
    legacy_prompt: str,
    legacy_plan: dict[str, Any] | None = None,
    generation_id: UUID | None = None,
    execution_mode: Literal["legacy", "plan_first_shadow", "plan_first"] = "plan_first_shadow",
) -> CodegenRequestV1:
    return CodegenRequestV1(
        generation_id=generation_id,
        project_id=project_id,
        generation_plan=plan.model_dump(mode="json"),
        resolved_context=resolved_context,
        resolved_context_hash=_context_hash(resolved_context),
        application_request={
            "user_intent": plan.intent.original_intent or plan.intent.summary,
            "presentation_preferences": {},
        },
        generation_constraints={
            "required": list(plan.constraints.required_ids),
            "forbidden": list(resolved_context.forbidden_constraints),
        },
        execution_mode=execution_mode,
        compatibility={
            "legacy_prompt": legacy_prompt,
            "legacy_plan": dict(legacy_plan or {}),
        },
    )


def build_codegen_input(
    request: CodegenRequestV1,
    *,
    legacy_system_prompt: str,
    legacy_user_prompt: str,
) -> tuple[str, str, dict[str, Any]]:
    """Build provider input without allowing compatibility prose to override a plan."""

    if request.execution_mode != "plan_first":
        return legacy_system_prompt, legacy_user_prompt, {
            "execution_mode": request.execution_mode,
            "generation_plan_hash": request.resolved_context.plan_hash,
        }

    structured = {
        "generation_plan": request.generation_plan,
        "resolved_generation_context": request.resolved_context.model_dump(mode="json"),
    }
    system_prompt = (
        PLAN_FIRST_INSTRUCTION
        + "\n## Resolved generation context\n"
        + json.dumps(structured, sort_keys=True, separators=(",", ":"))
    )
    user_prompt = str(
        request.application_request.get("user_intent")
        or request.generation_plan.get("intent", {}).get("summary")
        or legacy_user_prompt
    ).strip()
    return system_prompt, user_prompt, {
        "execution_mode": request.execution_mode,
        "generation_plan_hash": request.resolved_context.plan_hash,
        "resolved_context_hash": request.resolved_context_hash or _context_hash(
            request.resolved_context
        ),
    }


def _resolve_feature_pack(pack_id: str) -> ResolvedAsset:
    canonical = canonical_feature_pack(pack_id)
    job_pack = get_job_pack(pack_id)
    if job_pack is not None:
        return ResolvedAsset(
            id=pack_id,
            kind="feature_pack",
            contract={"kind": "job_feature_pack", **job_pack.model_dump(mode="json")},
        )
    return ResolvedAsset(
        id=pack_id,
        kind="feature_pack",
        contract=load_pack_contract(canonical, {}).model_dump(mode="json"),
    )


def _availability(requirement) -> str:
    if requirement is None or requirement.kind == "native":
        return "native"
    if requirement.kind == "credential_configuration":
        return "degraded_only"
    if requirement.kind == "external_provider_required":
        return "provider_required"
    if requirement.kind == "external_system_dependency":
        return "external_system_required"
    return "unsupported"


def _context_hash(context: ResolvedGenerationContext) -> str:
    payload = json.dumps(
        context.model_dump(mode="json"), sort_keys=True, separators=(",", ":")
    ).encode()
    return hashlib.sha256(payload).hexdigest()


__all__ = [
    "CodegenRequestV1",
    "GenerationContextResolutionError",
    "ResolvedGenerationContext",
    "build_codegen_request",
    "build_codegen_input",
    "resolve_generation_context",
]
