#!/usr/bin/env python3
"""
g3_independent_verifier.py — .done 생성 전 독립 검증 스크립트
보고서에 명시된 핵심 변경사항이 실제 코드에 존재하는지 확인한다.

사용법:
    python3 g3_independent_verifier.py --task-id task-1883
"""

import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path

WORKSPACE_ROOT = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")


def parse_args():
    parser = argparse.ArgumentParser(description=".done 생성 전 독립 검증 스크립트")
    parser.add_argument(
        "--task-id",
        required=True,
        help="검증할 태스크 ID (예: task-1883)",
    )
    parser.add_argument(
        "--team-id",
        required=False,
        default=None,
        help="검증할 팀 ID (예: dev1-team). 지정 시 팀 범위 검증 수행",
    )
    return parser.parse_args()


def load_report(task_id: str) -> str | None:
    """보고서 파일을 읽어 내용을 반환한다. 없으면 None."""
    report_path = Path(WORKSPACE_ROOT) / "memory" / "reports" / f"{task_id}.md"
    if not report_path.exists():
        return None
    return report_path.read_text(encoding="utf-8")


def parse_verification_table(report_content: str) -> list[dict]:
    """
    보고서에서 "수정 파일별 검증 상태" 테이블을 파싱한다.

    반환: [{"file_path": str, "description": str, "keyword": str | None, "status": str}, ...]
    테이블이 없으면 빈 리스트 반환 (Lv.2 이하 → SKIP 처리).
    """
    # 테이블 섹션 존재 여부 확인
    if "수정 파일별 검증 상태" not in report_content:
        return []

    # V-9: 해당 섹션만 추출 (다음 ## 헤더까지)
    section_start = report_content.index("수정 파일별 검증 상태")
    section_text = report_content[section_start:]
    # 다음 ## 헤더 찾기
    next_header_match = re.search(r'\n#{2,}\s', section_text[1:])  # 첫 문자 이후부터 검색
    if next_header_match:
        section_text = section_text[:next_header_match.start() + 1]

    entries = []
    # 마크다운 테이블 행 파싱: | 컬럼1 | 컬럼2 | ... |
    # 헤더행(파일, 변경 내용, grep 검증, 상태)과 구분선(---|---|...)은 스킵
    table_row_re = re.compile(r"^\|(.+)\|$")
    separator_re = re.compile(r"^\|[\s\-|]+\|$")
    # grep "키워드" OK 형식에서 키워드 추출
    keyword_re = re.compile(r'grep\s+"([^"]+)"\s+OK', re.IGNORECASE)

    # 파일 경로 유효성 검증 패턴 (V-9)
    KNOWN_PATH_PREFIXES = (
        "scripts/", "teams/", "projects/", "workers/", "utils/",
        "memory/", "tests/", "dashboard/", "src/", "lib/",
        "config/", "prompts/", "/",
    )
    FILE_EXTENSIONS = re.compile(r'\.\w{1,5}(?::\d+)?$')  # .py, .ts:42 등

    for line in section_text.splitlines():
        line = line.strip()
        if not table_row_re.match(line):
            continue
        if separator_re.match(line):
            continue

        # 셀 분리
        cells = [c.strip() for c in line.strip("|").split("|")]
        if len(cells) < 4:
            continue

        # 헤더 행 스킵 (첫 번째 셀이 "파일" 등 키워드인 경우)
        header_keywords = {"파일", "file", "파일 ", " 파일"}
        if cells[0].strip() in header_keywords or cells[0].strip().lower() == "파일":
            continue
        # 상태 셀이 "상태" 혹은 "status"이면 헤더
        if cells[3].strip().lower() in {"상태", "status"}:
            continue

        file_col = cells[0].strip()
        # V-9: 파일 경로 유효성 검증
        if not file_col.startswith(KNOWN_PATH_PREFIXES) and not FILE_EXTENSIONS.search(file_col):
            continue
        description = cells[1].strip()
        grep_col = cells[2].strip()
        status = cells[3].strip().lower()

        # 키워드 추출
        m = keyword_re.search(grep_col)
        keyword = m.group(1) if m else None

        entries.append(
            {
                "file_path": file_col,
                "description": description,
                "keyword": keyword,
                "status": status,
            }
        )

    return entries


