#!/usr/bin/env python3
"""
检查 Alembic migration 编号连续性。

扫描 alembic/versions/ 下所有 migration 文件，提取 revision 和 down_revision，
验证整条链是否完整——每个 down_revision（除了 baseline 的 None）都必须对应一个存在的 revision。

支持两种格式:
    revision: str = "001"          # 旧格式（带类型注解）
    revision = "038_error_retry"   # 新格式（带描述后缀）

匹配策略：提取 revision 名称的数字前缀（如 "001"、"038"）作为唯一标识进行匹配，
兼容纯数字编号和"数字_描述"两种命名风格。

用法:
    python3 scripts/ci/check_migration_chain.py [alembic_versions_dir]

默认目录: alembic/versions/

退出码:
    0 - 链完整
    1 - 有断链或其他错误
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

# 匹配 revision / down_revision，支持带类型注解和不带类型注解两种格式
# revision: str = "xxx" 或 revision = "xxx"
REV_PATTERN = re.compile(
    r'^\s*revision\s*(?::\s*str\s*)?=\s*["\']([^"\']+)["\']',
    re.MULTILINE,
)
DOWN_PATTERN = re.compile(
    r'^\s*down_revision\s*(?::\s*(?:Union\[str,\s*None\]|str\s*\|\s*None|None|str)\s*)?=\s*(["\']([^"\']+)["\']|None)',
    re.MULTILINE,
)

# 提取 revision 名称的数字前缀，如 "001" 或 "038_error_retry" → "038"
NUM_PREFIX_PATTERN = re.compile(r"^(\d+)")


def num_prefix(name: str) -> str:
    """提取 revision 名称的数字前缀。"""
    m = NUM_PREFIX_PATTERN.match(name)
    return m.group(1) if m else name


def extract_migration_info(filepath: Path) -> tuple[str, str | None]:
    """从 migration 文件中提取 revision 和 down_revision（返回完整名称）。"""
    content = filepath.read_text(encoding="utf-8")

    rev_match = REV_PATTERN.search(content)
    down_match = DOWN_PATTERN.search(content)

    if not rev_match:
        raise ValueError(f"{filepath.name}: 未找到 revision 定义")

    revision = rev_match.group(1)

    if not down_match:
        raise ValueError(f"{filepath.name}: 未找到 down_revision 定义")

    # down_match group(2) 是引号内的值，如果是 None 则 group(2) 为 None
    down_revision = down_match.group(2)

    return revision, down_revision


def check_chain(versions_dir: Path) -> list[str]:
    """检查 migration 链是否完整，返回错误列表。"""
    errors: list[str] = []

    if not versions_dir.is_dir():
        return [f"目录不存在: {versions_dir}"]

    py_files = sorted(versions_dir.glob("*.py"))
    if not py_files:
        return [f"目录下没有 migration 文件: {versions_dir}"]

    # 收集所有 revision（用数字前缀做唯一标识）
    revisions_by_num: dict[str, str] = {}  # 数字前缀 -> 完整 revision 名
    revision_files: dict[str, str] = {}  # 数字前缀 -> 文件名
    down_revisions: list[tuple[str, str | None]] = []  # (文件名, down_revision 数字前缀或None)

    for f in py_files:
        if f.name.startswith("__"):
            continue
        try:
            rev, down = extract_migration_info(f)
        except ValueError as e:
            errors.append(str(e))
            continue

        rev_num = num_prefix(rev)

        if rev_num in revisions_by_num:
            errors.append(
                f"编号重复: 编号 {rev_num} 同时出现在 "
                f"{f.name} (revision={rev}) 和 {revision_files[rev_num]} (revision={revisions_by_num[rev_num]})"
            )
        else:
            revisions_by_num[rev_num] = rev
            revision_files[rev_num] = f.name

        down_num = num_prefix(down) if down else None
        down_revisions.append((f.name, down_num))

    if errors:
        return errors

    # 检查每个 down_revision 是否存在
    baselines = 0
    for filename, down_num in down_revisions:
        if down_num is None:
            baselines += 1
            continue

        if down_num not in revisions_by_num:
            errors.append(
                f"断链: {filename} 的 down_revision 指向编号 '{down_num}'，但没有任何 migration 的 revision 是这个编号"
            )

    if baselines == 0:
        errors.append("没有找到 baseline migration（down_revision = None 的文件）")
    elif baselines > 1:
        errors.append(f"发现 {baselines} 个 baseline migration，通常只能有 1 个")

    # 额外检查：数字编号是否连续（只对能提取出数字的）
    if revisions_by_num and not errors:
        nums = sorted(int(n) for n in revisions_by_num if n.isdigit())
        if nums:
            expected = list(range(nums[0], nums[-1] + 1))
            missing = [n for n in expected if n not in nums]
            if missing:
                missing_str = ", ".join(f"{n:03d}" for n in missing)
                errors.append(f"编号不连续: 缺少编号 {missing_str}")

    return errors


def main() -> int:
    if len(sys.argv) > 1:
        versions_dir = Path(sys.argv[1])
    else:
        versions_dir = Path("alembic/versions")

    print(f"检查 migration 编号连续性: {versions_dir}")
    print()

    errors = check_chain(versions_dir)

    py_files = [f for f in versions_dir.glob("*.py") if not f.name.startswith("__")]

    if errors:
        print(f"❌ Migration 链有问题（共 {len(py_files)} 个文件，{len(errors)} 个错误）:")
        for e in errors:
            print(f"  - {e}")
        print()
        print("请修复后再提交。常见原因：")
        print("  1. 新 migration 的 down_revision 编号写错了")
        print("  2. 多个 PR 同时加 migration，编号冲突")
        print("  3. 合并代码时漏了某个 migration 文件")
        return 1

    print(f"✅ Migration 链完整，共 {len(py_files)} 个版本")
    return 0


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