"""Deterministic recipe → Feature Pack → Starter expansion.

One generic resolver. Discovery and Codegen must not invent Starter-specific
branches. Explicit user exclusions remain authoritative over a matched recipe.
"""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

from src.domain.app_blocks.job_packs import (
    JOB_FEATURE_PACKS,
    JobFeaturePack,
    job_pack_ids_for_recipe,
)
from src.domain.app_blocks.schemas import ProductIntent
from src.domain.app_blocks.vertical_recipes import VerticalRecipe
from src.domain.composition_recipes.schemas import CompositionRecipe
from src.domain.composition_recipes.authority import canonical_recipe
from src.domain.starter_library.archetypes import (
    ArchetypeStarter,
    get_archetype_starter,
    starter_id_for_recipe,
)


class ResolvedComposition(BaseModel):
    model_config = ConfigDict(extra="forbid")
    matched_recipe_id: str | None = None
    composition_recipe_version: int | None = None
    starter_id: str | None = None
    feature_pack_ids: list[str] = Field(default_factory=list)
    required_capabilities: list[str] = Field(default_factory=list)
    forbidden_capabilities: list[str] = Field(default_factory=list)
    excluded_by_intent: list[str] = Field(default_factory=list)
    architecture_gaps: list[str] = Field(default_factory=list)
    required_app_blocks: list[str] = Field(default_factory=list)
    required_feature_packs: list[str] = Field(default_factory=list)


def excluded_block_ids(intent: ProductIntent) -> set[str]:
    from src.domain.app_blocks.planner import _RECIPE_NEGATION_TERMS, _corpus, _negated

    text = _corpus(intent)
    excluded: set[str] = set()
    for block_id, terms in _RECIPE_NEGATION_TERMS.items():
        if _negated(text, terms):
            excluded.add(block_id)
    return excluded


def reconcile_pack(pack: JobFeaturePack, excluded: set[str]) -> JobFeaturePack | None:
    """Drop negated required blocks instead of treating the pack as opaque."""
    remaining = [block for block in pack.required_app_blocks if block not in excluded]
    if not remaining:
        return None
    return pack.model_copy(update={"required_app_blocks": remaining})


def resolve_composition(
    recipe: VerticalRecipe | None,
    intent: ProductIntent | None = None,
    composition_recipe: CompositionRecipe | None = None,
) -> ResolvedComposition:
    composition_recipe = canonical_recipe(
        composition_recipe=composition_recipe,
        legacy_recipe=recipe,
    )
    # The adapter above makes the legacy path semantically canonical.  Keep the
    # original object only for compatibility-only fields that have no v1 DTO.
    legacy_recipe = recipe
    excluded = excluded_block_ids(intent) if intent is not None else set()
    recipe_id = composition_recipe.slug if composition_recipe else None
    starter_id = (
        composition_recipe.starter_id
        if composition_recipe
        else starter_id_for_recipe(recipe_id)
    )
    starter = get_archetype_starter(starter_id)
    pack_ids: list[str] = []
    capabilities: list[str] = []
    forbidden: list[str] = []
    gaps: list[str] = []
    required_blocks = list(composition_recipe.required_app_blocks) if composition_recipe else []
    required_packs = list(composition_recipe.required_feature_packs) if composition_recipe else []
    if composition_recipe:
        capabilities.extend(composition_recipe.required_capabilities)

    pack_ids_for_recipe = job_pack_ids_for_recipe(recipe_id)
    if composition_recipe:
        pack_ids_for_recipe = list(dict.fromkeys(
            [*composition_recipe.required_feature_packs, *pack_ids_for_recipe]
        ))
    for pack_id in pack_ids_for_recipe:
        pack = JOB_FEATURE_PACKS.get(pack_id)
        if pack is None:
            continue
        reconciled = reconcile_pack(pack, excluded)
        if reconciled is None:
            continue
        pack_ids.append(pack.id)
        capabilities.extend(reconciled.required_app_blocks)
        forbidden.extend(pack.forbidden_implicit_capabilities)
        gaps.extend(pack.architecture_gaps)

    if starter is not None:
        capabilities.extend(starter.required_capabilities)
        forbidden.extend(starter.forbidden_implicit_capabilities)
        if recipe is not None and "membership.member" in excluded:
            if starter.id == "membership_association":
                starter_id = "internal_operations"
                starter = get_archetype_starter(starter_id)

    if legacy_recipe is not None:
        for item in legacy_recipe.unsupported_assumptions:
            if item.startswith("forbidden:"):
                forbidden.append(item.split(":", 1)[1])

    capabilities = [item for item in dict.fromkeys(capabilities) if item not in excluded]
    forbidden = [item for item in dict.fromkeys(forbidden)]
    return ResolvedComposition(
        matched_recipe_id=recipe_id,
        composition_recipe_version=composition_recipe.version if composition_recipe else None,
        starter_id=starter_id,
        feature_pack_ids=pack_ids,
        required_capabilities=capabilities,
        forbidden_capabilities=forbidden,
        excluded_by_intent=sorted(excluded),
        architecture_gaps=list(dict.fromkeys(gaps)),
        required_app_blocks=list(dict.fromkeys(required_blocks)),
        required_feature_packs=list(dict.fromkeys(required_packs)),
    )


def starter_shell(starter_id: str | None) -> ArchetypeStarter | None:
    return get_archetype_starter(starter_id)