def resolve_file_path(raw_path: str) -> Path:
    """
    파일경로:라인번호 형식에서 순수 경로를 추출하고
    상대 경로면 WORKSPACE_ROOT 기준으로 절대 경로로 변환한다.
    """
    # 마지막 콜론 이후가 숫자면 라인번호로 간주하여 제거
    colon_line_re = re.compile(r"^(.+):(\d+)$")
    m = colon_line_re.match(raw_path)
    if m:
        raw_path = m.group(1)

    p = Path(raw_path)
    if not p.is_absolute():
        p = Path(WORKSPACE_ROOT) / p
    return p


def check_planned_items(entries: list[dict]) -> tuple[str, list[str]]:
    """
    planned 상태 항목이 존재하면 FAIL.
    반환: (status, details)
    """
    planned = [e for e in entries if e.get("status") == "planned"]
    if not planned:
        return "PASS", []
    names = [e.get("file_path", e.get("description", "unknown")) for e in planned]
    detail = f"planned 항목 {len(planned)}건 발견: {names}"
    return "FAIL", [detail]


def check_team_scope(entries: list[dict], team_id: str) -> tuple[str, list[str]]:
    """
    verified 파일 경로가 해당 팀 범위 내인지 검증한다.
    범위 규칙:
      - teams/{team}/ 하위 파일은 해당 팀만 수정 가능
      - teams/shared/, scripts/, utils/, memory/, tests/, config/, prompts/ 등 공용 파일은 모든 팀 허용
    범위 밖 파일 발견 시 WARN (FAIL 아님).
    반환: (status, warnings)
    """
    # 공용 경로 접두사 (모든 팀 허용)
    SHARED_PREFIXES = (
        "teams/shared/", "scripts/", "utils/", "memory/", "tests/",
        "config/", "prompts/", "dashboard/", "src/", "lib/",
        "projects/",
    )

    warnings = []
    for entry in entries:
        raw_path = entry["file_path"]
        # 라인번호 제거
        colon_line_re = re.compile(r"^(.+):(\d+)$")
        m = colon_line_re.match(raw_path)
        clean_path = m.group(1) if m else raw_path

        # 절대 경로면 WORKSPACE_ROOT 기준 상대 경로로 변환
        if clean_path.startswith("/"):
            ws_root = WORKSPACE_ROOT.rstrip("/") + "/"
            if clean_path.startswith(ws_root):
                clean_path = clean_path[len(ws_root):]
            else:
                # WORKSPACE_ROOT 밖 절대 경로 → 범위 밖
                warnings.append(f"team_scope: {raw_path} is outside WORKSPACE_ROOT")
                continue

        # 공용 접두사 체크
        if any(clean_path.startswith(prefix) for prefix in SHARED_PREFIXES):
            continue  # 공용 파일 → 허용

        # teams/ 하위 파일인지 체크
        if clean_path.startswith("teams/"):
            # teams/{team_name}/... 형식에서 team_name 추출
            parts = clean_path.split("/")
            if len(parts) >= 2:
                file_team = parts[1]  # e.g., "dev1", "dev2"
                # team_id에서 팀 단축명 추출 (dev1-team → dev1, dev2-team → dev2)
                team_short = team_id.replace("-team", "")
                if file_team != team_short:
                    warnings.append(f"team_scope: {raw_path} belongs to team '{file_team}', not '{team_short}'")
            continue

        # teams/ 이외 최상위 파일 → 공용으로 간주 (허용)

    if warnings:
        return "WARN", warnings
    return "PASS", []


