#!/usr/bin/env python3
"""Cursor → Claude Code 反向迁移决策固化脚本。

用途：
  1. `--check` 模式：扫描当前仓库与用户目录，校验本次反向迁移的所有产物
     是否就位（CLAUDE.md 单轨化追加章节、AGENTS.md 已归档、.cursor/ 已删除、
     用户级 skills 已剥包装、敏感串扫描通过）。
  2. 作为决策记录与回滚参考——脚本注释里明确写了每条决策的原因和源/目标。
  3. 不再实现 `--apply` 模式：本次迁移已手动执行完毕，重跑没有价值；
     如需复用决策，请参考 `tools/cursor/migrate_ai_environment.py` 的结构
     与本脚本里的常量定义。

使用：
  python tools/claude-code/migrate_ai_environment.py --check
  python tools/claude-code/migrate_ai_environment.py --check --strict   # 失败立即退出
"""
from __future__ import annotations

import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable

REPO_ROOT = Path(os.environ.get("NEOZQYY_ROOT", Path(__file__).resolve().parents[2]))
USER_HOME = Path(os.environ.get("USERPROFILE") or os.path.expanduser("~"))

BACKUP_ROOT = USER_HOME / ".claude" / "backups" / "pre-claude-code-migration-2026-05-02"


# ---------- 决策常量（迁移决策一处定义） ----------

# 单轨化：以下 5 个 AGENTS.md 应被删除（备份在 BACKUP_ROOT/project_root/）
AGENTS_MD_TO_REMOVE = [
    "AGENTS.md",
    "apps/backend/AGENTS.md",
    "apps/etl/connectors/feiqiu/AGENTS.md",
    "apps/demo-miniprogram/AGENTS.md",
    "db/AGENTS.md",
]

# 单轨化：以下文件应保留（CLAUDE.md 是 Claude Code 原生加载入口）
CLAUDE_MD_REQUIRED = [
    "CLAUDE.md",
    "apps/backend/CLAUDE.md",
    "apps/etl/connectors/feiqiu/CLAUDE.md",
    "apps/demo-miniprogram/CLAUDE.md",
    "db/CLAUDE.md",
]

# 已归档删除的 Cursor 资产
CURSOR_PATHS_TO_REMOVE = [
    ".cursor",
    ".cursorignore",
]

# 项目级 Claude Code 资产（必须存在）
PROJECT_CLAUDE_REQUIRED = [
    ".claude/settings.json",
    ".claude/commands/audit.md",
    ".claude/commands/db-docs.md",
    ".claude/commands/doc-sync.md",
    ".claude/commands/pre-change.md",
    ".claude/commands/spec-close.md",
    ".claude/hooks/session_start_context.py",
    ".claude/hooks/pre_read_archived_block.py",
    ".claude/hooks/pre_demo_protect.py",
    ".claude/hooks/post_edit_audit_reminder.py",
    ".claude/hooks/post_edit_db_doc_sync.py",
    ".claude/hooks/post_edit_rls_dual_schema.py",
    ".claude/hooks/stop_audit_check.py",
    ".claude/hooks/stop_verify_check.py",
]

# 用户级 8 个 subagent
USER_AGENTS_REQUIRED = [
    "architect.md",
    "code-reviewer.md",
    "database-reviewer.md",
    "planner.md",
    "python-reviewer.md",
    "refactor-cleaner.md",
    "security-reviewer.md",
    "tdd-guide.md",
]

# 用户级 skills（12 个，回迁自 ~/.cursor/skills/，剥包装）
USER_SKILLS_REQUIRED = [
    "agent-introspection-debugging",
    "claude-agent-roles",
    "claude-rules-reference",
    "code-tour",
    "codebase-onboarding",
    "neozqyy-claude-code-migration",  # 重命名自 neozqyy-cursor-migration
    "repo-scan",
    "rules-distill",
    "search-first",
    "security-review",
    "strategic-compact",
    "tdd-workflow",
]

# 第 10.1 节：自动触发（保留 description 关键词激活）
USER_SKILLS_AUTO_TRIGGER = {"strategic-compact", "neozqyy-claude-code-migration"}

