from src.domain.app_blocks.canonical import canonicalize_capability_id
from src.domain.app_blocks.catalogs import canonical_feature_pack
from src.domain.app_blocks.errors import UnknownAppBlockError
from src.domain.app_blocks.registry import AppBlockRegistry
from src.domain.app_blocks.schemas import (
    AppBlockGap,
    AppBlockManifest,
    ConfigurationRequirement,
    DependencyRef,
    OptionalRequirementStatus,
    PlannedBlock,
)

OPTIONAL_STATUS_SATISFIED = "satisfied"
OPTIONAL_STATUS_UNAVAILABLE = "unavailable"

OPTIONAL_REQUIRES_CONSUMPTION: dict[str, object] = {
    "slice_id": "9c_optional_requires_consumption",
    "status": "complete",
    "planner_optional_requirements": "non_blocking",
    "planner_optional_auto_install": False,
    "canonical_capability_normalization": True,
    "plan_json_includes_optional_requirements": False,
    "database_schema_change": False,
    "runtime_change": False,
    "codegen_behavior_change": False,
    "docs": "docs/audits/APP_BLOCKS_OPTIONAL_REQUIRES_9C.md",
}


def resolve_block(
    manifest: AppBlockManifest,
    registry: AppBlockRegistry,
    *,
    primitives: frozenset[str],
    known_packs: frozenset[str],
) -> tuple[
    list[str],
    list[str],
    list[str],
    list[AppBlockGap],
    list[ConfigurationRequirement],
    list[OptionalRequirementStatus],
]:
    """Expand one block into pack/primitive ids, nested block ids, gaps, config, optionals.

    ``optional_requires`` are observed only. They never install packs, primitives, or nested blocks.
    """
    packs: list[str] = []
    primitive_ids: list[str] = []
    nested_blocks: list[str] = []
    gaps: list[AppBlockGap] = []
    configs: list[ConfigurationRequirement] = []
    optionals: list[OptionalRequirementStatus] = []

    if not manifest.implemented:
        fallback = manifest.composition_fallback
        if fallback and fallback.supported:
            for pack_id in fallback.feature_packs:
                _apply_required(
                    DependencyRef(kind="feature_pack", id=pack_id),
                    manifest.id,
                    registry,
                    primitives,
                    known_packs,
                    packs,
                    primitive_ids,
                    nested_blocks,
                    gaps,
                )
            # Fallback blocks may themselves declare supported fallback. The
            # compiler resolves them and validates the reachable graph.
            nested_blocks.extend(fallback.blocks)
        else:
            gaps.append(
                AppBlockGap(
                    type="block_missing",
                    id=manifest.id,
                    severity="build_blocking",
                    affects=[manifest.id],
                    message=(
                        f"App Block {manifest.id} is {manifest.status}. "
                        "Do not invent a private backend substitute."
                    ),
                )
            )
            return packs, primitive_ids, nested_blocks, gaps, configs, optionals

    for dep in manifest.requires:
        _apply_required(
            dep,
            manifest.id,
            registry,
            primitives,
            known_packs,
            packs,
            primitive_ids,
            nested_blocks,
            gaps,
        )
    optionals = observe_optional_requires(
        manifest,
        registry,
        primitives=primitives,
        known_packs=known_packs,
    )

    for binding in manifest.provider_bindings:
        capability, _alias = canonicalize_capability_id(binding.capability)
        if binding.missing_configuration_behavior == "block_install":
            severity = "build_blocking"
        elif binding.configuration_required:
            severity = "runtime_blocking"
        else:
            severity = "optional"
        if severity != "build_blocking":
            configs.append(
                ConfigurationRequirement(
                    capability=capability,
                    providers=binding.providers,
                    behavior=binding.missing_configuration_behavior,
                )
            )
            gaps.append(
                AppBlockGap(
                    type="configuration_required",
                    id=capability,
                    severity=severity,
                    affects=[manifest.id],
                    message=(f"{capability} may be degraded until a provider is configured."),
                )
            )
        else:
            gaps.append(
                AppBlockGap(
                    type="configuration_required",
                    id=capability,
                    severity="build_blocking",
                    affects=[manifest.id],
                    message=f"{capability} must be configured before install.",
                )
            )
    return packs, primitive_ids, nested_blocks, gaps, configs, optionals


