"""Authorization v2 provisioning for one project: one idempotent workflow.

Runs after the auth config, signing key and roles exist. Stages, each safe to repeat:

1. Reconcile memberships from the workspace (owner, admins) and link Cloud identities.
   Uses the same backfill planner and apply as the manual rollout scripts.
2. Compute drift against the live Cloud roles. Read-only.
3. Enter shadow only when the project is READY and drift is SAFE. Otherwise the state stays
   where it was and the reason is reported.

Shadow is Core-only. Cloud enforcement (authz_v2_enabled) and projection_state = enabled are
never set here.
"""

from __future__ import annotations

from dataclasses import dataclass
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.project_auth.backfill import READY, apply_backfill
from src.domain.project_auth.drift_report import SAFE, fetch_cloud_users, inventory_project
from src.domain.project_auth.projection_eligibility import (
    DISABLED,
    SHADOW,
    project_state,
    set_projection_state,
)

COMPLETE = "complete"
BLOCKED = "blocked"
RETRY = "retry"


@dataclass(frozen=True)
class AuthProvisionOutcome:
    status: str
    projection_state: str
    reason: str | None = None


def decide(
    backfill_classification: str,
    backfill_reasons: tuple[str, ...],
    drift_classification: str,
    drift_reasons: tuple[str, ...],
) -> tuple[str, str | None]:
    """Pure gate: complete only when the backfill is READY and drift is SAFE."""
    if backfill_classification != READY:
        return BLOCKED, "backfill:" + ",".join(backfill_reasons or ("not_ready",))
    if drift_classification != SAFE:
        return BLOCKED, "drift:" + ",".join(drift_reasons or (drift_classification,))
    return COMPLETE, None


async def provision_authz_v2(
    db: AsyncSession,
    project_id: UUID,
    *,
    cloud_base_url: str,
    cloud_token: str,
) -> AuthProvisionOutcome:
    """Run the workflow for one project. Commits after each write stage, so a retry resumes
    from the last completed stage."""
    current = await project_state(db, project_id)
    if not cloud_base_url or not cloud_token:
        return AuthProvisionOutcome(RETRY, current, "cloud_not_configured")
    try:
        cloud_users = await fetch_cloud_users(cloud_base_url, cloud_token, str(project_id))
    except Exception as exc:
        # Cloud unavailable: no writes, so a retry is always safe.
        return AuthProvisionOutcome(RETRY, current, f"cloud_unavailable:{type(exc).__name__}")

    backfill = await apply_backfill(db, project_id, cloud_users)
    if backfill.classification != READY:
        await db.rollback()
        status, reason = decide(backfill.classification, backfill.reasons, "", ())
        return AuthProvisionOutcome(status, current, reason)
    await db.commit()

    drift = await inventory_project(db, project_id, cloud_base_url, cloud_token)
    status, reason = decide(READY, (), drift.classification, drift.reasons)
    if status == COMPLETE and current == DISABLED:
        await set_projection_state(db, project_id, SHADOW)
        await db.commit()
        return AuthProvisionOutcome(COMPLETE, SHADOW)
    # Blocked, or already shadow/enabled: never downgrade an existing state here.
    return AuthProvisionOutcome(status, current, reason)