# Cursor 时代留下的兼容前缀，迁回时应被剥掉
CURSOR_RESIDUAL_PATTERNS = [
    re.compile(r"disable-model-invocation:\s*true", re.IGNORECASE),
    re.compile(r"^>\s*迁移说明：本 skill 从 [^\n]+ 转换而来", re.MULTILINE),
]

# 根 CLAUDE.md 必须包含的迁移章节（与原有项目章节并存）
CLAUDE_MD_MIGRATION_SECTIONS = [
    "## CLI / Shell 中文与编码（强制）",
    "## Claude Code 资产入口",
    "## Hook 与权限",
    "## 不破坏原则",
    "## 历史追溯",
]

# 历史索引敏感扫描
SENSITIVE_PATTERNS = {
    "DSN_with_password": re.compile(r"://[^:/\s]+:[^@/\s'\"]+@[^/\s'\"]+/[A-Za-z]"),
    "Anthropic_key": re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{30,}\b"),
    "OpenAI_key": re.compile(r"\bsk-proj-[A-Za-z0-9_\-]{30,}\b"),
    "JWT_long": re.compile(r"\beyJ[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{30,}\.[A-Za-z0-9_\-]{30,}\b"),
    "AWS_AKIA": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
}


# ---------- 检查框架 ----------

@dataclass
class CheckResult:
    name: str
    passed: bool
    detail: str = ""


@dataclass
class CheckReport:
    results: list[CheckResult] = field(default_factory=list)

    def add(self, result: CheckResult) -> None:
        self.results.append(result)

    def passed_count(self) -> int:
        return sum(1 for r in self.results if r.passed)

    def failed(self) -> list[CheckResult]:
        return [r for r in self.results if not r.passed]


def _check_files_exist(paths: list[str], base: Path, label: str) -> CheckResult:
    missing = [p for p in paths if not (base / p).exists()]
    if missing:
        return CheckResult(label, False, f"缺失 {len(missing)} 个：{missing[:5]}")
    return CheckResult(label, True, f"全部 {len(paths)} 个就位")


def _check_files_absent(paths: list[str], base: Path, label: str) -> CheckResult:
    present = [p for p in paths if (base / p).exists()]
    if present:
        return CheckResult(label, False, f"应已删除但仍存在 {len(present)} 个：{present}")
    return CheckResult(label, True, f"全部 {len(paths)} 个已归档删除")


def check_root_claude_md(report: CheckReport) -> None:
    """根 CLAUDE.md 是否包含迁移所需章节（与原有项目章节并存）。"""
    p = REPO_ROOT / "CLAUDE.md"
    if not p.exists():
        report.add(CheckResult("根 CLAUDE.md 存在", False, "文件不存在"))
        return
    text = p.read_text(encoding="utf-8")
    missing = [s for s in CLAUDE_MD_MIGRATION_SECTIONS if s not in text]
    if missing:
        report.add(CheckResult("根 CLAUDE.md 迁移章节", False, f"缺章节：{missing}"))
    else:
        report.add(CheckResult("根 CLAUDE.md 迁移章节", True, f"全部 {len(CLAUDE_MD_MIGRATION_SECTIONS)} 节存在"))


def check_agents_md_removed(report: CheckReport) -> None:
    report.add(_check_files_absent(AGENTS_MD_TO_REMOVE, REPO_ROOT, "AGENTS.md 已归档（5 个）"))


def check_cursor_removed(report: CheckReport) -> None:
    report.add(_check_files_absent(CURSOR_PATHS_TO_REMOVE, REPO_ROOT, ".cursor/ 与 .cursorignore 已归档"))


def check_claude_md_present(report: CheckReport) -> None:
    report.add(_check_files_exist(CLAUDE_MD_REQUIRED, REPO_ROOT, "CLAUDE.md 单轨保留（5 个）"))


def check_project_claude_assets(report: CheckReport) -> None:
    report.add(_check_files_exist(PROJECT_CLAUDE_REQUIRED, REPO_ROOT, ".claude/ 项目级资产"))


