"""Operator-only capability drift inspection and repair.

The default operation is read-only.  Applying a repair is explicit and always ends
with a non-secret Cloud readback so an operator can distinguish Core intent from the
state Cloud actually observed.
"""
from __future__ import annotations

from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.database import get_db
from src.domain.admin.router import require_admin
from src.domain.auth.models import User
from src.domain.capabilities.operator_reconciliation import (
    apply_reconciliation,
    build_reconciliation_plan,
)
from src.domain.project_auth.projection_eligibility import project_state
from src.domain.project_auth.repository import (
    ProjectAuthConfigRepository,
    ProjectAuthKeyRepository,
    ProjectAuthRoleRepository,
)
from src.domain.project_auth.service import ProjectAuthService
from src.integrations.cloud_control_client import CloudControlClient

router = APIRouter(
    prefix="/api/admin/projects/{project_id}/capabilities",
    tags=["admin", "capability-lifecycle"],
)


@router.get("/reconcile")
async def inspect_project_capability_reconciliation(
    project_id: UUID,
    _: User = Depends(require_admin),
    db: AsyncSession = Depends(get_db),
) -> dict:
    """Read-only desired-vs-observed composition repair plan."""
    plan = await build_reconciliation_plan(db, project_id)
    return {**plan, "mode": "inspect", "applied": [], "verified": False}


@router.post("/reconcile")
async def apply_project_capability_reconciliation(
    project_id: UUID,
    apply: bool = Query(False, description="Must be true to mutate project capability state."),
    _: User = Depends(require_admin),
    db: AsyncSession = Depends(get_db),
) -> dict:
    """Explicitly adopt/install missing canonical dependencies, then verify state."""
    plan = await build_reconciliation_plan(db, project_id)
    if not apply:
        return {**plan, "mode": "inspect", "applied": [], "verified": False}
    from src.core.database import async_session_factory
    from src.workers.tasks.cloud_sync import sync_project_to_cloud

    # Feature-pack entity provisioning assumes the Cloud project row already
    # exists. Existing projects may have missed the original bootstrap job, so
    # establish the Core -> Cloud control-plane projection before installing
    # entities. The bootstrap endpoint is idempotent.
    bootstrap = await sync_project_to_cloud(
        {"db_factory": async_session_factory}, str(project_id)
    )
    if bootstrap.get("status") != "synced":
        raise HTTPException(
            status_code=502,
            detail={
                "error": "cloud_project_bootstrap_failed",
                "result": bootstrap,
            },
        )
    result = await apply_reconciliation(db, project_id, plan)
    control_result = None
    if not result["blocked"] and not plan.get("composition_errors"):
        from src.workers.tasks.reconcile_project_capabilities import (
            reconcile_project_capabilities,
        )

        # Reuse the normal Cloud sync/readback path after installing the
        # missing canonical dependencies. This keeps operator repair and the
        # periodic reconciler on one idempotent control-plane implementation.
        control_result = await reconcile_project_capabilities(
            {"db_factory": async_session_factory}, str(project_id)
        )
    refreshed = await build_reconciliation_plan(db, project_id)
    remaining = (
        refreshed["missing_packs"]
        + refreshed["blocked_installations"]
        + refreshed.get("composition_errors", [])
    )
    if result["blocked"] or remaining or (control_result and control_result.get("errors")):
        # Required dependency failure is observable and keeps readiness blocked.
        raise HTTPException(
            status_code=409,
            detail={
                "error": "required_dependency_reconciliation_blocked",
                "plan": refreshed,
                "applied": result["applied"],
                "control_plane": control_result,
            },
        )
    return {
        **refreshed,
        "mode": "apply",
        "applied": result["applied"],
        "control_plane": control_result,
        "verified": True,
    }


