"""Set per-project projection state to shadow (or back to disabled), gated on a drift report.

Shadow lets Core reconcile a project but never sends anything to Cloud. This script never
sets `enabled`; that is a separate, later step with its own approval.

Safety rules:
- Dry run is the default. Nothing is written without --apply.
- Projects must be named explicitly (--project-id, repeatable, or --project-ids-file).
- A project is changed only if the drift report you pass in (--drift-report, the JSON from
  scripts.authz_v2_drift_inventory --json) classifies it SAFE. Missing or non-SAFE projects
  are skipped and reported.
- Each change is audited (projection_state_changed) and committed per project.

Usage (P2HS-BE root, same .env as the API):

    python -m scripts.authz_v2_set_projection_state --project-ids-file safe.txt \
        --drift-report drift_all_applied.json                       # dry run
    python -m scripts.authz_v2_set_projection_state --project-ids-file safe.txt \
        --drift-report drift_all_applied.json --apply
    python -m scripts.authz_v2_set_projection_state --project-id <uuid> \
        --drift-report drift.json --state disabled --apply          # rollback

Exit code is 0 when every requested project was changed (or was already in the target state).
"""

from __future__ import annotations

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

from src.core.orm_import import import_all_orm_models

import_all_orm_models()

SAFE = "SAFE"


def _load_ids(args: argparse.Namespace) -> list[UUID]:
    raw: list[str] = list(args.project_id or [])
    if args.project_ids_file:
        for line in Path(args.project_ids_file).read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line and not line.startswith("#"):
                raw.append(line)
    seen: list[UUID] = []
    for value in raw:
        project_id = UUID(value)
        if project_id not in seen:
            seen.append(project_id)
    return seen


def _safe_projects(drift_path: str) -> set[str]:
    rows = json.loads(Path(drift_path).read_text(encoding="utf-8"))
    return {r["project_id"] for r in rows if r.get("classification") == SAFE}


async def _run(project_ids: list[UUID], safe: set[str], state: str, apply: bool) -> int:
    from src.core.database import async_session_factory
    from src.domain.project_auth.projection_eligibility import project_state, set_projection_state

    results: list[dict] = []
    for project_id in project_ids:
        entry: dict = {"project_id": str(project_id), "target": state}
        if str(project_id) not in safe:
            entry.update(result="SKIPPED", reason="not_safe_in_drift_report", written=False)
            results.append(entry)
            continue
        try:
            async with async_session_factory() as db:
                previous = await project_state(db, project_id)
                if previous == state:
                    entry.update(result="UNCHANGED", previous=previous, written=False)
                    await db.rollback()
                elif apply:
                    await set_projection_state(db, project_id, state)
                    await db.commit()
                    entry.update(result="CHANGED", previous=previous, written=True)
                else:
                    entry.update(result="WOULD_CHANGE", previous=previous, written=False)
                    await db.rollback()
        except Exception as exc:  # one project's failure must not stop the batch
            entry.update(result="ERROR", reason=f"{type(exc).__name__}:{str(exc)[:160]}",
                         written=False)
        results.append(entry)

    mode = "APPLY" if apply else "DRY RUN"
    for r in results:
        print(f"{r['project_id']}  {r['result']:<12}  {r.get('previous', '-')} -> {r['target']}"
              f"  {r.get('reason', '')}")
    counts: dict[str, int] = {}
    for r in results:
        counts[r["result"]] = counts.get(r["result"], 0) + 1
    written = sum(1 for r in results if r["written"])
    print(f"\n[{mode}] {len(results)} requested, {written} written. " + ", ".join(
        f"{k}={v}" for k, v in sorted(counts.items())))

    bad = {"ERROR", "SKIPPED"}
    return 1 if any(r["result"] in bad for r in results) else 0


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--project-id", action="append", help="project UUID (repeatable)")
    parser.add_argument("--project-ids-file", help="file with one project UUID per line")
    parser.add_argument("--drift-report", required=True,
                        help="drift JSON from authz_v2_drift_inventory --json")
    parser.add_argument("--state", choices=["shadow", "disabled"], default="shadow",
                        help="target state; 'enabled' is intentionally not offered here")
    parser.add_argument("--apply", action="store_true", help="write changes (default: dry run)")
    args = parser.parse_args()
    project_ids = _load_ids(args)
    if not project_ids:
        parser.error("name projects with --project-id or --project-ids-file; no 'all' mode")
    safe = _safe_projects(args.drift_report)
    sys.exit(asyncio.run(_run(project_ids, safe, args.state, args.apply)))


if __name__ == "__main__":
    main()
