"""Reconcile project composition dependencies in Core and P2HS Cloud.

This is an operator-controlled cohort tool. Dry run is the default. Nothing is
written without --apply. Applying first bootstraps each project into Cloud,
then adopts/installs the canonical App Block dependency closure, and finally
runs the normal Cloud sync/readback reconciliation.

Examples:
    python -m scripts.reconcile_project_capabilities --project-id <uuid>
    python -m scripts.reconcile_project_capabilities --all
    python -m scripts.reconcile_project_capabilities --all --limit 25 --apply

--all is intentionally explicit. The script never deletes packs, disables
capabilities, enables AuthZ-v2, or rewrites generated source.
"""
from __future__ import annotations

import argparse
import asyncio
import json
import sys
from uuid import UUID

from sqlalchemy import select
from src.core.database import async_session_factory
from src.core.orm_import import import_all_orm_models
from src.domain.projects.models import Project

import_all_orm_models()


async def _project_ids(args: argparse.Namespace) -> list[UUID]:
    requested = [UUID(value) for value in args.project_id or []]
    if requested:
        return list(dict.fromkeys(requested))
    if not args.all:
        return []
    async with async_session_factory() as db:
        stmt = (
            select(Project.id)
            .where(
                Project.status.in_(("ready", "creating")),
                Project.deleted_at.is_(None),
            )
            .order_by(Project.updated_at.asc())
            .limit(args.limit)
        )
        if args.workspace_id:
            stmt = stmt.where(Project.workspace_id == UUID(args.workspace_id))
        if args.source_kind:
            stmt = stmt.where(Project.source_kind == args.source_kind)
        return list((await db.execute(stmt)).scalars())


async def _one(project_id: UUID, apply: bool) -> dict:
    from src.domain.capabilities.operator_reconciliation import (
        apply_reconciliation,
        build_reconciliation_plan,
    )
    from src.workers.tasks.cloud_sync import sync_project_to_cloud
    from src.workers.tasks.reconcile_project_capabilities import (
        reconcile_project_capabilities,
    )

    result: dict = {"project_id": str(project_id), "mode": "apply" if apply else "inspect"}
    async with async_session_factory() as db:
        plan = await build_reconciliation_plan(db, project_id)
        result["plan"] = plan
        if not apply:
            return result

    bootstrap = await sync_project_to_cloud(
        {"db_factory": async_session_factory}, str(project_id)
    )
    result["bootstrap"] = bootstrap
    if bootstrap.get("status") != "synced":
        result["verified"] = False
        result["error"] = "cloud_project_bootstrap_failed"
        return result

    async with async_session_factory() as db:
        applied = await apply_reconciliation(db, project_id, plan)
        result["applied"] = applied
        if applied.get("blocked"):
            result["verified"] = False
            result["error"] = "required_dependency_reconciliation_blocked"
            return result

    control = await reconcile_project_capabilities(
        {"db_factory": async_session_factory}, str(project_id)
    )
    result["control_plane"] = control
    async with async_session_factory() as db:
        result["plan_after"] = await build_reconciliation_plan(db, project_id)
    after = result["plan_after"]
    result["verified"] = not (
        after.get("missing_packs")
        or after.get("blocked_installations")
        or after.get("composition_errors")
        or control.get("errors")
    )
    return result


async def _run(args: argparse.Namespace) -> int:
    ids = await _project_ids(args)
    if not ids:
        print("Specify --project-id (repeatable) or explicit --all.", file=sys.stderr)
        return 2

    results: list[dict] = []
    for project_id in ids:
        try:
            results.append(await _one(project_id, args.apply))
        except Exception as exc:  # one project must not abort a cohort
            results.append(
                {
                    "project_id": str(project_id),
                    "verified": False,
                    "error": f"{type(exc).__name__}:{str(exc)[:500]}",
                }
            )

    if args.json:
        print(json.dumps(results, indent=2, default=str))
    else:
        mode = "APPLY" if args.apply else "DRY RUN"
        for item in results:
            plan = item.get("plan_after") or item.get("plan") or {}
            print(
                f"{item['project_id']} {mode} "
                f"missing={','.join(plan.get('missing_packs') or []) or '-'} "
                f"blocked={','.join(plan.get('blocked_installations') or []) or '-'} "
                f"verified={item.get('verified', False)} "
                f"error={item.get('error', '-')}"
            )

    return 0 if results and all(item.get("verified") is True for item in results) else 1


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--project-id", action="append", help="project UUID; repeatable")
    parser.add_argument("--all", action="store_true", help="select all ready/creating projects")
    parser.add_argument("--workspace-id", help="limit --all to one workspace UUID")
    parser.add_argument("--source-kind", help="limit --all to one project source kind")
    parser.add_argument("--limit", type=int, default=100, help="maximum projects for --all")
    parser.add_argument("--apply", action="store_true", help="write Core/Cloud state")
    parser.add_argument("--json", action="store_true", help="emit machine-readable results")
    args = parser.parse_args()
    if args.limit < 1:
        parser.error("--limit must be positive")
    sys.exit(asyncio.run(_run(args)))


if __name__ == "__main__":
    main()