async def _auth_reconciliation(
    project_id: UUID,
    db: AsyncSession,
    *,
    apply: bool,
) -> dict:
    config_repo = ProjectAuthConfigRepository(db)
    key_repo = ProjectAuthKeyRepository(db)
    core_config = await config_repo.get_by_project(project_id)
    core_key = await key_repo.get_active_for_project(project_id)
    projection_state_value = await project_state(db, project_id)
    cloud = CloudControlClient()

    desired = {
        # This endpoint is the repair surface for the required Auth capability;
        # absence of a Core row/key is therefore itself a repair action.
        "auth_config": True,
        "public_key": True,
        "authz_v2": projection_state_value == "enabled",
        "authz_rollout_mode": projection_state_value,
        "authz_policy_version": core_config.authz_policy_version if core_config else None,
        "authz_policy_hash": core_config.authz_policy_hash if core_config else None,
    }
    observed: dict = {}
    cloud_error: str | None = None
    try:
        observed = await cloud.get_auth_projection(str(project_id))
    except Exception as exc:
        cloud_error = str(exc)[:500]

    plan = []
    if core_config is None:
        plan.append("create_auth_config")
    if core_key is None:
        plan.append("create_public_key")
    if desired["auth_config"] and not observed.get("auth_config_present"):
        plan.append("push_auth_config")
    elif (
        core_config is not None
        and (core_config.cloud_synced_at is None or core_config.cloud_sync_error)
        and observed.get("auth_config_present")
    ):
        # Cloud may already be healthy after an earlier push, while Core's local
        # acknowledgement was lost or never recorded. Re-push idempotently so the
        # persisted capability status can truthfully become READY.
        plan.append("record_core_projection_ack")
    if desired["public_key"] and not observed.get("auth_public_key_present"):
        plan.append("push_public_key")
    if (
        desired["authz_v2"] != bool(observed.get("authz_v2_enabled"))
        or desired["authz_rollout_mode"] != observed.get("authz_rollout_mode", "disabled")
    ):
        plan.append("set_authz_v2")
    if (
        desired["authz_policy_hash"]
        and desired["authz_policy_hash"] != observed.get("authz_policy_hash")
    ):
        plan.append("push_authz_policy")

    if apply:
        if cloud_error:
            raise HTTPException(
                status_code=502,
                detail="Cloud readback unavailable: " + cloud_error,
            )
        initial_plan = list(plan)
        service = ProjectAuthService(
            config_repo=config_repo,
            key_repo=key_repo,
            role_repo=ProjectAuthRoleRepository(db),
            cloud_client=cloud,
        )
        if "create_auth_config" in initial_plan or "create_public_key" in initial_plan:
            await service.ensure_default_auth(project_id)
        if any(
            action in initial_plan
            for action in ("push_auth_config", "push_authz_policy", "create_auth_config", "record_core_projection_ack")
        ):
            await service.push_config_with_roles(project_id)
        if "push_public_key" in initial_plan or "create_public_key" in initial_plan:
            await service.push_active_key(project_id)
        if "set_authz_v2" in initial_plan:
            # Enabling is fail-closed in both Core and Cloud. A config/key/policy
            # readback mismatch must be repaired before the rollout gate changes.
            observed = await cloud.get_auth_projection(str(project_id))
            missing = []
            if not observed.get("auth_config_present"):
                missing.append("AUTH_CONFIG_MISSING")
            if not observed.get("auth_public_key_present"):
                missing.append("AUTH_RUNTIME_NOT_READY")
            if not desired["authz_policy_hash"] or desired["authz_policy_hash"] != observed.get("authz_policy_hash"):
                missing.append("AUTHZ_POLICY_PROJECTION_MISSING")
            if missing:
                raise HTTPException(
                    status_code=409,
                    detail={"error": "AUTHZ_ENABLE_BLOCKED", "missing": missing},
                )
            result = await cloud.set_authz_v2(
                str(project_id), mode=projection_state_value
            )
            if not result.get("ok", False):
                raise HTTPException(status_code=502, detail="Cloud AuthZ repair failed")
        await db.commit()
        observed = await cloud.get_auth_projection(str(project_id))
        remaining = []
        if desired["auth_config"] != bool(observed.get("auth_config_present")):
            remaining.append("auth_config_readback_mismatch")
        if desired["public_key"] != bool(observed.get("auth_public_key_present")):
            remaining.append("public_key_readback_mismatch")
        if desired["authz_v2"] != bool(observed.get("authz_v2_enabled")):
            remaining.append("authz_readback_mismatch")
        if desired["authz_rollout_mode"] != observed.get("authz_rollout_mode", "disabled"):
            remaining.append("authz_rollout_mode_readback_mismatch")
        if desired["authz_policy_hash"] and desired["authz_policy_hash"] != observed.get("authz_policy_hash"):
            remaining.append("authz_policy_readback_mismatch")
        if remaining:
            raise HTTPException(
                status_code=502,
                detail={
                    "error": "capability_reconcile_verification_failed",
                    "remaining": remaining,
                },
            )
    else:
        initial_plan = []
        remaining = plan

    return {
        "project_id": str(project_id),
        "mode": "apply" if apply else "inspect",
        "desired": desired,
        "observed": observed,
        "plan": plan,
        "applied": initial_plan if apply else [],
        "remaining": remaining if apply else plan,
        "cloud_error": cloud_error,
        "verified": apply and not remaining,
    }


@router.get("/{capability}/reconcile")
async def inspect_capability_reconciliation(
    capability: str,
    project_id: UUID,
    _: User = Depends(require_admin),
    db: AsyncSession = Depends(get_db),
) -> dict:
    if capability != "auth":
        raise HTTPException(
            status_code=404,
            detail="Operator reconciliation is not registered for this capability",
        )
    return await _auth_reconciliation(project_id, db, apply=False)


@router.post("/{capability}/reconcile")
async def apply_capability_reconciliation(
    capability: str,
    project_id: UUID,
    apply: bool = Query(False, description="Must be true to mutate Core or Cloud state."),
    _: User = Depends(require_admin),
    db: AsyncSession = Depends(get_db),
) -> dict:
    if capability != "auth":
        raise HTTPException(
            status_code=404,
            detail="Operator reconciliation is not registered for this capability",
        )
    if not apply:
        return await _auth_reconciliation(project_id, db, apply=False)
    return await _auth_reconciliation(project_id, db, apply=True)