def check_file_existence(entries: list[dict]) -> tuple[str, list[str]]:
    """
    각 항목의 파일이 실제로 존재하는지 확인한다.
    반환: (status, missing_files)
    """
    missing = []
    for entry in entries:
        path = resolve_file_path(entry["file_path"])
        if not path.exists():
            missing.append(str(entry["file_path"]))

    status = "FAIL" if missing else "PASS"
    return status, missing


def run_grep_verification(entries: list[dict]) -> tuple[str, list[str]]:
    """
    verified 상태인 항목에 대해 grep -c "키워드" 파일경로를 실행한다.
    0건이면 FAIL.
    반환: (status, failed_items)
    """
    failed = []
    for entry in entries:
        if entry["status"] != "verified":
            continue
        keyword = entry["keyword"]
        if not keyword:
            continue

        file_path = resolve_file_path(entry["file_path"])
        if not file_path.exists():
            # 파일 없음은 file_existence에서 이미 처리
            continue

        try:
            result = subprocess.run(
                ["grep", "-c", keyword, str(file_path)],
                capture_output=True,
                text=True,
                timeout=30,
            )
            count_str = result.stdout.strip()
            count = int(count_str) if count_str.isdigit() else 0
            if count == 0:
                failed.append(f"grep_verification: '{keyword}' not found in {entry['file_path']}")
        except subprocess.TimeoutExpired:
            failed.append(f"grep_verification: timeout while searching '{keyword}' in {entry['file_path']}")
        except Exception as e:
            failed.append(f"grep_verification: error searching '{keyword}' in {entry['file_path']}: {e}")

    status = "FAIL" if failed else "PASS"
    return status, failed


def find_test_files(task_id: str) -> list[Path]:
    """
    태스크 ID에서 숫자 키를 추출하여 관련 테스트 파일을 탐색한다.
    패턴: tests/test_*{key}*.py
    """
    # task-1883 → 1883
    key_match = re.search(r"(\d+)", task_id)
    if not key_match:
        return []
    key = key_match.group(1)

    tests_dir = Path(WORKSPACE_ROOT) / "tests"
    if not tests_dir.exists():
        return []

    pattern = f"test_*{key}*.py"
    found = list(tests_dir.glob(pattern))
    return found


def run_pytest(test_files: list[Path]) -> tuple[str, str]:
    """
    pytest를 실행한다.
    반환: (status, reason)
    """
    if not test_files:
        return "SKIP", "no test files found"

    cmd = ["python3", "-m", "pytest"] + [str(f) for f in test_files] + ["--tb=short", "-q"]
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30,
            cwd=WORKSPACE_ROOT,
        )
        if result.returncode != 0:
            output = (result.stdout + result.stderr).strip()
            reason = output[-500:] if len(output) > 500 else output
            return "FAIL", reason
        return "PASS", ""
    except subprocess.TimeoutExpired:
        return "FAIL", "pytest timeout (30s)"
    except Exception as e:
        return "FAIL", f"pytest error: {e}"


def parse_report_test_results(report_content: str) -> dict | None:
    """
    보고서에서 'N passed, M failed' 패턴을 추출.
    반환: {"passed": int, "failed": int} 또는 None (패턴 없으면)
    """
    # 패턴: "X passed" 또는 "X passed, Y failed" 또는 "X passed, Y failed in Z.ZZs"
    passed_match = re.search(r"(\d+)\s+passed", report_content)
    failed_match = re.search(r"(\d+)\s+failed", report_content)

    if not passed_match:
        return None  # 테스트 결과 패턴 없음

    return {
        "passed": int(passed_match.group(1)),
        "failed": int(failed_match.group(1)) if failed_match else 0,
    }


def parse_actual_pytest_output(output: str) -> dict | None:
    """pytest 출력에서 passed/failed 수를 추출."""
    passed_match = re.search(r"(\d+)\s+passed", output)
    failed_match = re.search(r"(\d+)\s+failed", output)

    if not passed_match and not failed_match:
        return None

    return {
        "passed": int(passed_match.group(1)) if passed_match else 0,
        "failed": int(failed_match.group(1)) if failed_match else 0,
    }


