"""repository identity, protected slugs, and rename journal (STATE-WP-0085-T02)

The existing repository UUID remains the durable State Hub identity. This
migration adds only new rows around it: an explicit unverified Forge identity,
one canonical slug record, and empty operation history. It deliberately does
not infer Forge coordinates from a slug, remote URL, checkout, or Git commit.

Revision ID: f3c4d5e6a7b8
Revises: e2b3c4d5f6a7
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB, UUID

revision = "f3c4d5e6a7b8"
down_revision = "e2b3c4d5f6a7"
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.create_table(
        "repository_forge_identities",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "repo_id",
            UUID(as_uuid=True),
            sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
            nullable=False,
        ),
        sa.Column("provider", sa.String(length=40), nullable=True),
        sa.Column("forge_instance", sa.String(length=255), nullable=True),
        sa.Column("forge_owner", sa.String(length=255), nullable=True),
        sa.Column("forge_repository_id", sa.BigInteger(), nullable=True),
        sa.Column(
            "verification_state",
            sa.String(length=20),
            nullable=False,
            server_default="unverified",
        ),
        sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("verified_by", sa.String(length=160), nullable=True),
        sa.Column(
            "verification_evidence", JSONB(), nullable=False, server_default="{}"
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column(
            "updated_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.UniqueConstraint(
            "repo_id", name="uq_repository_forge_identities_repo_id"
        ),
        sa.UniqueConstraint(
            "id",
            "repo_id",
            "verification_state",
            name="uq_repository_forge_identity_verified_ref",
        ),
        sa.UniqueConstraint(
            "provider",
            "forge_instance",
            "forge_owner",
            "forge_repository_id",
            name="uq_repository_forge_identity_tuple",
        ),
        sa.CheckConstraint(
            "verification_state IN ('unverified', 'verified')",
            name="ck_repository_forge_identity_state",
        ),
        sa.CheckConstraint(
            "forge_repository_id IS NULL OR forge_repository_id > 0",
            name="ck_repository_forge_id_positive",
        ),
        sa.CheckConstraint(
            "verification_state != 'verified' OR "
            "(provider IS NOT NULL AND forge_instance IS NOT NULL "
            "AND forge_owner IS NOT NULL AND forge_repository_id IS NOT NULL "
            "AND verified_at IS NOT NULL AND verified_by IS NOT NULL)",
            name="ck_repository_forge_verified_complete",
        ),
        sa.CheckConstraint(
            "verification_state = 'verified' OR "
            "(verified_at IS NULL AND verified_by IS NULL)",
            name="ck_repository_forge_unverified_has_no_attestation",
        ),
    )

    op.create_table(
        "repository_rename_operations",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "repo_id",
            UUID(as_uuid=True),
            sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
            nullable=False,
        ),
        sa.Column("forge_identity_id", UUID(as_uuid=True), nullable=False),
        sa.Column(
            "forge_identity_state",
            sa.String(length=20),
            nullable=False,
            server_default="verified",
        ),
        sa.Column("expected_provider", sa.String(length=40), nullable=False),
        sa.Column(
            "expected_forge_instance", sa.String(length=255), nullable=False
        ),
        sa.Column("expected_forge_owner", sa.String(length=255), nullable=False),
        sa.Column("expected_forge_repository_id", sa.BigInteger(), nullable=False),
        sa.Column("expected_source_commit", sa.String(length=64), nullable=False),
        sa.Column("expected_default_branch", sa.String(length=255), nullable=False),
        sa.Column("old_slug", sa.String(length=100), nullable=False),
        sa.Column("new_slug", sa.String(length=100), nullable=False),
        sa.Column("old_coordinates", JSONB(), nullable=False),
        sa.Column("new_coordinates", JSONB(), nullable=False),
        sa.Column(
            "phase", sa.String(length=32), nullable=False, server_default="draft"
        ),
        sa.Column("actor", sa.String(length=160), nullable=False),
        sa.Column("phase_changed_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("preflighted_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("preflight_expires_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("rolled_back_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("evidence", JSONB(), nullable=False, server_default="{}"),
        sa.Column("error_code", sa.String(length=80), nullable=True),
        sa.Column("error_message", sa.Text(), nullable=True),
        sa.Column("error_details", JSONB(), nullable=True),
        sa.Column("error_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column(
            "updated_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.CheckConstraint(
            "phase IN ('draft', 'preflighted', 'forge-renamed', "
            "'statehub-rebound', 'source-synced', 'consumers-verified', "
            "'completed', 'rollback-preflight', 'rolled-back')",
            name="ck_repository_rename_phase",
        ),
        sa.CheckConstraint(
            "old_slug != new_slug", name="ck_repository_rename_distinct_slugs"
        ),
        sa.CheckConstraint(
            "expected_forge_repository_id > 0",
            name="ck_repository_rename_forge_id_positive",
        ),
        sa.CheckConstraint(
            "forge_identity_state = 'verified'",
            name="ck_repository_rename_verified_identity",
        ),
        sa.CheckConstraint(
            "phase = 'draft' OR preflighted_at IS NOT NULL",
            name="ck_repository_rename_preflight_timestamp",
        ),
        sa.CheckConstraint(
            "phase != 'completed' OR completed_at IS NOT NULL",
            name="ck_repository_rename_completed_timestamp",
        ),
        sa.CheckConstraint(
            "phase != 'rolled-back' OR rolled_back_at IS NOT NULL",
            name="ck_repository_rename_rollback_timestamp",
        ),
        sa.ForeignKeyConstraint(
            ["forge_identity_id", "repo_id", "forge_identity_state"],
            [
                "repository_forge_identities.id",
                "repository_forge_identities.repo_id",
                "repository_forge_identities.verification_state",
            ],
            name="fk_repository_rename_verified_identity",
            ondelete="RESTRICT",
        ),
    )
    op.create_index(
        "ix_repository_rename_operations_repo_id",
        "repository_rename_operations",
        ["repo_id"],
    )
    op.create_index(
        "ix_repository_rename_operations_old_slug",
        "repository_rename_operations",
        ["old_slug"],
    )
    op.create_index(
        "ix_repository_rename_operations_new_slug",
        "repository_rename_operations",
        ["new_slug"],
    )
    op.create_index(
        "ix_repository_rename_phase",
        "repository_rename_operations",
        ["phase"],
    )
    op.create_index(
        "uq_repository_rename_active_repo",
        "repository_rename_operations",
        ["repo_id"],
        unique=True,
        postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"),
    )
    op.create_index(
        "uq_repository_rename_active_new_slug",
        "repository_rename_operations",
        ["new_slug"],
        unique=True,
        postgresql_where=sa.text("phase NOT IN ('completed', 'rolled-back')"),
    )

    op.create_table(
        "repository_slugs",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "repo_id",
            UUID(as_uuid=True),
            sa.ForeignKey("managed_repos.id", ondelete="RESTRICT"),
            nullable=False,
        ),
        sa.Column("slug", sa.String(length=100), nullable=False),
        sa.Column("kind", sa.String(length=16), nullable=False),
        sa.Column(
            "protected", sa.Boolean(), nullable=False, server_default=sa.true()
        ),
        sa.Column(
            "source_operation_id",
            UUID(as_uuid=True),
            sa.ForeignKey("repository_rename_operations.id", ondelete="RESTRICT"),
            nullable=True,
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column(
            "updated_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.UniqueConstraint("slug", name="uq_repository_slugs_slug"),
        sa.CheckConstraint(
            "kind IN ('canonical', 'alias')", name="ck_repository_slug_kind"
        ),
        sa.CheckConstraint(
            "kind != 'alias' OR protected",
            name="ck_repository_slug_alias_protected",
        ),
    )
    op.create_index(
        "ix_repository_slugs_source_operation_id",
        "repository_slugs",
        ["source_operation_id"],
    )
    op.create_index(
        "ix_repository_slugs_repo_kind",
        "repository_slugs",
        ["repo_id", "kind"],
    )
    op.create_index(
        "uq_repository_slugs_one_canonical",
        "repository_slugs",
        ["repo_id"],
        unique=True,
        postgresql_where=sa.text("kind = 'canonical'"),
    )

    # Explicitly mark every existing repository unverified. Do not parse or
    # infer Forge coordinates from current mutable fields.
    op.execute(
        sa.text(
            """
            INSERT INTO repository_forge_identities
                (id, repo_id, verification_state, verification_evidence,
                 created_at, updated_at)
            SELECT gen_random_uuid(), id, 'unverified', '{}'::jsonb, now(), now()
            FROM managed_repos
            """
        )
    )

    op.execute(
        sa.text(
            """
            CREATE FUNCTION guard_repository_forge_identity_history()
            RETURNS trigger AS $$
            BEGIN
                IF TG_OP = 'DELETE' THEN
                    RAISE EXCEPTION 'repository Forge identity rows are durable';
                END IF;
                IF OLD.verification_state = 'verified' AND ROW(
                    NEW.repo_id,
                    NEW.provider,
                    NEW.forge_instance,
                    NEW.forge_owner,
                    NEW.forge_repository_id,
                    NEW.verification_state,
                    NEW.verified_at,
                    NEW.verified_by,
                    NEW.verification_evidence
                ) IS DISTINCT FROM ROW(
                    OLD.repo_id,
                    OLD.provider,
                    OLD.forge_instance,
                    OLD.forge_owner,
                    OLD.forge_repository_id,
                    OLD.verification_state,
                    OLD.verified_at,
                    OLD.verified_by,
                    OLD.verification_evidence
                ) THEN
                    RAISE EXCEPTION 'verified repository Forge identity is immutable';
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql
            """
        )
    )
    op.execute(
        sa.text(
            """
            CREATE TRIGGER repository_forge_identity_history_guard
            BEFORE UPDATE OR DELETE ON repository_forge_identities
            FOR EACH ROW EXECUTE FUNCTION guard_repository_forge_identity_history()
            """
        )
    )
    op.execute(
        sa.text(
            """
            CREATE FUNCTION guard_repository_slug_history()
            RETURNS trigger AS $$
            BEGIN
                IF TG_OP = 'DELETE' THEN
                    RAISE EXCEPTION 'repository slug rows are durable';
                END IF;
                IF NEW.repo_id IS DISTINCT FROM OLD.repo_id
                   OR NEW.slug IS DISTINCT FROM OLD.slug THEN
                    RAISE EXCEPTION 'repository slug ownership is immutable';
                END IF;
                IF OLD.kind = 'alias' AND ROW(
                    NEW.kind, NEW.protected, NEW.source_operation_id
                ) IS DISTINCT FROM ROW(
                    OLD.kind, OLD.protected, OLD.source_operation_id
                ) THEN
                    RAISE EXCEPTION 'protected repository alias is immutable';
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql
            """
        )
    )
    op.execute(
        sa.text(
            """
            CREATE TRIGGER repository_slug_history_guard
            BEFORE UPDATE OR DELETE ON repository_slugs
            FOR EACH ROW EXECUTE FUNCTION guard_repository_slug_history()
            """
        )
    )
    op.execute(
        sa.text(
            """
            CREATE FUNCTION guard_repository_rename_operation_history()
            RETURNS trigger AS $$
            BEGIN
                IF TG_OP = 'DELETE' THEN
                    RAISE EXCEPTION 'repository rename operation rows are durable';
                END IF;
                IF ROW(
                    NEW.repo_id,
                    NEW.forge_identity_id,
                    NEW.forge_identity_state,
                    NEW.expected_provider,
                    NEW.expected_forge_instance,
                    NEW.expected_forge_owner,
                    NEW.expected_forge_repository_id,
                    NEW.expected_source_commit,
                    NEW.expected_default_branch,
                    NEW.old_slug,
                    NEW.new_slug,
                    NEW.old_coordinates,
                    NEW.new_coordinates,
                    NEW.actor,
                    NEW.created_at
                ) IS DISTINCT FROM ROW(
                    OLD.repo_id,
                    OLD.forge_identity_id,
                    OLD.forge_identity_state,
                    OLD.expected_provider,
                    OLD.expected_forge_instance,
                    OLD.expected_forge_owner,
                    OLD.expected_forge_repository_id,
                    OLD.expected_source_commit,
                    OLD.expected_default_branch,
                    OLD.old_slug,
                    OLD.new_slug,
                    OLD.old_coordinates,
                    OLD.new_coordinates,
                    OLD.actor,
                    OLD.created_at
                ) THEN
                    RAISE EXCEPTION 'repository rename operation assertions are immutable';
                END IF;
                RETURN NEW;
            END;
            $$ LANGUAGE plpgsql
            """
        )
    )
    op.execute(
        sa.text(
            """
            CREATE TRIGGER repository_rename_operation_history_guard
            BEFORE UPDATE OR DELETE ON repository_rename_operations
            FOR EACH ROW EXECUTE FUNCTION guard_repository_rename_operation_history()
            """
        )
    )
    op.execute(
        sa.text(
            """
            INSERT INTO repository_slugs
                (id, repo_id, slug, kind, protected, created_at, updated_at)
            SELECT gen_random_uuid(), id, slug, 'canonical', true, now(), now()
            FROM managed_repos
            """
        )
    )


def downgrade() -> None:
    # These tables are additive. Dropping them restores the prior schema and
    # cannot rewrite or delete any pre-existing work or telemetry row.
    op.drop_table("repository_slugs")
    op.drop_table("repository_rename_operations")
    op.drop_table("repository_forge_identities")
    op.execute(sa.text("DROP FUNCTION guard_repository_slug_history()"))
    op.execute(sa.text("DROP FUNCTION guard_repository_rename_operation_history()"))
    op.execute(sa.text("DROP FUNCTION guard_repository_forge_identity_history()"))