def check_user_agents(report: CheckReport) -> None:
    base = USER_HOME / ".claude" / "agents"
    report.add(_check_files_exist(USER_AGENTS_REQUIRED, base, "用户级 8 个 subagent"))


def check_user_skills(report: CheckReport) -> None:
    base = USER_HOME / ".claude" / "skills"
    if not base.exists():
        report.add(CheckResult("用户级 12 个 skills 目录", False, f"目录不存在：{base}"))
        return
    missing = [name for name in USER_SKILLS_REQUIRED if not (base / name / "SKILL.md").exists()]
    if missing:
        report.add(CheckResult("用户级 12 个 skills 目录", False, f"缺失 {len(missing)}：{missing}"))
    else:
        report.add(CheckResult("用户级 12 个 skills 目录", True, "全部 12 个 SKILL.md 就位"))


def check_user_skills_stripped(report: CheckReport) -> None:
    """检查用户级 skills 已剥掉 Cursor 包装。"""
    base = USER_HOME / ".claude" / "skills"
    if not base.exists():
        report.add(CheckResult("Skills 已剥 Cursor 包装", False, "目录不存在"))
        return
    residual = []
    for name in USER_SKILLS_REQUIRED:
        skill_md = base / name / "SKILL.md"
        if not skill_md.exists():
            continue
        text = skill_md.read_text(encoding="utf-8")
        for pat in CURSOR_RESIDUAL_PATTERNS:
            if pat.search(text):
                residual.append((name, pat.pattern[:50]))
                break
    if residual:
        report.add(CheckResult("Skills 已剥 Cursor 包装", False, f"残留 {len(residual)}：{residual[:5]}"))
    else:
        report.add(CheckResult("Skills 已剥 Cursor 包装", True, "12 个 skill 均已清理"))


def check_mcp_single_source(report: CheckReport) -> None:
    """.mcp.json 是单一源，不应有 .cursor/mcp.json 副本。"""
    primary = REPO_ROOT / ".mcp.json"
    duplicate = REPO_ROOT / ".cursor" / "mcp.json"
    if not primary.exists():
        report.add(CheckResult("MCP 单一源", False, ".mcp.json 不存在"))
        return
    if duplicate.exists():
        report.add(CheckResult("MCP 单一源", False, ".cursor/mcp.json 仍存在（应已随 .cursor/ 归档）"))
        return
    try:
        cfg = json.loads(primary.read_text(encoding="utf-8"))
    except Exception as e:
        report.add(CheckResult("MCP 单一源", False, f".mcp.json 解析失败：{e}"))
        return
    servers = cfg.get("mcpServers", {})
    issues = []
    for name in ("pg-etl", "pg-app"):
        if name in servers and not servers[name].get("disabled", False):
            issues.append(f"{name} 应 disabled=true")
    for name in ("pg-etl-test", "pg-app-test"):
        if name in servers and servers[name].get("disabled", False):
            issues.append(f"{name} 应 disabled=false")
    if issues:
        report.add(CheckResult("MCP 单一源", False, "; ".join(issues)))
    else:
        report.add(CheckResult("MCP 单一源", True, ".mcp.json 是唯一源，prod/test 状态正确"))


def check_history_index_present(report: CheckReport) -> None:
    p = REPO_ROOT / "docs" / "ai-env-history"
    if not p.exists():
        report.add(CheckResult("docs/ai-env-history/ 入仓", False, "目录不存在"))
        return
    file_count = sum(1 for f in p.rglob("*") if f.is_file())
    if file_count < 100:
        report.add(CheckResult("docs/ai-env-history/ 入仓", False, f"文件偏少：{file_count}"))
    else:
        report.add(CheckResult("docs/ai-env-history/ 入仓", True, f"{file_count} 个文件"))


