"""Forge connections move to the user level (#2778)

Revision ID: 0078
Revises: 0077
Create Date: 2026-08-19

A forge token is a user's credential, not an instance's: the single
admin-settings config meant every user's snippet-freshness and coverage reads
ran under the operator's token. Each user now owns a keyring of connections —
one per forge host — and projects resolve forge reads on their OWNER's
keyring, with an optional per-project pin (projects.forge_connection_id).

The data move carries the existing admin config into a connection row for the
first admin user (host parsed from the base URL), then deletes the old
setting keys outright — no legacy dual-read (rule #22). The env-var channel
(FORGE_KIND/FORGE_BASE_URL/FORGE_TOKEN) is untouched by this migration; it
survives as an implicit keyring entry for admin users only.
"""
from urllib.parse import urlsplit

import sqlalchemy as sa
from alembic import op

revision = "0078"
down_revision = "0077"
branch_labels = None
depends_on = None

_SETTING_KEYS = ("forge_kind", "forge_base_url", "forge_token")


def upgrade() -> None:
    op.create_table(
        "forge_connections",
        sa.Column("id", sa.Integer(), primary_key=True),
        sa.Column(
            "user_id",
            sa.Integer(),
            sa.ForeignKey("users.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column("kind", sa.Text(), nullable=False),
        sa.Column("base_url", sa.Text(), nullable=False),
        sa.Column("host", sa.Text(), nullable=False),
        sa.Column("token", sa.Text(), nullable=False),
        sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
        sa.UniqueConstraint("user_id", "host", name="uq_forge_connections_user_host"),
    )
    op.add_column(
        "projects",
        sa.Column(
            "forge_connection_id",
            sa.BigInteger(),
            sa.ForeignKey(
                "forge_connections.id",
                ondelete="SET NULL",
                name="fk_projects_forge_connection_id",
            ),
            nullable=True,
        ),
    )

    # Data move: the admin-settings config becomes the first admin's keyring
    # row. All three values must be present — a partial config never produced
    # an adapter, so carrying it over would invent a connection that never
    # worked.
    conn = op.get_bind()
    row = conn.execute(
        sa.text(
            "SELECT s.key, s.value FROM settings s"
            " JOIN users u ON u.id = s.user_id"
            " WHERE u.role = 'admin' AND s.key IN :keys"
            " AND s.user_id = ("
            "   SELECT MIN(id) FROM users WHERE role = 'admin'"
            " )"
        ).bindparams(sa.bindparam("keys", expanding=True)),
        {"keys": list(_SETTING_KEYS)},
    ).fetchall()
    values = {key: (value or "").strip() for key, value in row}
    kind = values.get("forge_kind", "").lower()
    base_url = values.get("forge_base_url", "").rstrip("/")
    token = values.get("forge_token", "")
    host = (urlsplit(base_url).hostname or "").lower()
    if kind and base_url and token and host:
        conn.execute(
            sa.text(
                "INSERT INTO forge_connections"
                " (user_id, kind, base_url, host, token, created_at, updated_at)"
                " SELECT MIN(id), :kind, :base_url, :host, :token, NOW(), NOW()"
                " FROM users WHERE role = 'admin'"
            ),
            {"kind": kind, "base_url": base_url, "host": host, "token": token},
        )
    conn.execute(
        sa.text(
            "DELETE FROM settings WHERE key IN :keys"
        ).bindparams(sa.bindparam("keys", expanding=True)),
        {"keys": list(_SETTING_KEYS)},
    )


def downgrade() -> None:
    # Reverse data move: the first admin's row (if any) becomes the admin
    # settings again. Other users' rows have no pre-0078 representation and
    # are dropped with the table.
    conn = op.get_bind()
    row = conn.execute(
        sa.text(
            "SELECT user_id, kind, base_url, token FROM forge_connections"
            " WHERE user_id = (SELECT MIN(id) FROM users WHERE role = 'admin')"
            " ORDER BY id LIMIT 1"
        )
    ).fetchone()
    if row is not None:
        for key, value in (
            ("forge_kind", row.kind),
            ("forge_base_url", row.base_url),
            ("forge_token", row.token),
        ):
            conn.execute(
                sa.text(
                    "INSERT INTO settings (user_id, key, value)"
                    " VALUES (:uid, :key, :value)"
                    " ON CONFLICT (user_id, key) DO UPDATE SET value = :value"
                ),
                {"uid": row.user_id, "key": key, "value": value},
            )
    op.drop_column("projects", "forge_connection_id")
    op.drop_table("forge_connections")
