"""OIDC identity + avatar / display_name on users

Revision ID: 0015_oidc_and_avatar
Revises: 0014_share_status
Create Date: 2026-04-22

Lets users sign in via an OIDC provider (Authentik) and carry a profile
image / display name from the provider. Password-only users are
unaffected.

  1. Add users.oidc_issuer, users.oidc_sub (identity pair from the IdP).
  2. Add users.avatar_url, users.display_name (profile bits from claims
     or manually set).
  3. Make users.hashed_password nullable — OIDC-only users have no local
     password. Existing rows all have hashes so the NULLability change
     is backwards-compatible.
  4. Partial unique index on (oidc_issuer, oidc_sub) WHERE oidc_sub IS
     NOT NULL so multiple password-only users (both NULL) don't collide.
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

revision: str = "0015_oidc_and_avatar"
down_revision: Union[str, None] = "0014_share_status"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    conn = op.get_bind()

    # 1 + 2. Add new columns (idempotent — create_all on fresh installs
    # already built them from the model).
    for col_def in (
        "oidc_issuer VARCHAR",
        "oidc_sub VARCHAR",
        "avatar_url VARCHAR",
        "display_name VARCHAR",
    ):
        conn.execute(sa.text(f"ALTER TABLE users ADD COLUMN IF NOT EXISTS {col_def}"))

    # 3. Drop NOT NULL on hashed_password. Postgres only — SQLite can't
    # alter column nullability in place, but the SQLite escape hatch is
    # used for fresh local dev where create_all already wrote the new
    # nullable definition.
    if conn.dialect.name == "postgresql":
        conn.execute(sa.text(
            "ALTER TABLE users ALTER COLUMN hashed_password DROP NOT NULL"
        ))

    # 4. Partial unique index — Postgres supports the WHERE clause so
    # NULLs don't collide; SQLite treats NULLs as distinct in unique
    # indexes already, so a plain unique index is safe there too.
    if conn.dialect.name == "postgresql":
        conn.execute(sa.text(
            "CREATE UNIQUE INDEX IF NOT EXISTS ix_users_oidc_identity "
            "ON users (oidc_issuer, oidc_sub) WHERE oidc_sub IS NOT NULL"
        ))
    else:
        conn.execute(sa.text(
            "CREATE UNIQUE INDEX IF NOT EXISTS ix_users_oidc_identity "
            "ON users (oidc_issuer, oidc_sub)"
        ))


def downgrade() -> None:
    conn = op.get_bind()
    conn.execute(sa.text("DROP INDEX IF EXISTS ix_users_oidc_identity"))

    if conn.dialect.name == "postgresql":
        # Can't re-apply NOT NULL if any OIDC-only user has NULL — so
        # only do it when safe.
        conn.execute(sa.text(
            "UPDATE users SET hashed_password = '' WHERE hashed_password IS NULL"
        ))
        conn.execute(sa.text(
            "ALTER TABLE users ALTER COLUMN hashed_password SET NOT NULL"
        ))

    for col in ("display_name", "avatar_url", "oidc_sub", "oidc_issuer"):
        conn.execute(sa.text(f"ALTER TABLE users DROP COLUMN IF EXISTS {col}"))
