"""repair tenant identity lookup indexes

Revision ID: 20260718_0029
Revises: 20260717_0028
Create Date: 2026-07-18
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "20260718_0029"
down_revision: str | None = "20260717_0028"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


class _TargetIndex:
    __slots__ = (
        "table_name",
        "column_name",
        "index_name",
        "tenant_unique_constraint",
    )

    def __init__(
        self,
        *,
        table_name: str,
        column_name: str,
        index_name: str,
        tenant_unique_constraint: str,
    ) -> None:
        self.table_name = table_name
        self.column_name = column_name
        self.index_name = index_name
        self.tenant_unique_constraint = tenant_unique_constraint


_TARGET_INDEXES = (
    _TargetIndex(
        table_name="organization_units",
        column_name="unit_code",
        index_name="ix_organization_units_unit_code",
        tenant_unique_constraint="uq_organization_units_tenant_code",
    ),
    _TargetIndex(
        table_name="employees",
        column_name="employee_no",
        index_name="ix_employees_employee_no",
        tenant_unique_constraint="uq_employees_tenant_employee_no",
    ),
    _TargetIndex(
        table_name="employees",
        column_name="email",
        index_name="ix_employees_email",
        tenant_unique_constraint="uq_employees_tenant_email",
    ),
)


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


def _inspector() -> sa.Inspector:
    return sa.inspect(op.get_bind())


def _assert_tenant_unique_constraint(target: _TargetIndex) -> None:
    constraints = {
        str(item.get("name") or ""): tuple(item.get("column_names") or ())
        for item in _inspector().get_unique_constraints(target.table_name)
    }
    expected_columns = ("tenant_id", target.column_name)
    actual_columns = constraints.get(target.tenant_unique_constraint)
    if actual_columns != expected_columns:
        raise RuntimeError(
            "cannot repair tenant identity lookup indexes: "
            f"{target.tenant_unique_constraint} must cover {expected_columns}, "
            f"found {actual_columns!r}"
        )


def _validated_present_targets() -> tuple[_TargetIndex, ...]:
    inspector = _inspector()
    present_targets: list[_TargetIndex] = []
    for target in _TARGET_INDEXES:
        if not inspector.has_table(target.table_name):
            continue
        columns = {
            str(item.get("name") or ""): item
            for item in inspector.get_columns(target.table_name)
        }
        required_columns = {"tenant_id", target.column_name}
        missing_columns = required_columns - columns.keys()
        if missing_columns:
            raise RuntimeError(
                "cannot repair tenant identity lookup indexes: "
                f"{target.table_name} is missing columns "
                f"{', '.join(sorted(missing_columns))}"
            )
        if bool(columns["tenant_id"].get("nullable", True)):
            raise RuntimeError(
                "cannot repair tenant identity lookup indexes: "
                f"{target.table_name}.tenant_id must be non-nullable"
            )
        _assert_tenant_unique_constraint(target)
        present_targets.append(target)
    return tuple(present_targets)


def _index_uniqueness(target: _TargetIndex) -> bool | None:
    indexes = [
        item
        for item in _inspector().get_indexes(target.table_name)
        if str(item.get("name") or "") == target.index_name
    ]
    if not indexes:
        return None
    if len(indexes) != 1:
        raise RuntimeError(
            "cannot repair tenant identity lookup indexes: "
            f"found multiple indexes named {target.index_name}"
        )

    index = indexes[0]
    actual_columns = tuple(index.get("column_names") or ())
    if actual_columns != (target.column_name,):
        raise RuntimeError(
            "cannot repair tenant identity lookup indexes: "
            f"{target.index_name} must cover only {target.column_name}, "
            f"found {actual_columns!r}"
        )
    if index.get("duplicates_constraint"):
        raise RuntimeError(
            "cannot repair tenant identity lookup indexes: "
            f"{target.index_name} backs a constraint and is not safe to replace"
        )

    dialect_options = dict(index.get("dialect_options") or {})
    predicate = dialect_options.get("postgresql_where")
    index_method = dialect_options.get("postgresql_using")
    operator_classes = dialect_options.get("postgresql_ops") or {}
    included_columns = (
        index.get("include_columns")
        or dialect_options.get("postgresql_include")
        or ()
    )
    column_sorting = index.get("column_sorting") or {}
    if (
        predicate is not None
        or included_columns
        or index_method not in (None, "btree")
        or operator_classes
        or column_sorting
    ):
        raise RuntimeError(
            "cannot repair tenant identity lookup indexes: "
            f"{target.index_name} is not a plain single-column btree index"
        )
    return bool(index.get("unique", False))


def _replace_index(target: _TargetIndex, *, unique: bool) -> None:
    current_uniqueness = _index_uniqueness(target)
    if current_uniqueness == unique:
        return
    if current_uniqueness is not None:
        op.drop_index(target.index_name, table_name=target.table_name)
    op.create_index(
        target.index_name,
        target.table_name,
        [target.column_name],
        unique=unique,
    )
    if _index_uniqueness(target) != unique:
        raise RuntimeError(
            "tenant identity lookup index replacement did not reach the expected state: "
            f"{target.index_name} unique={unique}"
        )


def _cross_tenant_duplicate_count(target: _TargetIndex) -> int:
    table = sa.table(
        target.table_name,
        sa.column("tenant_id"),
        sa.column(target.column_name),
    )
    value_column = table.c[target.column_name]
    duplicate_values = (
        sa.select(value_column)
        .where(value_column.is_not(None))
        .group_by(value_column)
        .having(sa.func.count(sa.distinct(table.c.tenant_id)) > 1)
        .subquery()
    )
    return int(
        op.get_bind().scalar(
            sa.select(sa.func.count()).select_from(duplicate_values)
        )
        or 0
    )


def upgrade() -> None:
    _require_postgresql()
    targets = _validated_present_targets()
    # 先校验所有旧索引形态，防止修到一半才发现同名索引承载了其他用途。
    for target in targets:
        _index_uniqueness(target)
    for target in targets:
        _replace_index(target, unique=False)
    for target in targets:
        _assert_tenant_unique_constraint(target)


def downgrade() -> None:
    _require_postgresql()
    targets = _validated_present_targets()
    # downgrade 会恢复旧的全局唯一索引；必须在任何 DDL 前一次性排除跨租户重复。
    for target in targets:
        _index_uniqueness(target)
    violations = {
        f"{target.table_name}.{target.column_name}": duplicate_count
        for target in targets
        if (duplicate_count := _cross_tenant_duplicate_count(target)) > 0
    }
    if violations:
        details = ", ".join(
            f"{name}={count}" for name, count in sorted(violations.items())
        )
        raise RuntimeError(
            "cannot downgrade tenant identity lookup indexes: "
            f"cross-tenant duplicate values exist ({details})"
        )
    for target in targets:
        _replace_index(target, unique=True)
    for target in targets:
        _assert_tenant_unique_constraint(target)
