"""add risk waiver decision metadata and separation of duties state

Revision ID: 20260716_0014
Revises: 20260716_0013
Create Date: 2026-07-16 19:10:00
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "20260716_0014"
down_revision: str | None = "20260716_0013"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def _require_postgresql() -> None:
    dialect_name = op.get_bind().dialect.name
    if dialect_name != "postgresql":
        raise RuntimeError(
            "20260716_0014 only supports PostgreSQL; "
            f"refusing to mutate {dialect_name} without transactional constraint DDL"
        )


def _require_no_waiver_audit_data_for_downgrade() -> None:
    bind = op.get_bind()
    disposition_count = int(
        bind.scalar(
            sa.text(
                "SELECT COUNT(*) FROM risk_dispositions "
                "WHERE waiver_requester_id IS NOT NULL "
                "OR waiver_decision IS NOT NULL "
                "OR lifecycle_status IN ('waived', 'waiver_rejected')"
            )
        )
        or 0
    )
    event_count = int(
        bind.scalar(
            sa.text(
                "SELECT COUNT(*) FROM risk_disposition_events "
                "WHERE action IN ('approve_waiver', 'reject_waiver')"
            )
        )
        or 0
    )
    if disposition_count or event_count:
        raise RuntimeError(
            "cannot downgrade risk waiver decisions: immutable waiver audit data exists "
            f"({disposition_count} dispositions, {event_count} decision events)"
        )


def upgrade() -> None:
    _require_postgresql()
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_requester_id", sa.String(length=120), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_requester_name", sa.String(length=120), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_requested_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_reason", sa.Text(), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_scope", sa.Text(), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_expires_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column(
            "waiver_conditions_json",
            sa.JSON(),
            nullable=False,
            server_default=sa.text("'[]'::json"),
        ),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_decision", sa.String(length=20), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_decider_id", sa.String(length=120), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_decider_name", sa.String(length=120), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_decided_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.add_column(
        "risk_dispositions",
        sa.Column("waiver_decision_reason", sa.Text(), nullable=True),
    )

    op.drop_constraint(
        "ck_risk_dispositions_lifecycle",
        "risk_dispositions",
        type_="check",
    )
    op.create_check_constraint(
        "ck_risk_dispositions_lifecycle",
        "risk_dispositions",
        "lifecycle_status IN ('open', 'supplement_requested', "
        "'remediation_in_progress', 'waiver_requested', 'waived', "
        "'waiver_rejected', 'resolved')",
    )
    op.create_check_constraint(
        "ck_risk_dispositions_waiver_request",
        "risk_dispositions",
        "(waiver_requester_id IS NULL AND waiver_requester_name IS NULL "
        "AND waiver_requested_at IS NULL AND waiver_reason IS NULL "
        "AND waiver_scope IS NULL AND waiver_expires_at IS NULL) OR "
        "(waiver_requester_id IS NOT NULL "
        "AND length(trim(waiver_requester_id)) > 0 "
        "AND waiver_requester_name IS NOT NULL "
        "AND length(trim(waiver_requester_name)) > 0 "
        "AND waiver_requested_at IS NOT NULL "
        "AND waiver_reason IS NOT NULL AND length(trim(waiver_reason)) > 0 "
        "AND waiver_scope IS NOT NULL AND length(trim(waiver_scope)) > 0 "
        "AND waiver_expires_at IS NOT NULL "
        "AND waiver_expires_at > waiver_requested_at)",
    )
    op.create_check_constraint(
        "ck_risk_dispositions_waiver_decision",
        "risk_dispositions",
        "(waiver_decision IS NULL AND waiver_decider_id IS NULL "
        "AND waiver_decider_name IS NULL AND waiver_decided_at IS NULL "
        "AND waiver_decision_reason IS NULL) OR "
        "(waiver_decision IN ('approved', 'rejected') "
        "AND waiver_requester_id IS NOT NULL "
        "AND waiver_decider_id IS NOT NULL "
        "AND length(trim(waiver_decider_id)) > 0 "
        "AND waiver_decider_name IS NOT NULL "
        "AND length(trim(waiver_decider_name)) > 0 "
        "AND waiver_decided_at IS NOT NULL "
        "AND waiver_decision_reason IS NOT NULL "
        "AND length(trim(waiver_decision_reason)) > 0)",
    )
    op.create_check_constraint(
        "ck_risk_dispositions_waiver_lifecycle",
        "risk_dispositions",
        "(lifecycle_status != 'waived' OR waiver_decision = 'approved') AND "
        "(lifecycle_status != 'waiver_rejected' OR waiver_decision = 'rejected')",
    )
    op.create_index(
        "ix_risk_dispositions_tenant_waiver_expiry",
        "risk_dispositions",
        ["tenant_id", "lifecycle_status", "waiver_expires_at"],
        unique=False,
    )

    op.drop_constraint(
        "ck_risk_disposition_events_action",
        "risk_disposition_events",
        type_="check",
    )
    op.create_check_constraint(
        "ck_risk_disposition_events_action",
        "risk_disposition_events",
        "action IN ('confirm', 'false_positive', 'request_supplement', "
        "'start_remediation', 'resolve', 'request_waiver', "
        "'approve_waiver', 'reject_waiver')",
    )


def downgrade() -> None:
    _require_postgresql()
    # 申请与决定元数据已经进入不可变事件快照时，禁止静默丢弃审计链。
    _require_no_waiver_audit_data_for_downgrade()
    op.drop_constraint(
        "ck_risk_disposition_events_action",
        "risk_disposition_events",
        type_="check",
    )
    op.create_check_constraint(
        "ck_risk_disposition_events_action",
        "risk_disposition_events",
        "action IN ('confirm', 'false_positive', 'request_supplement', "
        "'start_remediation', 'resolve', 'request_waiver')",
    )

    op.drop_index(
        "ix_risk_dispositions_tenant_waiver_expiry",
        table_name="risk_dispositions",
    )
    op.drop_constraint(
        "ck_risk_dispositions_waiver_lifecycle",
        "risk_dispositions",
        type_="check",
    )
    op.drop_constraint(
        "ck_risk_dispositions_waiver_decision",
        "risk_dispositions",
        type_="check",
    )
    op.drop_constraint(
        "ck_risk_dispositions_waiver_request",
        "risk_dispositions",
        type_="check",
    )
    op.drop_constraint(
        "ck_risk_dispositions_lifecycle",
        "risk_dispositions",
        type_="check",
    )
    op.create_check_constraint(
        "ck_risk_dispositions_lifecycle",
        "risk_dispositions",
        "lifecycle_status IN ('open', 'supplement_requested', "
        "'remediation_in_progress', 'waiver_requested', 'resolved')",
    )

    for column_name in (
        "waiver_decision_reason",
        "waiver_decided_at",
        "waiver_decider_name",
        "waiver_decider_id",
        "waiver_decision",
        "waiver_conditions_json",
        "waiver_expires_at",
        "waiver_scope",
        "waiver_reason",
        "waiver_requested_at",
        "waiver_requester_name",
        "waiver_requester_id",
    ):
        op.drop_column("risk_dispositions", column_name)
