#!/usr/bin/env python3
"""
校验 user-story.csv 的结构、状态、依赖和证据路径。

用法：
  python3 validate_user_stories.py docs/00-discovery/user-story.csv
  python3 validate_user_stories.py --file docs/00-discovery/user-story.csv --check-paths
  python3 validate_user_stories.py --file ... --strict   # 警告也当错误

检查项：
1. 必需列是否齐全
2. story_id 是否唯一、非空
3. priority 是否合法
4. status 是否合法
5. 依赖是否指向存在的故事
6. 是否存在循环依赖
7. 是否自依赖
8. 已完成故事是否缺失报告/manifest（--check-paths）
9. 是否有重复列
10. 是否有空行/全空行
"""

from __future__ import annotations

import argparse
import csv
import sys
from pathlib import Path

REQUIRED_COLUMNS = [
    "story_id", "module", "title", "as_a", "i_want", "so_that",
    "priority", "status", "owner", "branch", "spec_ref", "details_ref",
    "api_ref", "db_tables", "feature_report", "e2e_manifest",
    "unit_test_status", "integ_test_status",
    "dev_url", "staging_url", "prod_url",
    "dev_deploy_status", "staging_deploy_status", "prod_deploy_status",
    "estimate_sp", "actual_sp", "dependencies", "last_updated", "notes",
]

VALID_PRIORITIES = {"P0", "P1", "P2", "P3"}
VALID_STATUSES = {
    "draft", "ready", "designing", "dev", "unit_done", "integ_done",
    "qa_pass", "deployed_dev", "deployed_prod", "verified_prod",
    "blocked", "failed", "cancelled",
}
TEST_STATUSES = {"pending", "pass", "fail"}
DEPLOY_STATUSES = {"not_deployed", "deployed", "failed"}

STATUSES_REQUIRING_REPORT = {
    "unit_done", "integ_done", "qa_pass",
    "deployed_dev", "deployed_prod", "verified_prod",
}
STATUSES_REQUIRING_MANIFEST = {
    "qa_pass", "deployed_dev", "deployed_prod", "verified_prod",
}


class Report:
    def __init__(self) -> None:
        self.errors: list[str] = []
        self.warnings: list[str] = []

    def error(self, msg: str) -> None:
        self.errors.append(msg)

    def warn(self, msg: str) -> None:
        self.warnings.append(msg)

    def print(self) -> None:
        for w in self.warnings:
            print(f"⚠️  {w}")
        for e in self.errors:
            print(f"❌ {e}")
        print()
        print(f"错误：{len(self.errors)}，警告：{len(self.warnings)}")


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    p = argparse.ArgumentParser(description="校验 user-story.csv")
    p.add_argument("file", nargs="?", type=Path,
                   default=Path("docs/00-discovery/user-story.csv"))
    p.add_argument("--root", type=Path, default=Path("."),
                   help="检查路径时的仓库根目录")
    p.add_argument("--check-paths", action="store_true",
                   help="检查 feature_report / e2e_manifest 是否存在")
    p.add_argument("--strict", action="store_true", help="警告也视为错误")
    return p.parse_args(argv)


def split_deps(value: str) -> list[str]:
    # 允许 ; , 空格 分隔
    normalized = value.replace(",", ";").replace(" ", ";")
    return [x.strip() for x in normalized.split(";") if x.strip()]


def detect_cycles(graph: dict[str, list[str]], report: Report) -> None:
    WHITE, GRAY, BLACK = 0, 1, 2
    color: dict[str, int] = {node: WHITE for node in graph}
    stack: list[str] = []

    def dfs(node: str) -> None:
        color[node] = GRAY
        stack.append(node)
        for dep in graph.get(node, []):
            if dep not in graph:
                continue
            if color[dep] == GRAY:
                idx = stack.index(dep)
                cycle = stack[idx:] + [dep]
                report.error("循环依赖：" + " -> ".join(cycle))
            elif color[dep] == WHITE:
                dfs(dep)
        stack.pop()
        color[node] = BLACK

    for node in graph:
        if color[node] == WHITE:
            dfs(node)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    report = Report()

    if not args.file.exists():
        print(f"❌ 文件不存在：{args.file}", file=sys.stderr)
        return 2

    with args.file.open("r", encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        if reader.fieldnames is None:
            print("❌ CSV 没有表头", file=sys.stderr)
            return 2
        fieldnames = list(reader.fieldnames)
        rows = list(reader)

    # 1. 列检查
    missing = [c for c in REQUIRED_COLUMNS if c not in fieldnames]
    if missing:
        report.error("缺少列：" + ", ".join(missing))
    duplicates = {c for c in fieldnames if fieldnames.count(c) > 1}
    if duplicates:
        report.error("重复列：" + ", ".join(sorted(duplicates)))
    extra = [c for c in fieldnames if c not in REQUIRED_COLUMNS]
    if extra:
        report.warn("额外列（不一定是问题）：" + ", ".join(extra))

    # 2. 行检查
    ids: list[str] = []
    by_id: dict[str, dict] = {}
    for lineno, row in enumerate(rows, start=2):
        sid = (row.get("story_id") or "").strip()
        if not sid:
            report.error(f"第 {lineno} 行：story_id 为空")
            continue
        if sid in by_id:
            report.error(f"第 {lineno} 行：story_id 重复：{sid}")
        ids.append(sid)
        by_id[sid] = row

    for sid, row in by_id.items():
        priority = (row.get("priority") or "").strip()
        if priority not in VALID_PRIORITIES:
            report.error(f"{sid}：非法 priority {priority!r}，应为 {sorted(VALID_PRIORITIES)}")

        status = (row.get("status") or "").strip()
        if status not in VALID_STATUSES:
            report.error(f"{sid}：非法 status {status!r}，应为 {sorted(VALID_STATUSES)}")

        for col in ("unit_test_status", "integ_test_status"):
            value = (row.get(col) or "").strip()
            if value and value not in TEST_STATUSES:
                report.error(f"{sid}：{col} 非法值 {value!r}")

        for col in ("dev_deploy_status", "staging_deploy_status", "prod_deploy_status"):
            value = (row.get(col) or "").strip()
            if value and value not in DEPLOY_STATUSES:
                report.error(f"{sid}：{col} 非法值 {value!r}")

        deps = split_deps(row.get("dependencies") or "")
        if sid in deps:
            report.error(f"{sid}：依赖了自己")
        for dep in deps:
            if dep not in by_id:
                report.error(f"{sid}：依赖不存在的故事 {dep}")

        if args.check_paths:
            report_path = (row.get("feature_report") or "").strip()
            manifest_path = (row.get("e2e_manifest") or "").strip()
            if status in STATUSES_REQUIRING_REPORT:
                if not report_path:
                    report.error(f"{sid}：状态 {status} 但 feature_report 为空")
                elif not (args.root / report_path).exists():
                    report.error(f"{sid}：feature_report 不存在：{report_path}")
            if status in STATUSES_REQUIRING_MANIFEST:
                if not manifest_path:
                    report.error(f"{sid}：状态 {status} 但 e2e_manifest 为空")
                elif not (args.root / manifest_path).exists():
                    report.error(f"{sid}：e2e_manifest 不存在：{manifest_path}")

    # 3. 循环依赖
    graph = {sid: split_deps(row.get("dependencies") or "") for sid, row in by_id.items()}
    detect_cycles(graph, report)

    # 4. 汇总
    print(f"文件：{args.file}")
    print(f"故事数：{len(by_id)}")
    print()
    report.print()

    if report.errors:
        return 1
    if args.strict and report.warnings:
        return 1
    return 0


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