"""Single admission-consistency gate across every Fast Lane registry.

Each registry already validates its own construction-time invariants in
isolation (``job_packs._pack`` checks blocks/backend packs,
``canonical.compile_default_catalog`` checks the manifest directory,
``validate_recipes`` checks recipe App Block references). Nothing previously
checked them *against each other* in one place, so verifying a new App
Block / Feature Pack / job pack / archetype Starter / recipe admission meant
manually walking ~10 files (see docs/FEATURE_PACKS_APP_BLOCKS_STARTER.md's
PR checklist). Call ``check_admission_consistency()`` — or run
``tests/test_app_blocks_admission_consistency.py`` — after touching any of
them; an empty list means every registry agrees.
"""

from __future__ import annotations

from src.domain.app_blocks.canonical import CANONICAL_BLOCK_IDS
from src.domain.app_blocks.composition_resolve import resolve_composition
from src.domain.app_blocks.job_packs import JOB_FEATURE_PACKS, known_job_pack_ids
from src.domain.app_blocks.planner import RULES, _RECIPE_NEGATION_TERMS
from src.domain.app_blocks.schemas import ProductIntent
from src.domain.app_blocks.vertical_recipes import (
    load_recipes,
    match_recipe_for_intent,
    validate_recipes,
)
from src.domain.starter_library.archetypes import ARCHETYPE_STARTERS, known_archetype_ids


def check_admission_consistency() -> list[str]:
    """Return cross-registry problems. Empty means every registry agrees."""
    problems: list[str] = []

    rule_block_ids = {rule.block_id for rule in RULES}
    unknown_rule_blocks = rule_block_ids - CANONICAL_BLOCK_IDS
    if unknown_rule_blocks:
        problems.append(
            f"planner.RULES references unfrozen App Blocks: {sorted(unknown_rule_blocks)}"
        )

    unknown_negation_blocks = set(_RECIPE_NEGATION_TERMS) - CANONICAL_BLOCK_IDS
    if unknown_negation_blocks:
        problems.append(
            "planner._RECIPE_NEGATION_TERMS references unfrozen App Blocks: "
            f"{sorted(unknown_negation_blocks)}"
        )

    # A block a user can ask *for* (RULES) but never ask *against* would make
    # "do not add X" silently no-op for that block once a recipe pulls it in.
    rule_blocks_without_guard = rule_block_ids - set(_RECIPE_NEGATION_TERMS)
    if rule_blocks_without_guard:
        problems.append(
            "RULES blocks with no negation guard in _RECIPE_NEGATION_TERMS: "
            f"{sorted(rule_blocks_without_guard)}"
        )

    problems.extend(f"recipe {problem}" for problem in validate_recipes())

    known_starters = known_archetype_ids()
    for pack in JOB_FEATURE_PACKS.values():
        unknown_blocks = (
            set(pack.required_app_blocks) | set(pack.optional_app_blocks)
        ) - CANONICAL_BLOCK_IDS
        if unknown_blocks:
            problems.append(
                f"job pack {pack.id} references unfrozen App Blocks: {sorted(unknown_blocks)}"
            )
        unknown_starters = set(pack.compatible_starters) - known_starters
        if unknown_starters:
            problems.append(
                f"job pack {pack.id} references unknown archetype Starters: "
                f"{sorted(unknown_starters)}"
            )

    known_packs = known_job_pack_ids()
    for starter in ARCHETYPE_STARTERS.values():
        unknown_caps = set(starter.required_capabilities) - CANONICAL_BLOCK_IDS
        if unknown_caps:
            problems.append(
                f"archetype {starter.id} required_capabilities reference unfrozen "
                f"App Blocks: {sorted(unknown_caps)}"
            )
        unknown_job_packs = (
            set(starter.default_job_packs) | set(starter.optional_job_packs)
        ) - known_packs
        if unknown_job_packs:
            problems.append(
                f"archetype {starter.id} references unknown job packs: "
                f"{sorted(unknown_job_packs)}"
            )

    # Every composition-ready recipe must still be reachable by its own first
    # intent phrase, and the two independent match_recipe_for_intent call
    # sites (planner.select_capabilities, compiler.py's direct call feeding
    # resolve_composition) must agree on which recipe that phrase resolves to.
    for recipe in load_recipes(composition_ready_only=True):
        if not recipe.intent_phrases:
            continue
        probe = ProductIntent(source_prompt=recipe.intent_phrases[0])
        matched = match_recipe_for_intent(probe)
        if matched is None or matched.id != recipe.id:
            # A different, more specific recipe may legitimately win the same
            # phrase; that is a recipe-authoring concern, not a registry
            # cross-check, so it is out of scope here.
            continue
        resolved = resolve_composition(matched, probe)
        if resolved.matched_recipe_id != recipe.id:
            problems.append(
                f"recipe {recipe.id}: resolve_composition disagrees with "
                "match_recipe_for_intent on matched_recipe_id"
            )

    return problems