def check_history_no_secrets(report: CheckReport) -> None:
    p = REPO_ROOT / "docs" / "ai-env-history"
    if not p.exists():
        report.add(CheckResult("历史索引敏感扫描", False, "目录不存在"))
        return
    hits = []
    for f in p.rglob("*"):
        if not f.is_file():
            continue
        try:
            text = f.read_text(encoding="utf-8", errors="replace")
        except Exception:
            continue
        for label, pat in SENSITIVE_PATTERNS.items():
            for m in pat.finditer(text):
                snippet = m.group(0)
                # 白名单：文档中描述敏感扫描正则模式自身
                if "postgresql://|postgres://" in text[max(0, m.start()-30):m.end()+30]:
                    continue
                hits.append((f.relative_to(REPO_ROOT).as_posix(), label, snippet[:80]))
    if hits:
        report.add(CheckResult("历史索引敏感扫描", False, f"{len(hits)} hits：{hits[:3]}"))
    else:
        report.add(CheckResult("历史索引敏感扫描", True, "无密钥/DSN 泄露"))


def check_cold_storage_intact(report: CheckReport) -> None:
    """冷备路径仍在（tools/cursor、tools/codex 不应被删除，因为 .mcp.json 引用 mcp-postgres.ps1）。"""
    required = [
        "tools/cursor/migrate_ai_environment.py",
        "tools/codex/mcp-postgres.ps1",
    ]
    report.add(_check_files_exist(required, REPO_ROOT, "冷备资产保留"))


def check_backup_present(report: CheckReport) -> None:
    if not BACKUP_ROOT.exists():
        report.add(CheckResult("迁移备份目录", False, f"不存在：{BACKUP_ROOT}"))
        return
    manifest = BACKUP_ROOT / "BACKUP_MANIFEST.md"
    if not manifest.exists():
        report.add(CheckResult("迁移备份目录", False, "BACKUP_MANIFEST.md 缺失"))
        return
    report.add(CheckResult("迁移备份目录", True, str(BACKUP_ROOT)))


def check_memory_untouched(report: CheckReport) -> None:
    """用户记忆目录必须存在且未被迁移触碰。"""
    mem = USER_HOME / ".claude" / "projects" / "c--Project-NeoZQYY" / "memory"
    if not mem.exists():
        report.add(CheckResult("用户记忆目录未受影响", False, f"不存在：{mem}"))
        return
    files = list(mem.glob("*.md"))
    if len(files) < 5:
        report.add(CheckResult("用户记忆目录未受影响", False, f"文件数过少：{len(files)}"))
    else:
        report.add(CheckResult("用户记忆目录未受影响", True, f"{len(files)} 个记忆文件"))


CHECKS: list[Callable[[CheckReport], None]] = [
    check_backup_present,
    check_root_claude_md,
    check_claude_md_present,
    check_agents_md_removed,
    check_cursor_removed,
    check_project_claude_assets,
    check_user_agents,
    check_user_skills,
    check_user_skills_stripped,
    check_mcp_single_source,
    check_history_index_present,
    check_history_no_secrets,
    check_cold_storage_intact,
    check_memory_untouched,
]


def main() -> int:
    parser = argparse.ArgumentParser(description="Cursor → Claude Code 反向迁移决策固化与校验")
    parser.add_argument("--check", action="store_true", help="校验当前状态（默认行为）")
    parser.add_argument("--strict", action="store_true", help="任一检查失败即退出非零")
    parser.add_argument("--json", action="store_true", help="JSON 输出（供脚本消费）")
    args = parser.parse_args()

    report = CheckReport()
    for fn in CHECKS:
        try:
            fn(report)
        except Exception as e:
            report.add(CheckResult(fn.__name__, False, f"异常：{e}"))

    if args.json:
        out = {
            "passed": report.passed_count(),
            "total": len(report.results),
            "results": [
                {"name": r.name, "passed": r.passed, "detail": r.detail}
                for r in report.results
            ],
        }
        print(json.dumps(out, ensure_ascii=False, indent=2))
    else:
        for r in report.results:
            mark = "[OK]" if r.passed else "[X] "
            print(f"{mark} {r.name}: {r.detail}")
        print()
        print(f"通过 {report.passed_count()} / {len(report.results)}")
        if report.failed():
            print("\n失败项：")
            for r in report.failed():
                print(f"  - {r.name}: {r.detail}")

    if args.strict and report.failed():
        return 1
    return 0


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