"""Generic operator reconciliation for project composition and capabilities."""
from __future__ import annotations

from typing import Any
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.app_blocks.planner import source_prompt_from_discovery
from src.domain.codegen.app_block_plan import _latest_generation_plan
from src.domain.codegen.composition_manifest import render_core_composition_files
from src.domain.feature_packs.models import FeaturePack, ProjectFeaturePack
from src.domain.feature_packs.normalize import load_pack_contract
from src.domain.projects.discovery_repository import DiscoveryRepository

_CALLABLE = frozenset({
    "installed", "configuration_required", "configured", "deployed", "verified",
    "enabled", "supported",
})


async def desired_composition(db: AsyncSession, project_id: UUID) -> dict[str, Any]:
    """Return canonical desired packs and provenance for one project.

    A completed GenerationPlan wins over Discovery. Discovery is only a
    compatibility fallback for projects that predate persisted plans.
    """
    generation_plan = await _latest_generation_plan(db, project_id)
    if generation_plan:
        try:
            files = render_core_composition_files(generation_plan)
            plan = __import__("json").loads(files["p2hs/app-blocks-plan.json"])
            return {
                "source": "generation_plan",
                "confidence": "authoritative",
                "feature_packs": sorted(set(plan.get("required_feature_packs") or [])),
                "app_blocks": sorted(
                    {b.get("id") for b in plan.get("blocks") or [] if b.get("id")}
                ),
                "dependency_provenance": _dependency_provenance(plan),
                "generation_plan": generation_plan,
                "errors": [],
            }
        except (KeyError, TypeError, ValueError):
            return {
                "source": "generation_plan",
                "confidence": "authoritative",
                "feature_packs": [],
                "app_blocks": [],
                "generation_plan": generation_plan,
                "errors": ["generation_plan_composition_invalid"],
            }

    project = await DiscoveryRepository(db).get(project_id)
    spec = ((project.discovery_json or {}).get("result") or {}).get("spec") if project else None
    if spec:
        try:
            from src.domain.composition_recipes.compile import compile_pinned_install_plan

            plan = await compile_pinned_install_plan(
                db,
                project.discovery_json,
                spec,
                source_prompt=source_prompt_from_discovery(project.discovery_json),
                project_type=getattr(project, "type", None),
            )
        except (KeyError, TypeError, ValueError) as exc:
            return {
                "source": "discovery",
                "confidence": "fallback",
                "feature_packs": [],
                "app_blocks": [],
                "generation_plan": None,
                "errors": [f"discovery_composition_invalid:{str(exc)[:180]}"],
            }
        return {
            "source": "discovery",
            "confidence": "fallback",
            "feature_packs": sorted(set(plan.required_feature_packs)),
            "app_blocks": sorted({b.id for b in plan.blocks}),
            "dependency_provenance": {
                pack: [{"source": "discovery", "required_by": []}]
                for pack in sorted(set(plan.required_feature_packs))
            },
            "generation_plan": None,
            "errors": [],
        }
    return {
        "source": "none",
        "confidence": "none",
        "feature_packs": [],
        "app_blocks": [],
        "dependency_provenance": {},
        "generation_plan": None,
        "errors": [],
    }


