"""per-project access levels (BW Auth accounts kit)

Replaces the app-wide two-value role with named levels assigned per project.
`role` is dropped rather than kept alongside `level`: two sources of truth for
the same fact diverge, and the generated TypeScript client turns every missed
usage into a build error.

Revision ID: 0003_per_project_levels
Revises: 0002_direction_loop
Create Date: 2026-08-18
"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

revision: str = "0003_per_project_levels"
down_revision: str | None = "0002_direction_loop"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None

# Scout-specific permission strings, alongside the kit's standard vocabulary.
REVIEW = "scout.review"
RESULTS_VIEW = "scout.results.view"
PROJECT_MANAGE = "scout.project.manage"
PROJECTS_CREATE = "scout.projects.create"

# The seeded levels (D17). `admin` holds no levels.* permission — only the owner
# may rewrite the permission model — and cannot assign `admin`, so an admin can
# never mint another admin.
SEED_LEVELS = [
    (
        "admin",
        [
            "accounts.view", "accounts.add", "accounts.delete",
            "accounts.reset_password", "accounts.change_level",
            "instances.create", "instances.grant",
            "instances.default_all", "instances.change_user_level",
            PROJECTS_CREATE, PROJECT_MANAGE, RESULTS_VIEW, REVIEW,
        ],
        ["lead", "reviewer"],
    ),
    ("lead", [REVIEW, RESULTS_VIEW], []),
    ("reviewer", [REVIEW], []),
]


def upgrade() -> None:
    op.create_table(
        "app_levels",
        sa.Column("name", sa.String(40), primary_key=True),
        sa.Column("permissions", sa.JSON(), nullable=False),
        sa.Column("assignable", sa.JSON(), nullable=False),
    )

    levels = sa.table(
        "app_levels",
        sa.column("name", sa.String),
        sa.column("permissions", sa.JSON),
        sa.column("assignable", sa.JSON),
    )
    op.bulk_insert(
        levels,
        [
            {"name": name, "permissions": perms, "assignable": assign}
            for name, perms, assign in SEED_LEVELS
        ],
    )

    # --- app_accounts: the app-wide level -----------------------------------
    op.add_column("app_accounts", sa.Column("level", sa.String(40), nullable=True))
    op.add_column(
        "app_accounts",
        sa.Column("all_instances", sa.Boolean(), nullable=False, server_default="false"),
    )
    # admin -> admin everywhere; user -> reviewer, per-project only.
    op.execute(
        "UPDATE app_accounts SET level='admin', all_instances=true WHERE role='admin'"
    )
    op.execute(
        "UPDATE app_accounts SET level='reviewer', all_instances=false "
        "WHERE role IS DISTINCT FROM 'admin'"
    )
    op.alter_column("app_accounts", "level", nullable=False)
    op.drop_column("app_accounts", "role")

    # --- project_members: the per-project level -----------------------------
    op.add_column(
        "project_members",
        sa.Column("level", sa.String(40), nullable=False, server_default="reviewer"),
    )


def downgrade() -> None:
    op.add_column(
        "app_accounts",
        sa.Column("role", sa.String(32), nullable=False, server_default=""),
    )
    op.execute("UPDATE app_accounts SET role='admin' WHERE level='admin'")
    op.execute("UPDATE app_accounts SET role='user' WHERE level <> 'admin'")
    op.drop_column("app_accounts", "all_instances")
    op.drop_column("app_accounts", "level")
    op.drop_column("project_members", "level")
    op.drop_table("app_levels")