def observe_optional_requires(
    manifest: AppBlockManifest,
    registry: AppBlockRegistry,
    *,
    primitives: frozenset[str],
    known_packs: frozenset[str],
) -> list[OptionalRequirementStatus]:
    """Classify optional deps. Never mutates install lists. Canonicalizes aliases."""
    seen: dict[tuple[str, str], OptionalRequirementStatus] = {}
    for dep in manifest.optional_requires:
        kind, canonical_id, source_id, satisfied = _optional_observation(
            dep, registry, primitives, known_packs
        )
        key = (kind, canonical_id)
        status = OPTIONAL_STATUS_SATISFIED if satisfied else OPTIONAL_STATUS_UNAVAILABLE
        existing = seen.get(key)
        if existing is None:
            seen[key] = OptionalRequirementStatus(
                kind=kind,
                id=canonical_id,
                source_id=source_id,
                status=status,
            )
            continue
        if existing.status == OPTIONAL_STATUS_UNAVAILABLE and satisfied:
            seen[key] = existing.model_copy(update={"status": OPTIONAL_STATUS_SATISFIED})
    return list(seen.values())


def _optional_observation(
    dep: DependencyRef,
    registry: AppBlockRegistry,
    primitives: frozenset[str],
    known_packs: frozenset[str],
) -> tuple[str, str, str, bool]:
    if dep.kind == "primitive":
        canonical_id, _alias = canonicalize_capability_id(dep.id)
        return dep.kind, canonical_id, dep.id, canonical_id in primitives
    if dep.kind == "feature_pack":
        slug = canonical_feature_pack(dep.id)
        return dep.kind, slug, dep.id, slug in known_packs
    nested = registry.get(dep.id)
    implemented = bool(nested and nested.implemented)
    return dep.kind, dep.id, dep.id, implemented


def _apply_required(
    dep: DependencyRef,
    owner_id: str,
    registry: AppBlockRegistry,
    primitives: frozenset[str],
    known_packs: frozenset[str],
    packs: list[str],
    primitive_ids: list[str],
    nested_blocks: list[str],
    gaps: list[AppBlockGap],
) -> None:
    if dep.kind == "primitive":
        canonical_id, _alias = canonicalize_capability_id(dep.id)
        primitive_ids.append(canonical_id)
        if canonical_id not in primitives:
            gaps.append(
                AppBlockGap(
                    type="primitive_missing",
                    id=canonical_id,
                    severity="build_blocking",
                    affects=[owner_id],
                    message=(
                        f"Missing kernel primitive {canonical_id}. This is Level 2 work, "
                        "not permission to invent a project-local backend."
                    ),
                )
            )
        return
    if dep.kind == "feature_pack":
        slug = canonical_feature_pack(dep.id)
        packs.append(slug)
        if slug not in known_packs:
            gaps.append(
                AppBlockGap(
                    type="feature_pack_missing",
                    id=slug,
                    severity="build_blocking",
                    affects=[owner_id],
                    message=f"Feature pack {slug} is not in the Core catalog.",
                )
            )
        return
    nested_blocks.append(dep.id)
    try:
        nested = registry.require(dep.id)
    except UnknownAppBlockError:
        gaps.append(
            AppBlockGap(
                type="block_missing",
                id=dep.id,
                severity="build_blocking",
                affects=[owner_id],
                message=f"Unknown App Block dependency {dep.id}.",
            )
        )
        return
    if not nested.implemented:
        gaps.append(
            AppBlockGap(
                type="block_missing",
                id=dep.id,
                severity="build_blocking",
                affects=[owner_id],
                message=f"Required App Block {dep.id} is not implemented.",
            )
        )


def planned_from_manifest(
    manifest: AppBlockManifest,
    reason: str,
    slots: dict,
    optional_requirements: list[OptionalRequirementStatus] | None = None,
) -> PlannedBlock:
    # Pack references retain their authored namespace (semantic aliases are
    # legal here). Only primitive aliases use the canonical capability boundary.
    dependencies = {}
    for dep in manifest.requires:
        canonical_id = canonicalize_capability_id(dep.id)[0] if dep.kind == "primitive" else dep.id
        normalized = dep.model_copy(update={"id": canonical_id})
        dependencies[(dep.kind, canonical_id, dep.min_version or "", dep.min_version is None)] = (
            normalized
        )
    return PlannedBlock(
        id=manifest.id,
        version=manifest.version,
        reason=reason,
        slots=slots,
        dependencies=[dependencies[key] for key in sorted(dependencies)],
        protected_bindings=sorted({item.id for item in manifest.bindings if item.protected}),
        optional_requirements=list(optional_requirements or []),
    )
