"""First-time default auth provisioning for one project (AuthV2 rollout, pre-backfill).

Wraps ProjectAuthService.ensure_default_auth for a single project that has no auth config yet.
That call writes:

Core (local DB):
- the project auth config (passwordless, audiences staff and customer);
- an RSA key pair, with the private key encrypted;
- four default roles: member, staff, admin (codegen, admin tier), and super_admin (protected).

Cloud (live, non-fatal per step):
- pushes the auth config, roles and keys;
- upserts a Cloud app user for each workspace identity (owner, admins, members);
- revokes Cloud users previously marked as workspace-inherited who left the workspace.

It does not create memberships. Membership reconcile runs only when the project's projection
state is eligible, so a disabled project is skipped. Memberships come from the backfill step.

Safety rules:
- Dry run is the default. Nothing is written without --apply.
- Refuses any project that already has an auth config, so it only does a first provision.
- --core-only blocks every Cloud call. Cloud gets nothing until a later control-plane sync.
- Requires an explicit project id. There is no "all projects" mode.

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

    python -m scripts.authz_v2_ensure_default_auth --project-id <uuid>                 # dry run
    python -m scripts.authz_v2_ensure_default_auth --project-id <uuid> --core-only --apply
    python -m scripts.authz_v2_ensure_default_auth --project-id <uuid> --apply

Exit code is 0 when the project ends with an auth config and the planned writes are complete,
1 otherwise, 2 for refusals.
"""

from __future__ import annotations

import argparse
import asyncio
import sys
from uuid import UUID

from src.core.orm_import import import_all_orm_models

import_all_orm_models()

DEFAULT_ROLE_NAMES = ("member", "staff", "admin", "super_admin")


class _CloudBlocked:
    """Stands in for CloudControlClient under --core-only. Every Cloud call fails fast.

    ensure_default_auth catches these failures per step, so the Core writes still complete.
    """

    def __getattr__(self, name: str):
        async def _blocked(*_args, **_kwargs):
            raise RuntimeError(f"cloud_blocked_core_only:{name}")

        return _blocked


async def _run(project_id: UUID, apply: bool, core_only: bool) -> int:
    from sqlalchemy import select

    from src.core.database import async_session_factory
    from src.domain.project_auth.models import ProjectAuthConfig, ProjectAuthKey, ProjectAuthRole
    from src.domain.project_auth.repository import (
        ProjectAuthConfigRepository,
        ProjectAuthKeyRepository,
        ProjectAuthRoleRepository,
    )
    from src.domain.project_auth.service import ProjectAuthService
    from src.domain.projects.models import Project
    from src.integrations.cloud_control_client import CloudControlClient

    async with async_session_factory() as db:
        try:
            project = (await db.execute(select(Project).where(Project.id == project_id))).scalar_one_or_none()
            if project is None:
                print(f"[error] project {project_id} not found", file=sys.stderr)
                return 2

            existing_config = (await db.execute(
                select(ProjectAuthConfig).where(ProjectAuthConfig.project_id == project_id)
            )).scalar_one_or_none()
            if existing_config is not None:
                print(f"[refused] {project_id} already has an auth config; use the backfill instead",
                      file=sys.stderr)
                return 2

            existing_keys = (await db.execute(
                select(ProjectAuthKey.key_id).where(ProjectAuthKey.project_id == project_id)
            )).scalars().all()
            existing_roles = set((await db.execute(
                select(ProjectAuthRole.name).where(ProjectAuthRole.project_id == project_id)
            )).scalars().all())

            print(f"Project: {project.name!r} ({project_id}), workspace {project.workspace_id}")
            print(f"Existing keys: {len(existing_keys)}; existing roles: {sorted(existing_roles) or 'none'}")
            print("Planned Core writes:")
            print("  + auth config (passwordless; audiences staff, customer)")
            print("  + RSA key pair (private key encrypted)")
            for name in DEFAULT_ROLE_NAMES:
                if name not in existing_roles:
                    print(f"  + role {name}")
            print(f"Cloud: {'BLOCKED (--core-only)' if core_only else 'LIVE (push config, keys, workspace users)'}")

            if not apply:
                print("\n[DRY RUN] nothing written.")
                return 0

            cloud = _CloudBlocked() if core_only else CloudControlClient()
            service = ProjectAuthService(
                config_repo=ProjectAuthConfigRepository(db),
                key_repo=ProjectAuthKeyRepository(db),
                role_repo=ProjectAuthRoleRepository(db),
                cloud_client=cloud,
            )
            created = await service.ensure_default_auth(project_id)
            await db.commit()

            config_now = (await db.execute(
                select(ProjectAuthConfig.project_id).where(ProjectAuthConfig.project_id == project_id)
            )).scalar_one_or_none()
            roles_now = set((await db.execute(
                select(ProjectAuthRole.name).where(ProjectAuthRole.project_id == project_id)
            )).scalars().all())
            missing = [n for n in DEFAULT_ROLE_NAMES if n not in roles_now]
            print(f"\n[APPLY] created={created}; config={'yes' if config_now else 'no'}; "
                  f"roles={sorted(roles_now)}")
            if config_now is None or missing:
                print(f"[incomplete] missing config or roles: {missing or 'config'}", file=sys.stderr)
                return 1
            return 0
        except Exception:
            await db.rollback()
            raise


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--project-id", type=UUID, required=True)
    parser.add_argument("--apply", action="store_true", help="write changes (default: dry run)")
    parser.add_argument("--core-only", action="store_true",
                        help="block every Cloud call; Core writes only")
    args = parser.parse_args()
    sys.exit(asyncio.run(_run(args.project_id, args.apply, args.core_only)))


if __name__ == "__main__":
    main()
