"""establish trusted tenant identity foundation

Revision ID: 20260717_0025
Revises: 20260717_0024
Create Date: 2026-07-17 10:20:00
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "20260717_0025"
down_revision: str | None = "20260717_0024"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None

_DEFAULT_TENANT = "default"
_TENANT_SOURCE_TABLES = (
    "auth_sessions",
    "expense_cases",
    "commercial_billing_periods",
    "tenant_subscriptions",
    "financial_connector_configs",
    "agent_asset_release_observations",
)


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


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


def _has_table(table_name: str) -> bool:
    return _inspector().has_table(table_name)


def _has_column(table_name: str, column_name: str) -> bool:
    if not _has_table(table_name):
        return False
    return column_name in {
        str(item["name"]) for item in _inspector().get_columns(table_name)
    }


def _constraint_exists(table_name: str, constraint_name: str) -> bool:
    inspector = _inspector()
    names = {
        str(item.get("name") or "")
        for item in (
            *inspector.get_unique_constraints(table_name),
            *inspector.get_foreign_keys(table_name),
            *inspector.get_check_constraints(table_name),
        )
    }
    return constraint_name in names


def _index_exists(table_name: str, index_name: str) -> bool:
    return index_name in {
        str(item.get("name") or "") for item in _inspector().get_indexes(table_name)
    }


def _drop_unique_for_columns(table_name: str, columns: tuple[str, ...]) -> None:
    if not _has_table(table_name):
        return
    for item in _inspector().get_unique_constraints(table_name):
        if tuple(item.get("column_names") or ()) != columns:
            continue
        name = str(item.get("name") or "")
        if name:
            op.drop_constraint(name, table_name, type_="unique")


def _drop_foreign_keys_for_columns(
    table_name: str,
    columns: tuple[str, ...],
) -> None:
    if not _has_table(table_name):
        return
    for item in _inspector().get_foreign_keys(table_name):
        if tuple(item.get("constrained_columns") or ()) != columns:
            continue
        name = str(item.get("name") or "")
        if name:
            op.drop_constraint(name, table_name, type_="foreignkey")


def _ensure_unique(
    table_name: str,
    name: str,
    columns: tuple[str, ...],
) -> None:
    if not _constraint_exists(table_name, name):
        op.create_unique_constraint(name, table_name, list(columns))


def _ensure_index(
    table_name: str,
    name: str,
    columns: tuple[str, ...],
) -> None:
    if not _index_exists(table_name, name):
        op.create_index(name, table_name, list(columns))


def _ensure_tenant_column(table_name: str) -> None:
    if not _has_table(table_name):
        return
    if not _has_column(table_name, "tenant_id"):
        op.add_column(
            table_name,
            sa.Column("tenant_id", sa.String(length=64), nullable=True),
        )
    if table_name == "expense_claims" and _has_table("expense_case_links"):
        op.execute(
            sa.text(
                "UPDATE expense_claims AS claim SET tenant_id = COALESCE(("
                "SELECT link.tenant_id FROM expense_case_links AS link "
                "WHERE link.resource_type = 'expense_claim' "
                "AND link.resource_id = claim.id ORDER BY link.created_at ASC LIMIT 1"
                "), :default_tenant) WHERE claim.tenant_id IS NULL"
            ).bindparams(default_tenant=_DEFAULT_TENANT)
        )
    else:
        op.execute(
            sa.text(
                f"UPDATE {table_name} SET tenant_id = :default_tenant "
                "WHERE tenant_id IS NULL"
            ).bindparams(default_tenant=_DEFAULT_TENANT)
        )
    op.alter_column(
        table_name,
        "tenant_id",
        existing_type=sa.String(length=64),
        nullable=False,
        server_default=None,
    )


def _create_tenant_registry() -> None:
    if not _has_table("tenants"):
        op.create_table(
            "tenants",
            sa.Column("tenant_id", sa.String(length=64), nullable=False),
            sa.Column("tenant_code", sa.String(length=64), nullable=False),
            sa.Column("name", sa.String(length=160), nullable=False),
            sa.Column("status", sa.String(length=20), nullable=False),
            sa.Column(
                "created_at",
                sa.DateTime(timezone=True),
                server_default=sa.func.now(),
                nullable=False,
            ),
            sa.Column(
                "updated_at",
                sa.DateTime(timezone=True),
                server_default=sa.func.now(),
                nullable=False,
            ),
            sa.CheckConstraint(
                "status IN ('active', 'suspended', 'disabled')",
                name="ck_tenants_status",
            ),
            sa.CheckConstraint(
                "length(trim(tenant_id)) > 0 AND length(trim(tenant_code)) > 0 "
                "AND length(trim(name)) > 0",
                name="ck_tenants_identity",
            ),
            sa.PrimaryKeyConstraint("tenant_id"),
            sa.UniqueConstraint("tenant_code", name="uq_tenants_tenant_code"),
        )
        op.create_index("ix_tenants_status", "tenants", ["status"])
    op.execute(
        sa.text(
            "INSERT INTO tenants (tenant_id, tenant_code, name, status) VALUES "
            "('default', 'default', '默认企业', 'active'), "
            "('platform', 'platform', '平台管理域', 'active') "
            "ON CONFLICT DO NOTHING"
        )
    )
    for table_name in _TENANT_SOURCE_TABLES:
        if not _has_column(table_name, "tenant_id"):
            continue
        op.execute(
            sa.text(
                f"INSERT INTO tenants (tenant_id, tenant_code, name, status) "
                f"SELECT DISTINCT trim(tenant_id), trim(tenant_id), trim(tenant_id), "
                f"'active' FROM {table_name} "
                "WHERE tenant_id IS NOT NULL AND length(trim(tenant_id)) > 0 "
                "ON CONFLICT DO NOTHING"
            )
        )


def _tenantize_organizations() -> None:
    table = "organization_units"
    if not _has_table(table):
        return
    _ensure_tenant_column(table)
    _drop_unique_for_columns(table, ("unit_code",))
    _drop_foreign_keys_for_columns(table, ("parent_id",))
    _ensure_unique(table, "uq_organization_units_tenant_id", ("tenant_id", "id"))
    _ensure_unique(
        table,
        "uq_organization_units_tenant_code",
        ("tenant_id", "unit_code"),
    )
    if not _constraint_exists(table, "fk_organization_units_tenant"):
        op.create_foreign_key(
            "fk_organization_units_tenant",
            table,
            "tenants",
            ["tenant_id"],
            ["tenant_id"],
            ondelete="RESTRICT",
        )
    if not _constraint_exists(table, "fk_organization_units_tenant_parent"):
        op.create_foreign_key(
            "fk_organization_units_tenant_parent",
            table,
            table,
            ["tenant_id", "parent_id"],
            ["tenant_id", "id"],
            ondelete="RESTRICT",
        )
    _ensure_index(
        table,
        "ix_organization_units_tenant_name",
        ("tenant_id", "name"),
    )


def _tenantize_employees() -> None:
    table = "employees"
    if not _has_table(table):
        return
    _ensure_tenant_column(table)
    _drop_unique_for_columns(table, ("employee_no",))
    _drop_unique_for_columns(table, ("email",))
    _drop_foreign_keys_for_columns(table, ("organization_unit_id",))
    _drop_foreign_keys_for_columns(table, ("manager_id",))
    _ensure_unique(table, "uq_employees_tenant_id", ("tenant_id", "id"))
    _ensure_unique(
        table,
        "uq_employees_tenant_employee_no",
        ("tenant_id", "employee_no"),
    )
    _ensure_unique(table, "uq_employees_tenant_email", ("tenant_id", "email"))
    if not _constraint_exists(table, "fk_employees_tenant"):
        op.create_foreign_key(
            "fk_employees_tenant",
            table,
            "tenants",
            ["tenant_id"],
            ["tenant_id"],
            ondelete="RESTRICT",
        )
    if _has_table("organization_units") and not _constraint_exists(
        table,
        "fk_employees_tenant_organization_unit",
    ):
        op.create_foreign_key(
            "fk_employees_tenant_organization_unit",
            table,
            "organization_units",
            ["tenant_id", "organization_unit_id"],
            ["tenant_id", "id"],
            ondelete="RESTRICT",
        )
    if not _constraint_exists(table, "fk_employees_tenant_manager"):
        op.create_foreign_key(
            "fk_employees_tenant_manager",
            table,
            table,
            ["tenant_id", "manager_id"],
            ["tenant_id", "id"],
            ondelete="RESTRICT",
        )
    _ensure_index(table, "ix_employees_tenant_status", ("tenant_id", "employment_status"))
    _ensure_index(table, "ix_employees_tenant_name", ("tenant_id", "name"))


def _tenantize_financial_records() -> None:
    if _has_table("expense_claims"):
        table = "expense_claims"
        _ensure_tenant_column(table)
        _drop_unique_for_columns(table, ("claim_no",))
        _drop_foreign_keys_for_columns(table, ("employee_id",))
        _drop_foreign_keys_for_columns(table, ("department_id",))
        _ensure_unique(table, "uq_expense_claims_tenant_id", ("tenant_id", "id"))
        _ensure_unique(
            table,
            "uq_expense_claims_tenant_claim_no",
            ("tenant_id", "claim_no"),
        )
        if not _constraint_exists(table, "fk_expense_claims_tenant"):
            op.create_foreign_key(
                "fk_expense_claims_tenant",
                table,
                "tenants",
                ["tenant_id"],
                ["tenant_id"],
                ondelete="RESTRICT",
            )
        if _has_table("employees") and not _constraint_exists(
            table,
            "fk_expense_claims_tenant_employee",
        ):
            op.create_foreign_key(
                "fk_expense_claims_tenant_employee",
                table,
                "employees",
                ["tenant_id", "employee_id"],
                ["tenant_id", "id"],
                ondelete="RESTRICT",
            )
        if _has_table("organization_units") and not _constraint_exists(
            table,
            "fk_expense_claims_tenant_department",
        ):
            op.create_foreign_key(
                "fk_expense_claims_tenant_department",
                table,
                "organization_units",
                ["tenant_id", "department_id"],
                ["tenant_id", "id"],
                ondelete="RESTRICT",
            )
        _ensure_index(table, "ix_expense_claims_tenant_status", ("tenant_id", "status"))
        _ensure_index(
            table,
            "ix_expense_claims_tenant_occurred",
            ("tenant_id", "occurred_at"),
        )
    for table, number_column, constraint_name, dimension_column, index_name in (
        (
            "accounts_receivable",
            "receivable_no",
            "uq_accounts_receivable_tenant_no",
            "customer_id",
            "ix_accounts_receivable_tenant_customer",
        ),
        (
            "accounts_payable",
            "payable_no",
            "uq_accounts_payable_tenant_no",
            "vendor_id",
            "ix_accounts_payable_tenant_vendor",
        ),
    ):
        if not _has_table(table):
            continue
        _ensure_tenant_column(table)
        _drop_unique_for_columns(table, (number_column,))
        _ensure_unique(table, constraint_name, ("tenant_id", number_column))
        tenant_fk = f"fk_{table}_tenant"
        if not _constraint_exists(table, tenant_fk):
            op.create_foreign_key(
                tenant_fk,
                table,
                "tenants",
                ["tenant_id"],
                ["tenant_id"],
                ondelete="RESTRICT",
            )
        _ensure_index(table, index_name, ("tenant_id", dimension_column))


def _ensure_memberships() -> None:
    if not _has_table("employees"):
        return
    if not _has_table("tenant_memberships"):
        op.create_table(
            "tenant_memberships",
            sa.Column("id", sa.String(length=36), nullable=False),
            sa.Column("tenant_id", sa.String(length=64), nullable=False),
            sa.Column("employee_id", sa.String(length=36), nullable=False),
            sa.Column("status", sa.String(length=20), nullable=False),
            sa.Column("is_primary", sa.Boolean(), nullable=False),
            sa.Column(
                "created_at",
                sa.DateTime(timezone=True),
                server_default=sa.func.now(),
                nullable=False,
            ),
            sa.Column(
                "updated_at",
                sa.DateTime(timezone=True),
                server_default=sa.func.now(),
                nullable=False,
            ),
            sa.CheckConstraint(
                "status IN ('active', 'inactive')",
                name="ck_tenant_memberships_status",
            ),
            sa.ForeignKeyConstraint(
                ["tenant_id"],
                ["tenants.tenant_id"],
                name="fk_tenant_memberships_tenant",
                ondelete="RESTRICT",
            ),
            sa.ForeignKeyConstraint(
                ["tenant_id", "employee_id"],
                ["employees.tenant_id", "employees.id"],
                name="fk_tenant_memberships_tenant_employee",
                ondelete="CASCADE",
            ),
            sa.PrimaryKeyConstraint("id"),
            sa.UniqueConstraint(
                "tenant_id",
                "employee_id",
                name="uq_tenant_memberships_tenant_employee",
            ),
        )
        op.create_index(
            "ix_tenant_memberships_employee_active",
            "tenant_memberships",
            ["employee_id", "status"],
        )
        op.create_index(
            "ix_tenant_memberships_tenant_active",
            "tenant_memberships",
            ["tenant_id", "status"],
        )
    op.execute(
        sa.text(
            "INSERT INTO tenant_memberships "
            "(id, tenant_id, employee_id, status, is_primary) "
            "SELECT gen_random_uuid()::text, employee.tenant_id, employee.id, "
            "CASE WHEN employee.employment_status = '停用' THEN 'inactive' "
            "ELSE 'active' END, true FROM employees AS employee "
            "ON CONFLICT (tenant_id, employee_id) DO NOTHING"
        )
    )


def _bind_auth_sessions() -> None:
    if not _has_table("auth_sessions"):
        return
    if not _constraint_exists("auth_sessions", "fk_auth_sessions_tenant"):
        op.create_foreign_key(
            "fk_auth_sessions_tenant",
            "auth_sessions",
            "tenants",
            ["tenant_id"],
            ["tenant_id"],
            ondelete="RESTRICT",
        )


def upgrade() -> None:
    _require_postgresql()
    _create_tenant_registry()
    _tenantize_organizations()
    _tenantize_employees()
    _tenantize_financial_records()
    _ensure_memberships()
    _bind_auth_sessions()


def _require_safe_downgrade() -> None:
    for table_name in (
        "organization_units",
        "employees",
        "expense_claims",
        "accounts_receivable",
        "accounts_payable",
    ):
        if not _has_column(table_name, "tenant_id"):
            continue
        count = int(
            op.get_bind().scalar(
                sa.text(
                    f"SELECT COUNT(*) FROM {table_name} "
                    "WHERE tenant_id <> :default_tenant"
                ).bindparams(default_tenant=_DEFAULT_TENANT)
            )
            or 0
        )
        if count:
            raise RuntimeError(
                "cannot downgrade tenant identity foundation: "
                f"{table_name} contains non-default tenant data ({count})"
            )


def _restore_legacy_financial_constraints() -> None:
    if _has_table("expense_claims"):
        table = "expense_claims"
        for name in (
            "fk_expense_claims_tenant_department",
            "fk_expense_claims_tenant_employee",
            "fk_expense_claims_tenant",
            "uq_expense_claims_tenant_claim_no",
            "uq_expense_claims_tenant_id",
        ):
            if _constraint_exists(table, name):
                op.drop_constraint(name, table)
        for name in (
            "ix_expense_claims_tenant_occurred",
            "ix_expense_claims_tenant_status",
        ):
            if _index_exists(table, name):
                op.drop_index(name, table_name=table)
        _ensure_unique(table, "uq_expense_claims_claim_no", ("claim_no",))
        if _has_table("employees"):
            op.create_foreign_key(
                "fk_expense_claims_employee_id",
                table,
                "employees",
                ["employee_id"],
                ["id"],
            )
        if _has_table("organization_units"):
            op.create_foreign_key(
                "fk_expense_claims_department_id",
                table,
                "organization_units",
                ["department_id"],
                ["id"],
            )
        op.drop_column(table, "tenant_id")
    for table, constraint_name, number_column, index_name in (
        (
            "accounts_receivable",
            "uq_accounts_receivable_tenant_no",
            "receivable_no",
            "ix_accounts_receivable_tenant_customer",
        ),
        (
            "accounts_payable",
            "uq_accounts_payable_tenant_no",
            "payable_no",
            "ix_accounts_payable_tenant_vendor",
        ),
    ):
        if not _has_column(table, "tenant_id"):
            continue
        tenant_fk = f"fk_{table}_tenant"
        if _constraint_exists(table, tenant_fk):
            op.drop_constraint(tenant_fk, table, type_="foreignkey")
        if _constraint_exists(table, constraint_name):
            op.drop_constraint(constraint_name, table, type_="unique")
        if _index_exists(table, index_name):
            op.drop_index(index_name, table_name=table)
        _ensure_unique(table, f"uq_{table}_{number_column}", (number_column,))
        op.drop_column(table, "tenant_id")


def _restore_legacy_employee_constraints() -> None:
    if _has_table("employees"):
        table = "employees"
        for name in (
            "fk_employees_tenant_manager",
            "fk_employees_tenant_organization_unit",
            "fk_employees_tenant",
            "uq_employees_tenant_email",
            "uq_employees_tenant_employee_no",
            "uq_employees_tenant_id",
        ):
            if _constraint_exists(table, name):
                op.drop_constraint(name, table)
        for name in ("ix_employees_tenant_name", "ix_employees_tenant_status"):
            if _index_exists(table, name):
                op.drop_index(name, table_name=table)
        _ensure_unique(table, "uq_employees_employee_no", ("employee_no",))
        _ensure_unique(table, "uq_employees_email", ("email",))
        if _has_table("organization_units"):
            op.create_foreign_key(
                "fk_employees_organization_unit_id",
                table,
                "organization_units",
                ["organization_unit_id"],
                ["id"],
            )
        op.create_foreign_key(
            "fk_employees_manager_id",
            table,
            table,
            ["manager_id"],
            ["id"],
        )
        op.drop_column(table, "tenant_id")
    if _has_table("organization_units"):
        table = "organization_units"
        for name in (
            "fk_organization_units_tenant_parent",
            "fk_organization_units_tenant",
            "uq_organization_units_tenant_code",
            "uq_organization_units_tenant_id",
        ):
            if _constraint_exists(table, name):
                op.drop_constraint(name, table)
        if _index_exists(table, "ix_organization_units_tenant_name"):
            op.drop_index("ix_organization_units_tenant_name", table_name=table)
        _ensure_unique(table, "uq_organization_units_unit_code", ("unit_code",))
        op.create_foreign_key(
            "fk_organization_units_parent_id",
            table,
            table,
            ["parent_id"],
            ["id"],
        )
        op.drop_column(table, "tenant_id")


def downgrade() -> None:
    _require_postgresql()
    _require_safe_downgrade()
    if _has_table("auth_sessions") and _constraint_exists(
        "auth_sessions",
        "fk_auth_sessions_tenant",
    ):
        op.drop_constraint(
            "fk_auth_sessions_tenant",
            "auth_sessions",
            type_="foreignkey",
        )
    if _has_table("tenant_memberships"):
        op.drop_table("tenant_memberships")
    _restore_legacy_financial_constraints()
    _restore_legacy_employee_constraints()
    if _has_table("tenants"):
        if _index_exists("tenants", "ix_tenants_status"):
            op.drop_index("ix_tenants_status", table_name="tenants")
        op.drop_table("tenants")
