"""Apply the authorization v2 owner and admin backfill to an approved list of projects.

Writes Core membership state only: creates owner and admin memberships from the workspace,
grants the admin role, and links Cloud users. It never writes to Cloud, never changes
projection_state, and never turns on the projection flag.

Safety rules:
- Dry run is the default. Nothing is written without --apply.
- Projects must be named explicitly (--project-id, repeatable, or --project-ids-file).
  There is no "all projects" mode.
- Each project is planned, then re-planned and applied in its own transaction. A project that
  is not READY at apply time is skipped and reported, and the rest continue.
- Re-running is safe: a converged project plans zero operations.

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

    python -m scripts.authz_v2_backfill_apply --project-id <uuid>               # dry run
    python -m scripts.authz_v2_backfill_apply --project-ids-file batch1.txt      # dry run
    python -m scripts.authz_v2_backfill_apply --project-ids-file batch1.txt --apply

Exit code is 0 when every requested project is READY (applied or dry-run), 1 otherwise.
"""

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()


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


async def _run(project_ids: list[UUID], apply: bool, as_json: bool) -> int:
    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.project_auth.backfill import READY, apply_backfill, plan_backfill
    from src.domain.project_auth.drift_report import fetch_cloud_users

    settings = get_settings()
    if not settings.cloud_control_api_url or not settings.cloud_internal_token:
        print("[error] cloud_control_api_url and cloud_internal_token must be set", file=sys.stderr)
        return 2

    results: list[dict] = []
    for project_id in project_ids:
        entry: dict = {"project_id": str(project_id)}
        try:
            cloud_users = await fetch_cloud_users(
                settings.cloud_control_api_url, settings.cloud_internal_token, str(project_id)
            )
            async with async_session_factory() as db:
                if apply:
                    outcome = await apply_backfill(db, project_id, cloud_users)
                    if outcome.classification == READY:
                        await db.commit()
                    else:
                        await db.rollback()
                else:
                    outcome = await plan_backfill(db, project_id, cloud_users)
                    await db.rollback()
            entry.update(
                classification=outcome.classification,
                reasons=list(outcome.reasons),
                desired_owner=outcome.desired_owner,
                applied_operations=outcome.applied_operations,
                planned_operations=len(outcome.plan.operations) if outcome.plan else 0,
                written=bool(apply and outcome.classification == READY),
            )
        except Exception as exc:  # one project's failure must not stop the batch
            entry.update(classification="ERROR",
                         reasons=[f"{type(exc).__name__}:{str(exc)[:160]}"], written=False)
        results.append(entry)

    if as_json:
        print(json.dumps(results, indent=2))
    else:
        mode = "APPLY" if apply else "DRY RUN"
        for r in results:
            print(f"{r['project_id']}  {r['classification']}  "
                  f"owner={r.get('desired_owner') or '-'}  "
                  f"planned={r.get('planned_operations', 0)}  "
                  f"applied={r.get('applied_operations', 0)}  "
                  f"{', '.join(r.get('reasons', [])) or '-'}")
        ok = sum(1 for r in results if r["classification"] == READY)
        written = sum(1 for r in results if r["written"])
        print(f"\n[{mode}] {len(results)} requested, {ok} READY, {written} written.")

    return 0 if results and all(r["classification"] == READY for r 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("--project-ids-file", help="file with one project UUID per line")
    parser.add_argument("--apply", action="store_true", help="write changes (default is dry run)")
    parser.add_argument("--json", action="store_true")
    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")
    sys.exit(asyncio.run(_run(project_ids, args.apply, args.json)))


if __name__ == "__main__":
    main()
