#!/usr/bin/env python3
"""
数据库迁移破坏性变更安全检查

只检查 Alembic 迁移文件的 upgrade 函数中是否包含破坏性操作：
- DROP TABLE
- ALTER TABLE ... DROP COLUMN
- 列类型变更（可能导致数据丢失）
- NOT NULL 约束新增（无默认值时）
- RENAME TABLE / RENAME COLUMN

忽略 downgrade 函数中的操作（那是回滚逻辑，正常的）。

使用方式：
  # 检查所有迁移（不推荐，会扫历史已执行的迁移）
  python3 scripts/check_migration_safety.py

  # 只检查与目标分支相比新增的迁移（推荐用于CI）
  python3 scripts/check_migration_safety.py --diff-against origin/main

  # 只检查指定版本之后的迁移
  python3 scripts/check_migration_safety.py --since 030_xxx

退出码：
  0 - 安全 / 只有非破坏性变更
  1 - 检测到高风险破坏性变更
  2 - 检测到中风险变更，需人工确认
"""

from __future__ import annotations

import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple

REPO_ROOT = Path(__file__).resolve().parents[1]
ALEMBIC_VERSIONS_DIR = REPO_ROOT / "alembic" / "versions"

# 高风险模式：直接导致数据丢失（只在 upgrade 中检查）
HIGH_RISK_PATTERNS = [
    (r"\bop\.drop_table\(", "op.drop_table() - 删除表，数据永久丢失"),
    (r"\bop\.drop_column\(", "op.drop_column() - 删除列，数据永久丢失"),
]

# 中风险模式：可能导致数据丢失或兼容性问题
MEDIUM_RISK_PATTERNS = [
    (r"op\.alter_column\([^)]*nullable\s*=\s*False", "新增 NOT NULL 约束 - 旧数据可能为空导致迁移失败"),
    (r"op\.alter_column\([^)]*type_\s*=", "列类型变更 - 可能导致数据截断或转换失败"),
    (r"\bop\.rename_table\(", "op.rename_table() - 重命名表，可能导致依赖该表的代码报错"),
    (r"\bop\.rename_column\(", "op.rename_column() - 重命名列，可能导致依赖该列的代码报错"),
    (r"\bop\.drop_index\(", "op.drop_index() - 删除索引，可能影响查询性能"),
    (r"\bop\.drop_constraint\(", "op.drop_constraint() - 删除约束，可能影响数据完整性"),
]

# 安全模式：这些是安全的新增操作
SAFE_PATTERNS = [
    (r"\bop\.create_table\(", "新建表"),
    (r"\bop\.add_column\(", "新增列"),
    (r"\bop\.create_index\(", "新建索引"),
    (r"\bop\.create_unique_constraint\(", "新建唯一约束"),
    (r"\bop\.create_foreign_key\(", "新建外键约束"),
]


def extract_upgrade_content(content: str) -> str:
    """
    从迁移文件中提取 upgrade 函数的内容。
    只检查 upgrade 中的操作，忽略 downgrade。
    """
    upgrade_match = re.search(r"def upgrade\b[^:]*:", content)
    if not upgrade_match:
        return ""

    upgrade_start = upgrade_match.end()

    # 找到下一个顶层 def（通常是 def downgrade）作为结束位置
    rest = content[upgrade_start:]
    downgrade_match = re.search(r"\n\ndef\s+\w+\b", rest)
    if downgrade_match:
        upgrade_end = upgrade_start + downgrade_match.start()
    else:
        upgrade_end = len(content)

    return content[upgrade_start:upgrade_end]


def get_new_migrations_via_diff(diff_target: str) -> List[Path]:
    """
    通过 git diff 对比目标分支/commit，找出 alembic/versions/ 下新增的迁移文件。
    只包含新增文件（A状态），不包含修改或删除的文件。
    """
    try:
        result = subprocess.run(
            ["git", "diff", "--name-only", "--diff-filter=A", diff_target, "HEAD", "--", "alembic/versions/"],
            cwd=str(REPO_ROOT),
            capture_output=True,
            text=True,
            check=True,
        )
        files = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
        return [REPO_ROOT / f for f in files]
    except subprocess.CalledProcessError as e:
        print(f"⚠️  git diff 失败（{diff_target}）：{e.stderr.strip()}")
        print(f"   降级为检查所有迁移文件")
        return sorted(ALEMBIC_VERSIONS_DIR.glob("*.py"))