def cross_verify_test_results(report_content: str, task_id: str) -> tuple[str, dict]:
    """
    보고서의 테스트 결과와 실제 pytest 실행 결과를 비교.
    반환: (status, check_details)
    """
    report_results = parse_report_test_results(report_content)
    if report_results is None:
        return "SKIP", {"reason": "보고서에 테스트 결과 패턴 없음"}

    # 실제 pytest 실행
    test_files = find_test_files(task_id)
    if not test_files:
        return "SKIP", {"reason": "관련 테스트 파일 없음"}

    cmd = ["python3", "-m", "pytest"] + [str(f) for f in test_files] + ["--tb=short", "-q"]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, cwd=WORKSPACE_ROOT)
        actual_output = result.stdout + result.stderr
    except (subprocess.TimeoutExpired, Exception) as e:
        return "SKIP", {"reason": f"pytest 실행 실패: {e}"}

    actual_results = parse_actual_pytest_output(actual_output)
    if actual_results is None:
        return "SKIP", {"reason": "pytest 출력 파싱 실패"}

    # 비교
    report_failed = report_results["failed"]
    actual_failed = actual_results["failed"]
    report_passed = report_results["passed"]
    actual_passed = actual_results["passed"]

    mismatches = []
    if report_failed != actual_failed:
        mismatches.append(f"failed: 보고서={report_failed}, 실제={actual_failed}")
    if report_passed != actual_passed:
        mismatches.append(f"passed: 보고서={report_passed}, 실제={actual_passed}")

    if mismatches:
        detail_msg = f"보고서 테스트 결과 불일치: {'; '.join(mismatches)}"
        return "FAIL", {
            "report": report_results,
            "actual": actual_results,
            "detail": detail_msg,
        }

    return "PASS", {
        "report": report_results,
        "actual": actual_results,
    }


def write_fail_file(task_id: str, timestamp: str, fail_reasons: list[str]):
    """FAIL 시 memory/events/{task_id}.g3-fail 파일에 차단 사유를 기록한다."""
    events_dir = Path(WORKSPACE_ROOT) / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)

    fail_path = events_dir / f"{task_id}.g3-fail"
    data = {
        "task_id": task_id,
        "timestamp": timestamp,
        "fail_reasons": fail_reasons,
    }
    fail_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")


def check_report_quality(report_content: str) -> tuple[str, list[str]]:
    """
    보고서 최소 품질 게이트 (V-5).
    SCQA 패턴 2개 이상 존재 + 200자 이상 분량 확인.
    """
    issues = []

    # SCQA 패턴 확인 (S, C, Q, A 각각에 대해 존재 여부)
    scqa_patterns = {
        'S': [r'\*\*S\*\*\s*:', r'#+\s*S\s*[-–—]'],
        'C': [r'\*\*C\*\*\s*:', r'#+\s*C\s*[-–—]'],
        'Q': [r'\*\*Q\*\*\s*:', r'#+\s*Q\s*[-–—]'],
        'A': [r'\*\*A\*\*\s*:', r'#+\s*A\s*[-–—]'],
    }

    found_scqa = set()
    for letter, patterns in scqa_patterns.items():
        for pattern in patterns:
            if re.search(pattern, report_content):
                found_scqa.add(letter)
                break

    if len(found_scqa) < 2:
        issues.append(f"SCQA 패턴 부족: {len(found_scqa)}개 발견 (최소 2개 필요, 발견: {sorted(found_scqa)})")

    # 최소 분량 확인
    content_length = len(report_content)
    if content_length < 200:
        issues.append(f"보고서 분량 부족: {content_length}자 (최소 200자 필요)")

    status = "FAIL" if issues else "PASS"
    return status, issues


