"""add tenant-safe savings value ledger and append-only audit events

Revision ID: 20260716_0015
Revises: 20260716_0014
Create Date: 2026-07-16 20:10:00
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "20260716_0015"
down_revision: str | None = "20260716_0014"
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_0015 only supports PostgreSQL; "
            f"refusing to mutate {dialect_name} without transactional constraint DDL"
        )


def _require_empty_ledger_for_downgrade() -> None:
    bind = op.get_bind()
    counts = {
        table_name: int(
            bind.scalar(sa.text(f"SELECT COUNT(*) FROM {table_name}")) or 0
        )
        for table_name in (
            "profile_baseline_snapshots",
            "savings_opportunities",
            "savings_realizations",
            "savings_evidence_links",
            "savings_events",
        )
    }
    if any(counts.values()):
        summary = ", ".join(f"{name}={count}" for name, count in counts.items())
        raise RuntimeError(
            "cannot downgrade savings value ledger: immutable value facts exist "
            f"({summary})"
        )


def _json_object_default() -> sa.TextClause:
    return sa.text("'{}'::json")


def _json_array_default() -> sa.TextClause:
    return sa.text("'[]'::json")


def upgrade() -> None:
    _require_postgresql()
    op.create_table(
        "profile_baseline_snapshots",
        sa.Column("id", sa.String(length=36), nullable=False),
        sa.Column("tenant_id", sa.String(length=64), nullable=False),
        sa.Column("baseline_key", sa.String(length=160), nullable=False),
        sa.Column("baseline_type", sa.String(length=32), nullable=False),
        sa.Column("dimension_type", sa.String(length=50), nullable=False),
        sa.Column("dimension_id", sa.String(length=160), nullable=False),
        sa.Column("metric_key", sa.String(length=100), nullable=False),
        sa.Column("unit", sa.String(length=30), nullable=False),
        sa.Column("original_currency", sa.String(length=3), nullable=True),
        sa.Column("baseline_value", sa.Numeric(20, 4), nullable=False),
        sa.Column("window_start", sa.DateTime(timezone=True), nullable=True),
        sa.Column("window_end", sa.DateTime(timezone=True), nullable=True),
        sa.Column("sample_count", sa.Integer(), nullable=False, server_default="0"),
        sa.Column("method", sa.String(length=80), nullable=False),
        sa.Column("query_fingerprint", sa.String(length=80), nullable=False),
        sa.Column("data_quality_status", sa.String(length=20), nullable=False),
        sa.Column("data_quality_score", sa.Numeric(5, 4), nullable=False),
        sa.Column(
            "quality_issues_json",
            sa.JSON(),
            nullable=False,
            server_default=_json_array_default(),
        ),
        sa.Column("algorithm_version", sa.String(length=80), nullable=False),
        sa.Column("policy_version", sa.String(length=120), nullable=True),
        sa.Column("policy_effective_from", sa.Date(), nullable=True),
        sa.Column("policy_effective_to", sa.Date(), nullable=True),
        sa.Column("target_resource_type", sa.String(length=50), nullable=True),
        sa.Column("target_resource_id", sa.String(length=160), nullable=True),
        sa.Column("frozen_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("frozen_by", sa.String(length=120), nullable=False),
        sa.Column("valid_until", sa.DateTime(timezone=True), nullable=True),
        sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.func.now(),
        ),
        sa.CheckConstraint(
            "baseline_type IN ('historical_cohort', 'policy_counterfactual', 'manual')",
            name="ck_profile_baseline_snapshots_type",
        ),
        sa.CheckConstraint(
            "data_quality_status IN ('complete', 'partial', 'insufficient', 'invalid')",
            name="ck_profile_baseline_snapshots_quality_status",
        ),
        sa.CheckConstraint(
            "baseline_value >= 0 AND sample_count >= 0",
            name="ck_profile_baseline_snapshots_values",
        ),
        sa.CheckConstraint(
            "data_quality_score >= 0 AND data_quality_score <= 1",
            name="ck_profile_baseline_snapshots_quality_score",
        ),
        sa.CheckConstraint(
            "window_end IS NULL OR window_start IS NOT NULL",
            name="ck_profile_baseline_snapshots_window_pair",
        ),
        sa.CheckConstraint(
            "window_start IS NULL OR window_end IS NULL OR window_end >= window_start",
            name="ck_profile_baseline_snapshots_window_order",
        ),
        sa.CheckConstraint(
            "baseline_type != 'historical_cohort' OR "
            "(window_start IS NOT NULL AND window_end IS NOT NULL AND sample_count > 0)",
            name="ck_profile_baseline_snapshots_historical_shape",
        ),
        sa.CheckConstraint(
            "baseline_type != 'policy_counterfactual' OR "
            "(policy_version IS NOT NULL AND length(trim(policy_version)) > 0 "
            "AND policy_effective_from IS NOT NULL AND target_resource_type IS NOT NULL "
            "AND target_resource_id IS NOT NULL)",
            name="ck_profile_baseline_snapshots_policy_shape",
        ),
        sa.CheckConstraint(
            "policy_effective_to IS NULL OR policy_effective_from IS NOT NULL",
            name="ck_profile_baseline_snapshots_policy_pair",
        ),
        sa.CheckConstraint(
            "policy_effective_from IS NULL OR policy_effective_to IS NULL "
            "OR policy_effective_to >= policy_effective_from",
            name="ck_profile_baseline_snapshots_policy_order",
        ),
        sa.CheckConstraint(
            "valid_until IS NULL OR valid_until >= frozen_at",
            name="ck_profile_baseline_snapshots_validity",
        ),
        sa.CheckConstraint(
            "length(trim(baseline_key)) > 0 AND length(trim(query_fingerprint)) > 0",
            name="ck_profile_baseline_snapshots_keys",
        ),
        sa.CheckConstraint("version >= 1", name="ck_profile_baseline_snapshots_version"),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint(
            "tenant_id", "id", name="uq_profile_baseline_snapshots_tenant_id"
        ),
        sa.UniqueConstraint(
            "tenant_id",
            "baseline_key",
            name="uq_profile_baseline_snapshots_tenant_key",
        ),
    )
    op.create_index(
        "ix_profile_baseline_snapshots_lookup",
        "profile_baseline_snapshots",
        [
            "tenant_id",
            "baseline_type",
            "dimension_type",
            "dimension_id",
            "metric_key",
            "frozen_at",
        ],
    )
    op.create_index(
        "ix_profile_baseline_snapshots_quality",
        "profile_baseline_snapshots",
        ["tenant_id", "data_quality_status", "frozen_at"],
    )
    op.create_table(
        "savings_opportunities",
        sa.Column("id", sa.String(length=36), nullable=False),
        sa.Column("tenant_id", sa.String(length=64), nullable=False),
        sa.Column("opportunity_key", sa.String(length=180), nullable=False),
        sa.Column("benefit_key", sa.String(length=180), nullable=False),
        sa.Column("expense_case_id", sa.String(length=36), nullable=False),
        # expense_claims/items 仍由 legacy bootstrap 管理，仅保存已校验软引用。
        sa.Column("claim_id", sa.String(length=36), nullable=False),
        sa.Column("claim_no_snapshot", sa.String(length=80), nullable=False),
        sa.Column("claim_item_id", sa.String(length=36), nullable=True),
        sa.Column("discovery_business_event_id", sa.String(length=36), nullable=True),
        sa.Column("source_type", sa.String(length=50), nullable=False),
        sa.Column("source_id", sa.String(length=160), nullable=False),
        sa.Column("category", sa.String(length=60), nullable=False),
        sa.Column("value_kind", sa.String(length=20), nullable=False),
        sa.Column("title", sa.String(length=200), nullable=False),
        sa.Column("description", sa.Text(), nullable=False),
        sa.Column("exposure_amount", sa.Numeric(20, 4), nullable=False),
        sa.Column("baseline_snapshot_id", sa.String(length=36), nullable=False),
        sa.Column("baseline_amount", sa.Numeric(20, 4), nullable=False),
        sa.Column("target_amount", sa.Numeric(20, 4), nullable=False),
        sa.Column("estimated_gross", sa.Numeric(20, 4), nullable=False),
        sa.Column("estimated_cost", sa.Numeric(20, 4), nullable=False),
        sa.Column("estimated_net", sa.Numeric(20, 4), nullable=False),
        sa.Column("estimated_low", sa.Numeric(20, 4), nullable=False),
        sa.Column("estimated_high", sa.Numeric(20, 4), nullable=False),
        sa.Column("confidence", sa.Numeric(5, 4), nullable=False),
        sa.Column("currency", sa.String(length=3), nullable=False),
        sa.Column("reporting_currency", sa.String(length=3), nullable=False),
        sa.Column("attribution_method", sa.String(length=60), nullable=False),
        sa.Column("ai_decision_id", sa.String(length=36), nullable=True),
        sa.Column("suggested_action", sa.Text(), nullable=False),
        sa.Column("owner_id", sa.String(length=120), nullable=False),
        sa.Column("owner_name", sa.String(length=120), nullable=False),
        sa.Column("owner_role", sa.String(length=60), nullable=False),
        sa.Column("due_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("status", sa.String(length=24), nullable=False, server_default="identified"),
        sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
        sa.Column(
            "dimension_json", sa.JSON(), nullable=False, server_default=_json_object_default()
        ),
        sa.Column(
            "baseline_snapshot_json",
            sa.JSON(),
            nullable=False,
            server_default=_json_object_default(),
        ),
        sa.Column(
            "evidence_json", sa.JSON(), nullable=False, server_default=_json_array_default()
        ),
        sa.Column("accepted_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("realized_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column(
            "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.Column(
            "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.CheckConstraint(
            "value_kind IN ('cash', 'labor')",
            name="ck_savings_opportunities_value_kind",
        ),
        sa.CheckConstraint(
            "status IN ('identified', 'accepted', 'in_progress', 'realized', "
            "'verified', 'reversed', 'rejected', 'expired')",
            name="ck_savings_opportunities_status",
        ),
        sa.CheckConstraint(
            "exposure_amount >= 0 AND baseline_amount >= 0 AND target_amount >= 0 "
            "AND estimated_gross >= 0 AND estimated_cost >= 0 AND estimated_net >= 0 "
            "AND estimated_low >= 0 AND estimated_high >= 0",
            name="ck_savings_opportunities_amounts",
        ),
        sa.CheckConstraint(
            "estimated_net = estimated_gross - estimated_cost",
            name="ck_savings_opportunities_net_math",
        ),
        sa.CheckConstraint(
            "estimated_low <= estimated_net AND estimated_net <= estimated_high",
            name="ck_savings_opportunities_interval",
        ),
        sa.CheckConstraint(
            "confidence >= 0 AND confidence <= 1",
            name="ck_savings_opportunities_confidence",
        ),
        sa.CheckConstraint("version >= 1", name="ck_savings_opportunities_version"),
        sa.CheckConstraint(
            "length(trim(benefit_key)) > 0 AND length(trim(opportunity_key)) > 0",
            name="ck_savings_opportunities_keys",
        ),
        sa.CheckConstraint(
            "length(trim(currency)) = 3 AND length(trim(reporting_currency)) = 3",
            name="ck_savings_opportunities_currencies",
        ),
        sa.CheckConstraint(
            "status NOT IN ('accepted', 'in_progress', 'realized', 'verified', 'reversed') "
            "OR accepted_at IS NOT NULL",
            name="ck_savings_opportunities_acceptance",
        ),
        sa.CheckConstraint(
            "status NOT IN ('in_progress', 'realized', 'verified', 'reversed') "
            "OR started_at IS NOT NULL",
            name="ck_savings_opportunities_started",
        ),
        sa.CheckConstraint(
            "status NOT IN ('realized', 'verified', 'reversed') OR realized_at IS NOT NULL",
            name="ck_savings_opportunities_realized",
        ),
        sa.CheckConstraint(
            "status NOT IN ('verified', 'reversed') OR verified_at IS NOT NULL",
            name="ck_savings_opportunities_verified",
        ),
        sa.CheckConstraint(
            "status NOT IN ('verified', 'reversed', 'rejected', 'expired') "
            "OR closed_at IS NOT NULL",
            name="ck_savings_opportunities_closed",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "expense_case_id"],
            ["expense_cases.tenant_id", "expense_cases.id"],
            name="fk_savings_opportunities_tenant_case",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "expense_case_id", "discovery_business_event_id"],
            [
                "business_events.tenant_id",
                "business_events.expense_case_id",
                "business_events.id",
            ],
            name="fk_savings_opportunities_tenant_event",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "baseline_snapshot_id"],
            ["profile_baseline_snapshots.tenant_id", "profile_baseline_snapshots.id"],
            name="fk_savings_opportunities_tenant_baseline",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "expense_case_id", "ai_decision_id"],
            ["ai_decisions.tenant_id", "ai_decisions.expense_case_id", "ai_decisions.id"],
            name="fk_savings_opportunities_tenant_ai_decision",
            ondelete="RESTRICT",
        ),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("tenant_id", "id", name="uq_savings_opportunities_tenant_id"),
        sa.UniqueConstraint(
            "tenant_id", "opportunity_key", name="uq_savings_opportunities_tenant_key"
        ),
    )
    op.create_index(
        "ix_savings_opportunities_tenant_status_due",
        "savings_opportunities",
        ["tenant_id", "status", "due_at"],
    )
    op.create_index(
        "ix_savings_opportunities_tenant_case",
        "savings_opportunities",
        ["tenant_id", "expense_case_id", "created_at"],
    )
    op.create_index(
        "ix_savings_opportunities_tenant_benefit",
        "savings_opportunities",
        ["tenant_id", "benefit_key"],
    )
    op.create_index(
        "ix_savings_opportunities_tenant_owner",
        "savings_opportunities",
        ["tenant_id", "owner_id", "status"],
    )
    op.create_table(
        "savings_realizations",
        sa.Column("id", sa.String(length=36), nullable=False),
        sa.Column("tenant_id", sa.String(length=64), nullable=False),
        sa.Column("realization_key", sa.String(length=180), nullable=False),
        sa.Column("opportunity_id", sa.String(length=36), nullable=False),
        sa.Column("expense_case_id", sa.String(length=36), nullable=False),
        sa.Column("claim_id", sa.String(length=36), nullable=False),
        sa.Column("claim_item_id", sa.String(length=36), nullable=True),
        sa.Column("business_event_id", sa.String(length=36), nullable=True),
        sa.Column("realization_type", sa.String(length=20), nullable=False),
        sa.Column("reversal_of_realization_id", sa.String(length=36), nullable=True),
        sa.Column("realized_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("recorded_by_id", sa.String(length=120), nullable=False),
        sa.Column("recorded_by_name", sa.String(length=120), nullable=False),
        sa.Column("actual_gross", sa.Numeric(20, 4), nullable=False),
        sa.Column("incremental_cost", sa.Numeric(20, 4), nullable=False),
        sa.Column("actual_net", sa.Numeric(20, 4), nullable=False),
        sa.Column("original_currency", sa.String(length=3), nullable=False),
        sa.Column("reporting_amount", sa.Numeric(20, 4), nullable=False),
        sa.Column("reporting_currency", sa.String(length=3), nullable=False),
        sa.Column("fx_rate", sa.Numeric(20, 8), nullable=False),
        sa.Column("fx_source", sa.String(length=80), nullable=False),
        sa.Column("fx_date", sa.Date(), nullable=False),
        sa.Column("fx_version", sa.String(length=80), nullable=False),
        sa.Column("attribution_method", sa.String(length=60), nullable=False),
        sa.Column("attribution_ratio", sa.Numeric(7, 6), nullable=False),
        sa.Column("benefit_key", sa.String(length=180), nullable=False),
        sa.Column(
            "dedupe_status",
            sa.String(length=24),
            nullable=False,
            server_default="pending_review",
        ),
        sa.Column("canonical_realization_id", sa.String(length=36), nullable=True),
        sa.Column(
            "status",
            sa.String(length=24),
            nullable=False,
            server_default="pending_confirmation",
        ),
        sa.Column("finance_confirmer_id", sa.String(length=120), nullable=True),
        sa.Column("finance_confirmer_name", sa.String(length=120), nullable=True),
        sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("confirmation_note", sa.Text(), nullable=True),
        sa.Column("rejected_by_id", sa.String(length=120), nullable=True),
        sa.Column("rejected_by_name", sa.String(length=120), nullable=True),
        sa.Column("rejected_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("rejection_reason", sa.Text(), nullable=True),
        sa.Column("reversed_by_id", sa.String(length=120), nullable=True),
        sa.Column("reversed_by_name", sa.String(length=120), nullable=True),
        sa.Column("reversed_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("reversal_reason", sa.Text(), nullable=True),
        sa.Column(
            "baseline_snapshot_json",
            sa.JSON(),
            nullable=False,
            server_default=_json_object_default(),
        ),
        sa.Column(
            "final_snapshot_json",
            sa.JSON(),
            nullable=False,
            server_default=_json_object_default(),
        ),
        sa.Column(
            "evidence_json", sa.JSON(), nullable=False, server_default=_json_array_default()
        ),
        sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
        sa.Column(
            "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.Column(
            "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.CheckConstraint(
            "realization_type IN ('actual', 'reversal')",
            name="ck_savings_realizations_type",
        ),
        sa.CheckConstraint(
            "dedupe_status IN ('pending_review', 'canonical', 'duplicate', 'excluded')",
            name="ck_savings_realizations_dedupe_status",
        ),
        sa.CheckConstraint(
            "status IN ('pending_confirmation', 'finance_confirmed', 'rejected', 'reversed')",
            name="ck_savings_realizations_status",
        ),
        sa.CheckConstraint(
            "attribution_ratio > 0 AND attribution_ratio <= 1",
            name="ck_savings_realizations_attribution",
        ),
        sa.CheckConstraint(
            "incremental_cost >= 0 AND fx_rate > 0",
            name="ck_savings_realizations_cost_fx",
        ),
        sa.CheckConstraint(
            "actual_net = actual_gross - incremental_cost",
            name="ck_savings_realizations_net_math",
        ),
        sa.CheckConstraint(
            "(realization_type = 'actual' AND reversal_of_realization_id IS NULL "
            "AND actual_gross >= 0 AND actual_net >= 0 AND reporting_amount >= 0) OR "
            "(realization_type = 'reversal' AND reversal_of_realization_id IS NOT NULL "
            "AND actual_gross <= 0 AND actual_net <= 0 AND reporting_amount <= 0)",
            name="ck_savings_realizations_amount_direction",
        ),
        sa.CheckConstraint(
            "(dedupe_status = 'duplicate' AND canonical_realization_id IS NOT NULL "
            "AND canonical_realization_id <> id) OR "
            "(dedupe_status != 'duplicate' AND canonical_realization_id IS NULL)",
            name="ck_savings_realizations_duplicate_target",
        ),
        sa.CheckConstraint(
            "status != 'finance_confirmed' OR "
            "(finance_confirmer_id IS NOT NULL AND finance_confirmer_name IS NOT NULL "
            "AND confirmed_at IS NOT NULL AND confirmation_note IS NOT NULL "
            "AND (realization_type = 'reversal' OR finance_confirmer_id <> recorded_by_id) "
            "AND dedupe_status = 'canonical')",
            name="ck_savings_realizations_confirmation",
        ),
        sa.CheckConstraint(
            "status != 'rejected' OR (rejected_by_id IS NOT NULL "
            "AND rejected_by_name IS NOT NULL AND rejected_at IS NOT NULL "
            "AND rejection_reason IS NOT NULL)",
            name="ck_savings_realizations_rejection",
        ),
        sa.CheckConstraint(
            "status != 'reversed' OR (reversed_by_id IS NOT NULL "
            "AND reversed_by_name IS NOT NULL AND reversed_at IS NOT NULL "
            "AND reversal_reason IS NOT NULL)",
            name="ck_savings_realizations_reversal",
        ),
        sa.CheckConstraint("version >= 1", name="ck_savings_realizations_version"),
        sa.CheckConstraint(
            "length(trim(realization_key)) > 0 AND length(trim(benefit_key)) > 0",
            name="ck_savings_realizations_keys",
        ),
        sa.CheckConstraint(
            "length(trim(original_currency)) = 3 "
            "AND length(trim(reporting_currency)) = 3",
            name="ck_savings_realizations_currencies",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "opportunity_id"],
            ["savings_opportunities.tenant_id", "savings_opportunities.id"],
            name="fk_savings_realizations_tenant_opportunity",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "expense_case_id"],
            ["expense_cases.tenant_id", "expense_cases.id"],
            name="fk_savings_realizations_tenant_case",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "expense_case_id", "business_event_id"],
            [
                "business_events.tenant_id",
                "business_events.expense_case_id",
                "business_events.id",
            ],
            name="fk_savings_realizations_tenant_event",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            [
                "tenant_id", "opportunity_id", "benefit_key", "reversal_of_realization_id"
            ],
            [
                "savings_realizations.tenant_id", "savings_realizations.opportunity_id",
                "savings_realizations.benefit_key", "savings_realizations.id",
            ],
            name="fk_savings_realizations_tenant_reversal",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "benefit_key", "canonical_realization_id"],
            [
                "savings_realizations.tenant_id", "savings_realizations.benefit_key",
                "savings_realizations.id",
            ],
            name="fk_savings_realizations_tenant_canonical",
            ondelete="RESTRICT",
        ),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("tenant_id", "id", name="uq_savings_realizations_tenant_id"),
        sa.UniqueConstraint(
            "tenant_id", "realization_key", name="uq_savings_realizations_tenant_key"
        ),
        sa.UniqueConstraint(
            "tenant_id", "opportunity_id", "benefit_key", "id",
            name="uq_savings_realizations_tenant_opportunity_benefit_id",
        ),
        sa.UniqueConstraint(
            "tenant_id", "benefit_key", "id",
            name="uq_savings_realizations_tenant_benefit_id",
        ),
    )
    op.create_index(
        "uq_savings_realizations_actual_canonical_benefit",
        "savings_realizations",
        ["tenant_id", "benefit_key"],
        unique=True,
        postgresql_where=sa.text(
            "realization_type = 'actual' AND dedupe_status = 'canonical'"
        ),
    )
    op.create_index(
        "ix_savings_realizations_tenant_status_time",
        "savings_realizations",
        ["tenant_id", "status", "realized_at"],
    )
    op.create_index(
        "ix_savings_realizations_tenant_opportunity",
        "savings_realizations",
        ["tenant_id", "opportunity_id", "realized_at"],
    )
    op.create_index(
        "ix_savings_realizations_tenant_benefit",
        "savings_realizations",
        ["tenant_id", "benefit_key", "dedupe_status"],
    )
    op.create_table(
        "savings_evidence_links",
        sa.Column("id", sa.String(length=36), nullable=False),
        sa.Column("tenant_id", sa.String(length=64), nullable=False),
        sa.Column("evidence_key", sa.String(length=180), nullable=False),
        sa.Column("entity_type", sa.String(length=20), nullable=False),
        sa.Column("entity_id", sa.String(length=36), nullable=False),
        sa.Column("baseline_snapshot_id", sa.String(length=36), nullable=True),
        sa.Column("opportunity_id", sa.String(length=36), nullable=True),
        sa.Column("realization_id", sa.String(length=36), nullable=True),
        sa.Column("evidence_role", sa.String(length=50), nullable=False),
        sa.Column("resource_type", sa.String(length=50), nullable=False),
        sa.Column("resource_id", sa.String(length=160), nullable=False),
        sa.Column("source_system", sa.String(length=80), nullable=False),
        sa.Column("external_event_id", sa.String(length=160), nullable=True),
        sa.Column("content_hash", sa.String(length=80), nullable=False),
        sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("collected_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column(
            "verification_status",
            sa.String(length=20),
            nullable=False,
            server_default="unverified",
        ),
        sa.Column("verified_by", sa.String(length=120), nullable=True),
        sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column(
            "metadata_json", sa.JSON(), nullable=False, server_default=_json_object_default()
        ),
        sa.Column(
            "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.CheckConstraint(
            "entity_type IN ('baseline', 'opportunity', 'realization')",
            name="ck_savings_evidence_links_entity_type",
        ),
        sa.CheckConstraint(
            "(entity_type = 'baseline' AND baseline_snapshot_id = entity_id "
            "AND opportunity_id IS NULL AND realization_id IS NULL) OR "
            "(entity_type = 'opportunity' AND opportunity_id = entity_id "
            "AND baseline_snapshot_id IS NULL AND realization_id IS NULL) OR "
            "(entity_type = 'realization' AND realization_id = entity_id "
            "AND baseline_snapshot_id IS NULL AND opportunity_id IS NULL)",
            name="ck_savings_evidence_links_entity_shape",
        ),
        sa.CheckConstraint(
            "verification_status IN ('unverified', 'verified', 'rejected', 'unavailable')",
            name="ck_savings_evidence_links_verification",
        ),
        sa.CheckConstraint(
            "verification_status != 'verified' OR "
            "(verified_by IS NOT NULL AND verified_at IS NOT NULL)",
            name="ck_savings_evidence_links_verifier",
        ),
        sa.CheckConstraint(
            "length(trim(evidence_key)) > 0 AND length(trim(content_hash)) > 0",
            name="ck_savings_evidence_links_keys",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "baseline_snapshot_id"],
            ["profile_baseline_snapshots.tenant_id", "profile_baseline_snapshots.id"],
            name="fk_savings_evidence_links_tenant_baseline",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "opportunity_id"],
            ["savings_opportunities.tenant_id", "savings_opportunities.id"],
            name="fk_savings_evidence_links_tenant_opportunity",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "realization_id"],
            ["savings_realizations.tenant_id", "savings_realizations.id"],
            name="fk_savings_evidence_links_tenant_realization",
            ondelete="RESTRICT",
        ),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("tenant_id", "id", name="uq_savings_evidence_links_tenant_id"),
        sa.UniqueConstraint(
            "tenant_id", "evidence_key", name="uq_savings_evidence_links_tenant_key"
        ),
    )
    op.create_index(
        "ix_savings_evidence_links_entity",
        "savings_evidence_links",
        ["tenant_id", "entity_type", "entity_id", "collected_at"],
    )
    op.create_index(
        "ix_savings_evidence_links_resource",
        "savings_evidence_links",
        ["tenant_id", "resource_type", "resource_id"],
    )
    op.create_table(
        "savings_events",
        sa.Column("id", sa.String(length=36), nullable=False),
        sa.Column("tenant_id", sa.String(length=64), nullable=False),
        sa.Column("aggregate_type", sa.String(length=20), nullable=False),
        sa.Column("aggregate_id", sa.String(length=36), nullable=False),
        sa.Column("baseline_snapshot_id", sa.String(length=36), nullable=True),
        sa.Column("opportunity_id", sa.String(length=36), nullable=True),
        sa.Column("realization_id", sa.String(length=36), nullable=True),
        sa.Column("action", sa.String(length=60), nullable=False),
        sa.Column("actor_id", sa.String(length=120), nullable=False),
        sa.Column("actor_name", sa.String(length=120), nullable=False),
        sa.Column("actor_type", sa.String(length=20), nullable=False),
        sa.Column("request_id", sa.String(length=120), nullable=False),
        sa.Column("expected_version", sa.Integer(), nullable=False),
        sa.Column("result_version", sa.Integer(), nullable=False),
        sa.Column("payload_fingerprint", sa.String(length=80), nullable=False),
        sa.Column(
            "payload_json", sa.JSON(), nullable=False, server_default=_json_object_default()
        ),
        sa.Column("before_json", sa.JSON(), nullable=False, server_default=_json_object_default()),
        sa.Column("after_json", sa.JSON(), nullable=False, server_default=_json_object_default()),
        sa.Column(
            "response_json", sa.JSON(), nullable=False, server_default=_json_object_default()
        ),
        sa.Column("correlation_id", sa.String(length=64), nullable=True),
        sa.Column("causation_id", sa.String(length=64), nullable=True),
        sa.Column(
            "occurred_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
        ),
        sa.CheckConstraint(
            "aggregate_type IN ('baseline', 'opportunity', 'realization')",
            name="ck_savings_events_aggregate_type",
        ),
        sa.CheckConstraint(
            "(aggregate_type = 'baseline' AND baseline_snapshot_id = aggregate_id "
            "AND opportunity_id IS NULL AND realization_id IS NULL) OR "
            "(aggregate_type = 'opportunity' AND opportunity_id = aggregate_id "
            "AND baseline_snapshot_id IS NULL AND realization_id IS NULL) OR "
            "(aggregate_type = 'realization' AND realization_id = aggregate_id "
            "AND baseline_snapshot_id IS NULL AND opportunity_id IS NULL)",
            name="ck_savings_events_aggregate_shape",
        ),
        sa.CheckConstraint("length(trim(action)) > 0", name="ck_savings_events_action"),
        sa.CheckConstraint(
            "actor_type IN ('user', 'system', 'agent', 'service')",
            name="ck_savings_events_actor_type",
        ),
        sa.CheckConstraint(
            "expected_version >= 0 AND result_version >= 1 "
            "AND result_version >= expected_version",
            name="ck_savings_events_version",
        ),
        sa.CheckConstraint(
            "length(trim(request_id)) > 0 AND length(trim(payload_fingerprint)) > 0",
            name="ck_savings_events_request",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "baseline_snapshot_id"],
            ["profile_baseline_snapshots.tenant_id", "profile_baseline_snapshots.id"],
            name="fk_savings_events_tenant_baseline",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "opportunity_id"],
            ["savings_opportunities.tenant_id", "savings_opportunities.id"],
            name="fk_savings_events_tenant_opportunity",
            ondelete="RESTRICT",
        ),
        sa.ForeignKeyConstraint(
            ["tenant_id", "realization_id"],
            ["savings_realizations.tenant_id", "savings_realizations.id"],
            name="fk_savings_events_tenant_realization",
            ondelete="RESTRICT",
        ),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("tenant_id", "id", name="uq_savings_events_tenant_id"),
        sa.UniqueConstraint(
            "tenant_id", "actor_id", "request_id", name="uq_savings_events_actor_request"
        ),
        sa.UniqueConstraint(
            "tenant_id",
            "aggregate_type",
            "aggregate_id",
            "result_version",
            name="uq_savings_events_aggregate_version",
        ),
    )
    op.create_index(
        "ix_savings_events_tenant_aggregate_time",
        "savings_events",
        ["tenant_id", "aggregate_type", "aggregate_id", "occurred_at"],
    )
    op.create_index(
        "ix_savings_events_tenant_correlation",
        "savings_events",
        ["tenant_id", "correlation_id", "occurred_at"],
    )
    op.execute(
        """
        CREATE OR REPLACE FUNCTION prevent_savings_events_mutation()
        RETURNS trigger AS $$
        BEGIN
            RAISE EXCEPTION 'savings_events are append-only';
        END;
        $$ LANGUAGE plpgsql
        """
    )
    op.execute(
        """
        CREATE TRIGGER trg_savings_events_append_only
        BEFORE UPDATE OR DELETE ON savings_events
        FOR EACH ROW EXECUTE FUNCTION prevent_savings_events_mutation()
        """
    )


def downgrade() -> None:
    _require_postgresql()
    _require_empty_ledger_for_downgrade()
    op.execute("DROP TRIGGER IF EXISTS trg_savings_events_append_only ON savings_events")
    op.execute("DROP FUNCTION IF EXISTS prevent_savings_events_mutation()")
    op.drop_table("savings_events")
    op.drop_table("savings_evidence_links")
    op.drop_table("savings_realizations")
    op.drop_table("savings_opportunities")
    op.drop_table("profile_baseline_snapshots")