def find_new_migrations(since_revision: str | None = None, diff_against: str | None = None) -> List[Path]:
    """
    找出需要检查的迁移文件。
    优先级：diff_against > since_revision > 全部
    """
    if diff_against:
        return get_new_migrations_via_diff(diff_against)

    all_migrations = sorted(ALEMBIC_VERSIONS_DIR.glob("*.py"))
    if not since_revision:
        return all_migrations

    result = []
    found = False
    for m in all_migrations:
        if since_revision in m.name or since_revision in m.stem:
            found = True
            continue
        if found:
            result.append(m)

    return result if found else all_migrations


def analyze_migration(file_path: Path) -> Tuple[List[str], List[str], List[str]]:
    """分析单个迁移文件 upgrade 部分的风险等级"""
    content = file_path.read_text()
    upgrade_content = extract_upgrade_content(content)

    if not upgrade_content:
        return [], [], [f"{file_path.name}: 未找到 upgrade 函数"]

    high_risks = []
    medium_risks = []
    safes = []

    for pattern, desc in HIGH_RISK_PATTERNS:
        if re.search(pattern, upgrade_content):
            high_risks.append(f"{file_path.name}: {desc}")

    for pattern, desc in MEDIUM_RISK_PATTERNS:
        if re.search(pattern, upgrade_content):
            medium_risks.append(f"{file_path.name}: {desc}")

    for pattern, desc in SAFE_PATTERNS:
        if re.search(pattern, upgrade_content):
            safes.append(f"{file_path.name}: {desc}")

    return high_risks, medium_risks, safes


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        "--since",
        default=os.getenv("MIGRATION_SINCE_REVISION"),
        help="只检查指定版本之后的迁移（如：030_xxx），不传则检查所有迁移",
    )
    parser.add_argument(
        "--diff-against",
        default=os.getenv("MIGRATION_DIFF_AGAINST"),
        help="对比指定分支/commit，只检查新增的迁移文件（推荐用于CI，如 origin/main）",
    )
    parser.add_argument(
        "--warn-only",
        action="store_true",
        help="只警告不失败（用于非强制门禁场景）",
    )
    parser.add_argument(
        "--allow-medium-risk",
        action="store_true",
        help="允许中风险变更（只拦截高风险）",
    )
    args = parser.parse_args()

    migrations = find_new_migrations(args.since, args.diff_against)

    if not migrations:
        print("✅ 未找到需要检查的新增迁移文件，跳过")
        return 0

    print(f"🔍 正在检查 {len(migrations)} 个迁移文件的 upgrade 操作...")
    if args.diff_against:
        print(f"   （对比基准：{args.diff_against}，仅检查新增迁移）")
    print()

    all_high = []
    all_medium = []
    all_safe = []

    for m in migrations:
        high, medium, safe = analyze_migration(m)
        all_high.extend(high)
        all_medium.extend(medium)
        all_safe.extend(safe)

    if all_safe:
        print("✅ 安全变更：")
        for s in all_safe:
            print(f"   - {s}")
        print()

    if all_medium:
        print("⚠️  中风险变更（需人工确认）：")
        for m_item in all_medium:
            print(f"   - {m_item}")
        print()

    if all_high:
        print("❌ 高风险破坏性变更（禁止自动部署）：")
        for h in all_high:
            print(f"   - {h}")
        print()

    print("=" * 60)
    print(f"检查结果：{len(all_safe)} 项安全 / {len(all_medium)} 项中风险 / {len(all_high)} 项高风险")
    print()

    if all_high:
        print("❌ 检测到高风险破坏性变更，CI 检查失败！")
        print("   如果确认这是预期操作，请在 MR/PR 中说明原因并获得审批。")
        if args.warn_only:
            return 0
        return 1

    if all_medium and not args.allow_medium_risk:
        print("⚠️  检测到中风险变更，请人工确认后再部署。")
        if args.warn_only:
            return 0
        print("（如需仅拦截高风险，可使用 --allow-medium-risk 参数）")
        return 2

    print("✅ 未检测到破坏性变更")
    return 0


if __name__ == "__main__":
    sys.exit(main())

