from __future__ import annotations

import sys
from dataclasses import dataclass

from sqlalchemy import create_engine, inspect, text
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.exc import SQLAlchemyError

from app.core.config import get_settings
from app.db.schema_ownership import MIGRATION_OWNED_TABLES

# 迁移脚本新增或删除自有表时，必须同步更新该映射并补充对应测试。
MIGRATION_OWNED_TABLES_BY_REVISION: dict[str, frozenset[str]] = {
    "20260713_0001": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
        }
    ),
    "20260713_0002": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
        }
    ),
    "20260714_0003": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "ai_decisions",
            "ai_decision_feedback",
            "workflow_outcomes",
        }
    ),
    "20260714_0004": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "workflow_outcomes",
        }
    ),
    "20260714_0005": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "workflow_outcomes",
        }
    ),
    "20260716_0006": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "workflow_outcomes",
        }
    ),
    "20260716_0007": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "workflow_outcomes",
        }
    ),
    "20260716_0008": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "few_shot_samples",
            "workflow_outcomes",
        }
    ),
    "20260716_0009": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "few_shot_samples",
            "workflow_outcomes",
        }
    ),
    "20260716_0010": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "few_shot_samples",
            "workflow_outcomes",
            "approval_action_ledgers",
        }
    ),
    "20260716_0011": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "risk_dispositions",
            "risk_disposition_events",
            "few_shot_samples",
            "workflow_outcomes",
            "approval_action_ledgers",
        }
    ),
    "20260716_0012": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "risk_dispositions",
            "risk_disposition_events",
            "few_shot_samples",
            "workflow_outcomes",
            "approval_action_ledgers",
        }
    ),
    "20260716_0013": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "risk_dispositions",
            "risk_disposition_events",
            "few_shot_samples",
            "workflow_outcomes",
            "approval_action_ledgers",
            "approval_tasks",
            "approval_task_events",
        }
    ),
    "20260716_0014": frozenset(
        {
            "expense_cases",
            "expense_case_links",
            "business_events",
            "auth_sessions",
            "attachment_association_jobs",
            "ai_application_preview_decisions",
            "ai_decisions",
            "ai_decision_feedback",
            "memory_entries",
            "memory_evidence_links",
            "risk_observations",
            "risk_observation_feedback",
            "risk_dispositions",
            "risk_disposition_events",
            "few_shot_samples",
            "workflow_outcomes",
            "approval_action_ledgers",
            "approval_tasks",
            "approval_task_events",
        }
    ),
}
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0015"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0014"]
    | frozenset(
        {
            "profile_baseline_snapshots",
            "savings_opportunities",
            "savings_realizations",
            "savings_evidence_links",
            "savings_events",
        }
    )
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0016"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0015"]
    | frozenset(
        {
            "tenant_commercial_plans",
            "tenant_subscriptions",
            "commercial_entitlements",
            "usage_meter_events",
            "commercial_cost_events",
        }
    )
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0017"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0016"]
    | frozenset(
        {
            "financial_connector_configs",
            "financial_connector_events",
            "payment_reconciliation_cases",
            "payment_reconciliation_events",
        }
    )
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0018"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0017"]
    | frozenset(
        {
            "agent_asset_release_observations",
            "agent_asset_release_labels",
        }
    )
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0019"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0018"]
    | frozenset({"commercial_runtime_reservations"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0020"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0019"]
    | frozenset({"financial_connector_config_events"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0021"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0020"]
    | frozenset({"commercial_admin_events", "commercial_billing_periods"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0022"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0021"]
    | frozenset({"financial_connector_operational_events"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0023"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0022"]
    | frozenset({"agent_asset_release_audit_samples"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0024"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260716_0023"]
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0025"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0024"]
    | frozenset({"tenants"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0026"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0025"]
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0027"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0026"]
    | frozenset({"knowledge_onlyoffice_sessions"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0028"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0027"]
    | frozenset({"tenant_finance_report_configs", "tenant_finance_report_runs"})
)
MIGRATION_OWNED_TABLES_BY_REVISION["20260718_0029"] = (
    MIGRATION_OWNED_TABLES_BY_REVISION["20260717_0028"]
)
if MIGRATION_OWNED_TABLES_BY_REVISION["20260718_0029"] != MIGRATION_OWNED_TABLES:
    raise RuntimeError("latest Alembic revision must own the centralized migration table set")

# 0008 之前这三张表由旧 bootstrap / 风险服务按需创建。迁移前置检查允许
# 它们作为完整或部分旧资产存在，由 0008 统一收编；其他未来表仍严格拒绝。
LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES = frozenset(
    {"risk_observations", "risk_observation_feedback", "few_shot_samples"}
)


class MigrationPreflightError(RuntimeError):
    """Raised when the database schema cannot be safely advanced by Alembic."""


@dataclass(frozen=True)
class MigrationPreflightState:
    revision: str | None
    owned_tables: frozenset[str]


def _format_tables(table_names: frozenset[str]) -> str:
    return ", ".join(sorted(table_names)) or "none"


def _validate_connection(connection: Connection) -> MigrationPreflightState:
    table_names = frozenset(inspect(connection).get_table_names())
    owned_tables = table_names & MIGRATION_OWNED_TABLES

    if "alembic_version" not in table_names:
        unsafe_owned_tables = owned_tables - LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
        if unsafe_owned_tables:
            raise MigrationPreflightError(
                "unversioned database contains migration-owned tables "
                f"({_format_tables(unsafe_owned_tables)}); refusing to guess, stamp, or repair"
            )
        return MigrationPreflightState(revision=None, owned_tables=owned_tables)

    revisions = tuple(
        str(revision)
        for revision in connection.execute(
            text("SELECT version_num FROM alembic_version")
        ).scalars()
    )
    if not revisions:
        unsafe_owned_tables = owned_tables - LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
        if unsafe_owned_tables:
            raise MigrationPreflightError(
                "alembic_version has no recorded revision but migration-owned tables exist "
                f"({_format_tables(unsafe_owned_tables)}); refusing to guess, stamp, or repair"
            )
        return MigrationPreflightState(revision=None, owned_tables=owned_tables)

    if len(revisions) > 1:
        raise MigrationPreflightError(
            "multiple Alembic revisions are recorded "
            f"({', '.join(sorted(revisions))}); branch state is unsupported"
        )

    revision = revisions[0]
    expected_tables = MIGRATION_OWNED_TABLES_BY_REVISION.get(revision)
    if expected_tables is None:
        raise MigrationPreflightError(
            f"unknown Alembic revision {revision!r}; refusing to run migrations"
        )

    adoptable_tables = (
        LEGACY_ADOPTABLE_HISTORICAL_CASE_TABLES
        if revision
        not in {
            "20260716_0008",
            "20260716_0009",
            "20260716_0010",
            "20260716_0011",
            "20260716_0012",
            "20260716_0013",
            "20260716_0014",
            "20260716_0015",
            "20260716_0016",
            "20260716_0017",
            "20260716_0018",
            "20260716_0019",
            "20260716_0020",
            "20260716_0021",
            "20260716_0022",
            "20260716_0023",
            "20260717_0024",
            "20260717_0025",
            "20260717_0026",
            "20260717_0027",
            "20260717_0028",
            "20260718_0029",
        }
        else frozenset()
    )
    missing_tables = expected_tables - owned_tables
    unexpected_tables = owned_tables - expected_tables - adoptable_tables
    if missing_tables or unexpected_tables:
        raise MigrationPreflightError(
            f"migration-owned table set does not match revision {revision}: "
            f"missing={_format_tables(missing_tables)}; "
            f"unexpected={_format_tables(unexpected_tables)}"
        )

    return MigrationPreflightState(revision=revision, owned_tables=owned_tables)


def validate_migration_state(bind: Engine | Connection) -> MigrationPreflightState:
    """Inspect the database without changing its schema or Alembic revision state."""

    if isinstance(bind, Engine):
        with bind.connect() as connection:
            return _validate_connection(connection)
    return _validate_connection(bind)


def main() -> int:
    settings = get_settings()
    engine = create_engine(settings.resolved_database_url, pool_pre_ping=True)
    try:
        state = validate_migration_state(engine)
    except (MigrationPreflightError, SQLAlchemyError) as exc:
        print(f"Database migration preflight failed: {exc}", file=sys.stderr)
        return 1
    finally:
        engine.dispose()

    revision = state.revision or "unversioned/base"
    print(
        "Database migration preflight passed: "
        f"revision={revision}; owned_tables={_format_tables(state.owned_tables)}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