def _dependency_provenance(plan: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
    """Explain canonical pack requirements without making generated code authoritative."""
    blocks = plan.get("blocks") or []
    app_blocks = [
        block for block in blocks
        if isinstance(block, dict) and isinstance(block.get("id"), str)
    ]
    provenance: dict[str, list[dict[str, Any]]] = {}
    required_packs = {str(pack) for pack in plan.get("required_feature_packs") or []}
    for pack in sorted(required_packs):
        direct_sources = [
            {
                "source": "generation_plan",
                "required_by": block["id"],
                "requirement": block.get("requirement"),
            }
            for block in app_blocks
            if any(
                isinstance(dep, dict)
                and dep.get("kind") == "feature_pack"
                and str(dep.get("id")) == pack
                for dep in block.get("dependencies") or []
            )
        ] or [{"source": "generation_plan", "required_by": "generation_plan"}]
        provenance[pack] = direct_sources
    return provenance


async def build_reconciliation_plan(db: AsyncSession, project_id: UUID) -> dict[str, Any]:
    desired = await desired_composition(db, project_id)
    rows = list((await db.execute(
        select(ProjectFeaturePack, FeaturePack)
        .join(FeaturePack, FeaturePack.slug == ProjectFeaturePack.pack_slug)
        .where(ProjectFeaturePack.project_id == project_id)
    )).all())
    observed = {row.pack_slug: row for row, _ in rows}
    missing = sorted(set(desired["feature_packs"]) - set(observed))
    blocked = sorted(
        slug
        for slug, row in observed.items()
        if slug in desired["feature_packs"] and row.status == "blocked"
    )
    version_mismatches = sorted(
        row.pack_slug for row, pack in rows
        if row.pack_slug in desired["feature_packs"] and row.pack_version != pack.version
    )
    composition_errors = list(desired.get("errors") or [])
    missing_capabilities: set[str] = set()
    for slug in missing:
        try:
            contract = load_pack_contract(slug, {})
        except (KeyError, TypeError, ValueError):
            continue
        for provided in contract.provide_ids():
            if provided.startswith("form."):
                missing_capabilities.add("forms")
            elif provided.startswith("auth."):
                missing_capabilities.add("auth")
    repair_slugs = sorted(set(missing) | set(blocked))
    return {
        "project_id": str(project_id),
        "source": desired["source"],
        "confidence": desired["confidence"],
        "app_blocks": desired["app_blocks"],
        "desired_feature_packs": sorted(desired["feature_packs"]),
        "observed_feature_packs": sorted(observed),
        "missing_packs": missing,
        "missing_capability_bindings": sorted(missing_capabilities),
        "dependency_provenance": desired.get("dependency_provenance", {}),
        "stale_dependency_closure": bool(missing),
        "version_mismatches": version_mismatches,
        "blocked_installations": blocked,
        "composition_errors": composition_errors,
        "actions": (
            []
            if composition_errors
            else [{"action": "install_pack", "pack_slug": slug} for slug in repair_slugs]
        ),
    }


def composition_repair_plan(plan: dict[str, Any]) -> dict[str, Any]:
    """Install the packs Core already selected when they are missing, blocked, or stale.

    Version-stale rows are reinstalled so the current catalog composition is
    applied. Packs that are not in the desired composition are never added.
    An invalid composition produces no install actions.
    """
    if plan.get("composition_errors"):
        return {**plan, "actions": []}
    desired = set(plan.get("desired_feature_packs") or [])
    slugs = sorted(
        slug
        for slug in (
            set(plan.get("missing_packs") or [])
            | set(plan.get("blocked_installations") or [])
            | set(plan.get("version_mismatches") or [])
        )
        if slug in desired
    )
    return {
        **plan,
        "actions": [{"action": "install_pack", "pack_slug": slug} for slug in slugs],
    }


async def apply_reconciliation(
    db: AsyncSession, project_id: UUID, plan: dict[str, Any]
) -> dict[str, Any]:
    """Install only missing desired packs; callers verify Cloud/ledger afterwards."""
    from src.domain.connectors.repository import ConnectorRepository
    from src.domain.edge_functions.repository import (
        FunctionLibraryRepository,
        FunctionLibraryVersionRepository,
        ProjectFunctionRepository,
        ProjectFunctionTokenRepository,
        ProjectFunctionVersionRepository,
        ProjectWebhookEndpointRepository,
        WorkerRuntimeVersionRepository,
    )
    from src.domain.edge_functions.service import EdgeFunctionsService
    from src.domain.feature_packs.service import PackInstallService
    from src.domain.projects.repository import ProjectRepository
    from src.integrations.cloud_control_client import CloudControlClient

    cloud = CloudControlClient()
    edge = EdgeFunctionsService(
        library_repo=FunctionLibraryRepository(db),
        library_version_repo=FunctionLibraryVersionRepository(db),
        fn_repo=ProjectFunctionRepository(db),
        version_repo=ProjectFunctionVersionRepository(db),
        webhook_repo=ProjectWebhookEndpointRepository(db),
        runtime_repo=WorkerRuntimeVersionRepository(db),
        token_repo=ProjectFunctionTokenRepository(db),
        project_repo=ProjectRepository(db),
        connector_repo=ConnectorRepository(db),
        cloud_client=cloud,
    )
    installer = PackInstallService(db, edge, cloud)
    results = []
    for action in plan["actions"]:
        result = await installer.install_pack(
            project_id=project_id,
            pack_slug=action["pack_slug"],
            config={},
            user_prompt="operator reconciliation",
        )
        results.append(result)
        if not result.get("ok") or result.get("status") == "blocked":
            await db.commit()
            return {"applied": results, "blocked": True}
    await db.commit()
    return {"applied": results, "blocked": False}