def check_micro_commits(entries: list[dict], task_id: str) -> tuple[str, list[str]]:
    """수정 파일 5개 이상인데 커밋 1개 이하면 WARN."""
    verified_count = sum(1 for e in entries if e.get("status") == "verified")
    if verified_count < 5:
        return "PASS", []

    try:
        result = subprocess.run(
            ["git", "log", "--oneline", f"--grep={task_id}"],
            capture_output=True, text=True, timeout=10,
            cwd=WORKSPACE_ROOT,
        )
        commit_count = len([line for line in result.stdout.strip().splitlines() if line])
        if commit_count <= 1:
            return "WARN", [f"micro_commit: {verified_count}개 파일 수정, 커밋 {commit_count}개 (5개+ 파일 시 다중 커밋 권장)"]
    except Exception:
        return "PASS", []  # git 실패 시 스킵

    return "PASS", []


def check_gemini_review(report_content: str) -> tuple[str, list[str]]:
    """보고서의 Gemini 리뷰 결과와 실제 PR 코멘트를 교차검증한다."""
    # PR 번호 추출
    pr_match = re.search(r"PR\s*#(\d+)|/pulls?/(\d+)", report_content, re.IGNORECASE)
    if not pr_match:
        return "SKIP", ["보고서에서 PR 번호 추출 실패"]
    pr_number = pr_match.group(1) or pr_match.group(2)

    # repo 정보 추출
    repo_match = re.search(r"github\.com/([^/\s]+)/([^/\s#]+)", report_content)
    if not repo_match:
        return "SKIP", ["보고서에서 repo 정보 추출 실패"]
    owner, repo = repo_match.group(1), repo_match.group(2).rstrip("/")

    # 보고서의 High 건수 추출
    high_match = re.search(r"High\s+(\d+)\s*건", report_content, re.IGNORECASE)
    report_high = int(high_match.group(1)) if high_match else None

    # 실제 PR 코멘트 조회
    try:
        result = subprocess.run(
            ["gh", "api", f"repos/{owner}/{repo}/pulls/{pr_number}/comments"],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode != 0:
            return "SKIP", [f"PR #{pr_number} 코멘트 조회 실패"]
        comments = json.loads(result.stdout)
    except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
        return "SKIP", ["PR 코멘트 조회 중 오류"]

    # Gemini 코멘트 severity 파싱
    _high_re = re.compile(r"!\[(security-critical|critical|high)\]", re.IGNORECASE)
    actual_high = 0
    for c in comments:
        user = c.get("user", {}).get("login", "")
        if "gemini" not in user.lower():
            continue
        body = c.get("body", "")
        body_lower = body.lower()
        if (
            "severity: high" in body_lower
            or "severity: critical" in body_lower
            or "HIGH" in body
            or "CRITICAL" in body
            or _high_re.search(body)
            or "high-priority.svg" in body_lower
            or "critical.svg" in body_lower
        ):
            actual_high += 1

    details = [f"PR #{pr_number}: 실제 Gemini High={actual_high}건"]

    if report_high is not None and report_high != actual_high:
        details.append(f"불일치: 보고서 High={report_high}건, 실제 High={actual_high}건")
        return "FAIL", details

    if report_high is None and actual_high > 0:
        details.append(f"보고서에 High 건수 미명시, 실제 High={actual_high}건 발견")
        return "FAIL", details

    return "PASS", details


def check_three_step_why(task_id: str) -> tuple[str, list[str]]:
    """context-notes.md에 3 Step Why 기록이 있는지 확인. 없으면 WARN."""
    context_path = Path(WORKSPACE_ROOT) / "memory" / "plans" / "tasks" / task_id / "context-notes.md"
    if not context_path.exists():
        return "SKIP", ["context-notes.md 파일 없음 (Lv.2 이하 또는 3문서 미생성)"]

    content = context_path.read_text(encoding="utf-8")
    has_why = bool(re.search(r"1st\s+Why|2nd\s+Why|3rd\s+Why", content, re.IGNORECASE))
    if not has_why:
        return "WARN", ["three_step_why: context-notes.md에 '1st Why/2nd Why/3rd Why' 패턴 없음. 3 Step Why 자문 기록 필요."]

    return "PASS", []


def main():
    args = parse_args()
    task_id = args.task_id
    timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")

    fail_reasons: list[str] = []

    # ------------------------------------------------------------------
    # 1. 보고서 파싱
    # ------------------------------------------------------------------
    report_content = load_report(task_id)
    if report_content is None:
        result = {
            "task_id": task_id,
            "overall": "FAIL",
            "timestamp": timestamp,
            "checks": {
                "report_parse": {
                    "status": "FAIL",
                    "entries_found": 0,
                    "error": f"report not found: memory/reports/{task_id}.md",
                },
                "file_existence": {"status": "SKIP"},
                "grep_verification": {"status": "SKIP"},
                "pytest_execution": {"status": "SKIP"},
            },
            "fail_reasons": [f"report not found: memory/reports/{task_id}.md"],
        }
        print(json.dumps(result, ensure_ascii=False, indent=2))
        write_fail_file(task_id, timestamp, result["fail_reasons"])
        sys.exit(1)

    entries = parse_verification_table(report_content)

    # 테이블 없을 때: 최소 품질 게이트 적용 (V-5)
    if not entries:
        rq_status, rq_issues = check_report_quality(report_content)
        if rq_status == "FAIL":
            result = {
                "task_id": task_id,
                "overall": "FAIL",
                "timestamp": timestamp,
                "checks": {
                    "report_parse": {
                        "status": "SKIP",
                        "entries_found": 0,
                        "reason": "no '수정 파일별 검증 상태' table found",
                    },
                    "report_quality": {
                        "status": "FAIL",
                        "issues": rq_issues,
                    },
                    "file_existence": {"status": "SKIP"},
                    "grep_verification": {"status": "SKIP"},
                    "pytest_execution": {"status": "SKIP"},
                },
                "fail_reasons": rq_issues,
            }
            print(json.dumps(result, ensure_ascii=False, indent=2))
            write_fail_file(task_id, timestamp, rq_issues)
            sys.exit(1)

        # 품질 게이트 통과 → 기존처럼 PASS
        result = {
            "task_id": task_id,
            "overall": "PASS",
            "timestamp": timestamp,
            "checks": {
                "report_parse": {
                    "status": "SKIP",
                    "entries_found": 0,
                    "reason": "no '수정 파일별 검증 상태' table found (Lv.2 or below)",
                },
                "report_quality": {
                    "status": "PASS",
                    "issues": [],
                },
                "file_existence": {"status": "SKIP"},
                "grep_verification": {"status": "SKIP"},
                "pytest_execution": {"status": "SKIP"},
            },
            "fail_reasons": [],
        }
        print(json.dumps(result, ensure_ascii=False, indent=2))
        sys.exit(0)

    report_parse_check = {"status": "PASS", "entries_found": len(entries)}

    # ------------------------------------------------------------------
    # 1.5. planned 항목 검출
    # ------------------------------------------------------------------
    pc_status, pc_details = check_planned_items(entries)
    planned_check = {
        "status": pc_status,
        "planned_count": len([e for e in entries if e.get("status") == "planned"]),
        "details": pc_details,
    }
    if pc_status == "FAIL":
        fail_reasons.extend(pc_details)

    # ------------------------------------------------------------------
    # 1.6. 팀 범위 검증 (V-4)
    # ------------------------------------------------------------------
    team_scope_check: dict = {"status": "SKIP", "reason": "no --team-id specified"}
    if args.team_id:
        ts_status, ts_warnings = check_team_scope(entries, args.team_id)
        team_scope_check = {
            "status": ts_status,
            "team_id": args.team_id,
            "warnings": ts_warnings,
        }
        # WARN은 fail_reasons에 추가하지 않음 (FAIL이 아니므로)

    # ------------------------------------------------------------------
    # 2. 파일 존재 확인
    # ------------------------------------------------------------------
    fe_status, missing_files = check_file_existence(entries)
    file_existence_check = {
        "status": fe_status,
        "checked": len(entries),
        "missing": missing_files,
    }
    if fe_status == "FAIL":
        for f in missing_files:
            fail_reasons.append(f"file_existence: {f} not found")

    # ------------------------------------------------------------------
    # 3. grep 재검증
    # ------------------------------------------------------------------
    gv_status, grep_failed = run_grep_verification(entries)
    grep_verification_check = {
        "status": gv_status,
        "checked": sum(1 for e in entries if e["status"] == "verified" and e["keyword"]),
        "failed": grep_failed,
    }
    if gv_status == "FAIL":
        fail_reasons.extend(grep_failed)

    # ------------------------------------------------------------------
    # 4. pytest 실행
    # ------------------------------------------------------------------
    test_files = find_test_files(task_id)
    pt_status, pt_reason = run_pytest(test_files)
    pytest_check: dict = {"status": pt_status}
    if pt_status == "SKIP":
        pytest_check["reason"] = pt_reason
    elif pt_status == "FAIL":
        pytest_check["reason"] = pt_reason
        fail_reasons.append(f"pytest_execution: {pt_reason[:200]}")
    # PASS일 때는 reason 불필요

    # ------------------------------------------------------------------
    # 4.5. 보고서 테스트 결과 교차 검증
    # ------------------------------------------------------------------
    cv_status, cv_details = cross_verify_test_results(report_content, task_id)
    cross_verify_check = {"status": cv_status}
    cross_verify_check.update(cv_details)
    if cv_status == "FAIL":
        fail_reasons.append(f"cross_verify: {cv_details.get('detail', 'mismatch')}")

    # ------------------------------------------------------------------
    # 4.6. Micro-commit 검증 (WARN만)
    # ------------------------------------------------------------------
    mc_status, mc_warnings = check_micro_commits(entries, task_id)
    micro_commit_check = {"status": mc_status, "warnings": mc_warnings}

    # ------------------------------------------------------------------
    # 4.7. Gemini PR 리뷰 교차검증
    # ------------------------------------------------------------------
    gemini_check: dict = {"status": "SKIP", "reason": "no gemini review info in report"}
    if report_content and "gemini" in report_content.lower():
        gv_status, gv_details = check_gemini_review(report_content)
        gemini_check = {"status": gv_status, "details": gv_details}
        if gv_status == "FAIL":
            for d in gv_details:
                fail_reasons.append(f"gemini_review: {d}")

    # ------------------------------------------------------------------
    # 4.8. 3 Step Why 기록 검증 (WARN만)
    # ------------------------------------------------------------------
    tswhy_status, tswhy_warnings = check_three_step_why(task_id)
    three_step_why_check = {"status": tswhy_status, "warnings": tswhy_warnings}

    # ------------------------------------------------------------------
    # 5. 결과 종합
    # ------------------------------------------------------------------
    overall = "FAIL" if fail_reasons else "PASS"

    all_warnings = mc_warnings + tswhy_warnings

    result = {
        "task_id": task_id,
        "overall": overall,
        "timestamp": timestamp,
        "checks": {
            "report_parse": report_parse_check,
            "planned_check": planned_check,
            "team_scope": team_scope_check,
            "file_existence": file_existence_check,
            "grep_verification": grep_verification_check,
            "pytest_execution": pytest_check,
            "cross_verify": cross_verify_check,
            "gemini_review": gemini_check,
            "micro_commit": micro_commit_check,
            "three_step_why": three_step_why_check,
        },
        "fail_reasons": fail_reasons,
        "warnings": all_warnings,
    }

    print(json.dumps(result, ensure_ascii=False, indent=2))

    if overall == "FAIL":
        write_fail_file(task_id, timestamp, fail_reasons)
        sys.exit(1)

    sys.exit(0)


if __name__ == "__main__":
    main()
