#!/usr/bin/env python3
"""
qc_report_guard.py — 보고서 정직성 강제 (Guard MVP Phase 1)

qc-result JSON ↔ 보고서 verdict 불일치 차단.
task-2431 사고: qc_result=WARN 인데 보고서=OVERALL PASS → 여기서 FAIL.

Source-of-truth 룰:
  우선 1: YAML frontmatter qc_verdict 필드
  우선 2: ## QC Verdict 섹션 직속 한 줄
  둘 다 없으면 → FAIL (보고서 정직성 강제)
"""

import argparse
import json
import os
import re
import sys


# ── verdict 정규화 ────────────────────────────────────────────────────────────

# 허용 verdict 표현 → 내부 표준값
_VERDICT_NORMALIZE: dict[str, str] = {
    "PASS":           "PASS",
    "OVERALL PASS":   "PASS",
    "WARN":           "WARN",
    "OVERALL WARN":   "WARN",
    "PASS_WITH_WARN": "PASS_WITH_WARN",
    "PASS with WARN": "PASS_WITH_WARN",
    "PASS WITH WARN": "PASS_WITH_WARN",
    "FAIL":           "FAIL",
    "OVERALL FAIL":   "FAIL",
}

_FRONTMATTER_KEY_RE = re.compile(r"^qc_verdict\s*:\s*(.+)$", re.MULTILINE)
_QC_VERDICT_SECTION_RE = re.compile(r"^##\s+QC Verdict\s*$", re.MULTILINE)


def _is_in_code_block(text: str, pos: int) -> bool:
    """pos 위치가 코드 블록(```...) 안에 있으면 True."""
    before = text[:pos]
    # 홀수 번 ``` 등장 → 코드 블록 안
    count = before.count("```")
    return (count % 2) == 1


def _is_in_quote_block(line: str) -> bool:
    """인용 블록(> 로 시작) 또는 특정 prefix 패턴이면 True."""
    stripped = line.lstrip()
    if stripped.startswith(">"):
        return True
    # "이전 표현:", "정정된 표현:" 등 정정 prefix
    if re.match(r"^(이전 표현|정정된 표현|이전|정정)\s*:", stripped):
        return True
    return False


def _extract_frontmatter_verdict(text: str) -> str | None:
    """YAML frontmatter에서 qc_verdict 값 추출."""
    # frontmatter는 파일 맨 앞 --- ... --- 블록
    fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
    if not fm_match:
        return None
    fm_text = fm_match.group(1)
    m = _FRONTMATTER_KEY_RE.search(fm_text)
    if not m:
        return None
    raw = m.group(1).strip().strip('"').strip("'")
    return raw if raw else None


def _extract_section_verdict(text: str) -> str | None:
    """## QC Verdict 섹션 직속 첫 번째 비어있지 않은 줄 추출 (인용/코드 블록 제외)."""
    m = _QC_VERDICT_SECTION_RE.search(text)
    if not m:
        return None
    section_start = m.end()
    # 섹션 이후 줄들을 순회하여 첫 번째 유효 줄 찾기
    remaining = text[section_start:]
    for line in remaining.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        # 다음 섹션 헤더면 중단
        if stripped.startswith("#"):
            break
        # 인용/코드 무시
        if _is_in_quote_block(line):
            continue
        # 코드 블록 안인지: section_start 상대 위치로 판단
        pos_in_text = section_start + remaining.index(line)
        if _is_in_code_block(text, pos_in_text):
            continue
        return stripped
    return None


def _normalize_verdict(raw: str | None) -> str | None:
    """raw verdict 문자열 → 표준값 (PASS/WARN/FAIL). 매칭 안 되면 None."""
    if raw is None:
        return None
    cleaned = raw.strip()
    return _VERDICT_NORMALIZE.get(cleaned)


def extract_report_verdict(report_text: str) -> tuple[str | None, str]:
    """
    보고서에서 verdict 추출.
    반환: (normalized_verdict, source_label)
    source_label: "frontmatter" | "## QC Verdict" | "none"
    """
    fm_raw = _extract_frontmatter_verdict(report_text)
    sec_raw = _extract_section_verdict(report_text)

    fm_norm = _normalize_verdict(fm_raw)
    sec_norm = _normalize_verdict(sec_raw)

    if fm_norm and sec_norm:
        # 둘 다 있으면 일치해야 PASS — 불일치는 호출자에서 처리
        # 일단 frontmatter 우선 반환, source에 불일치 기록
        if fm_norm != sec_norm:
            return None, f"frontmatter/section 불일치 (fm={fm_raw}, sec={sec_raw})"
        return fm_norm, "frontmatter+## QC Verdict"
    elif fm_norm:
        return fm_norm, "frontmatter"
    elif sec_norm:
        return sec_norm, "## QC Verdict"
    else:
        return None, "none"


def _find_warn_fail_keys(checks_summary: dict) -> list[str]:
    """checks_summary에서 WARN/FAIL 상태 키 목록 반환."""
    return [k for k, v in checks_summary.items() if v in ("WARN", "FAIL")]


def _check_keys_in_report(keys: list[str], report_text: str) -> list[str]:
    """키 중 보고서 본문(인용/코드 외)에서 등장하지 않는 것 반환."""
    missing: list[str] = []
    lines = report_text.splitlines()
    in_code = False
    for key in keys:
        found = False
        in_code = False
        for line in lines:
            # 코드 블록 토글
            if line.strip().startswith("```"):
                in_code = not in_code
                continue
            if in_code:
                continue
            if _is_in_quote_block(line):
                continue
            if key in line:
                found = True
                break
        if not found:
            missing.append(key)
    return missing


# ── 핵심 check 함수 (pre_push_guard에서 import 사용) ─────────────────────────

def check(
    task_id: str,
    workspace: str = "/home/jay/workspace",
    report_path: str | None = None,
    qc_result_path: str | None = None,
) -> dict:
    """
    반환:
      {
        "ok": bool,
        "violations": list[str],
        "json_verdict": str,
        "report_verdict": str | None,
        "missing_warns": list[str],
      }
    """
    violations: list[str] = []
    json_verdict = "UNKNOWN"
    report_verdict: str | None = None
    missing_warns: list[str] = []

    # ── 기본 경로 ──
    if qc_result_path is None:
        qc_result_path = os.path.join(workspace, "memory", "events", f"{task_id}.qc-result")
    if report_path is None:
        report_path = os.path.join(workspace, "memory", "reports", f"{task_id}.md")

    # ── 1. qc-result JSON 읽기 ──
    if not os.path.exists(qc_result_path):
        return {
            "ok": False,
            "violations": [f"qc-result 파일 없음: {qc_result_path}"],
            "json_verdict": "MISSING",
            "report_verdict": None,
            "missing_warns": [],
        }

    try:
        with open(qc_result_path, encoding="utf-8") as f:
            qc_data = json.load(f)
    except Exception as e:
        return {
            "ok": False,
            "violations": [f"qc-result JSON 파싱 실패: {e}"],
            "json_verdict": "PARSE_ERROR",
            "report_verdict": None,
            "missing_warns": [],
        }

    json_verdict = qc_data.get("qc_result", "UNKNOWN")
    checks_summary: dict = qc_data.get("checks_summary", {})

    # ── 2. 보고서 읽기 ──
    if not os.path.exists(report_path):
        violations.append(f"보고서 파일 없음: {report_path}")
        return {
            "ok": False,
            "violations": violations,
            "json_verdict": json_verdict,
            "report_verdict": None,
            "missing_warns": [],
        }

    try:
        with open(report_path, encoding="utf-8") as f:
            report_text = f.read()
    except Exception as e:
        return {
            "ok": False,
            "violations": [f"보고서 읽기 실패: {e}"],
            "json_verdict": json_verdict,
            "report_verdict": None,
            "missing_warns": [],
        }

    # ── 3. verdict 추출 ──
    report_verdict, source_label = extract_report_verdict(report_text)

    if report_verdict is None:
        violations.append(
            f"verdict 마커 누락 — 보고서 정직성 강제 (source={source_label})"
        )
        return {
            "ok": False,
            "violations": violations,
            "json_verdict": json_verdict,
            "report_verdict": None,
            "missing_warns": [],
        }

    # ── 4. JSON ↔ report 매칭 ──
    # json_verdict 정규화
    json_norm = _VERDICT_NORMALIZE.get(json_verdict, json_verdict)

    norm_pair = (json_norm, report_verdict)
    ok_pairs = {
        ("PASS", "PASS"),
        ("WARN", "WARN"),
        ("WARN", "PASS_WITH_WARN"),
        ("FAIL", "FAIL"),
    }
    sago_pair = ("WARN", "PASS")  # ★ task-2431 사고
    if norm_pair == sago_pair:
        violations.append(
            f"MISMATCH (★ 사고 차단): JSON={json_verdict} 인데 보고서={report_verdict} (OVERALL PASS)"
        )
    elif norm_pair not in ok_pairs:
        violations.append(
            f"MISMATCH: JSON={json_verdict} vs 보고서={report_verdict}"
        )

    # ── 5. WARN/FAIL 항목 누락 검사 ──
    if json_norm in ("WARN", "FAIL") and checks_summary:
        warn_fail_keys = _find_warn_fail_keys(checks_summary)
        if warn_fail_keys:
            missing_warns = _check_keys_in_report(warn_fail_keys, report_text)
            if missing_warns:
                violations.append(
                    f"WARN/FAIL 항목 보고서 누락: {missing_warns}"
                )

    ok = len(violations) == 0
    return {
        "ok": ok,
        "violations": violations,
        "json_verdict": json_verdict,
        "report_verdict": report_verdict,
        "missing_warns": missing_warns,
    }


# ── CLI ───────────────────────────────────────────────────────────────────────

def main() -> None:
    parser = argparse.ArgumentParser(
        description="qc_report_guard.py — 보고서 정직성 강제 (Guard MVP Phase 1)"
    )
    parser.add_argument("--task-id", required=True, help="task ID (예: task-2434)")
    parser.add_argument("--workspace", default="/home/jay/workspace",
                        help="워크스페이스 루트 (기본: /home/jay/workspace)")
    parser.add_argument("--report-path", default=None,
                        help="보고서 경로 (기본: <ws>/memory/reports/<task_id>.md)")
    parser.add_argument("--qc-result-path", default=None,
                        help="qc-result 경로 (기본: <ws>/memory/events/<task_id>.qc-result)")
    args = parser.parse_args()

    result = check(
        task_id=args.task_id,
        workspace=args.workspace,
        report_path=args.report_path,
        qc_result_path=args.qc_result_path,
    )

    # 보고서 source 재계산 (출력용)
    report_path = args.report_path or os.path.join(
        args.workspace, "memory", "reports", f"{args.task_id}.md"
    )
    source_label = "none"
    if os.path.exists(report_path):
        try:
            with open(report_path, encoding="utf-8") as f:
                rt = f.read()
            _, source_label = extract_report_verdict(rt)
        except Exception:
            pass

    print(f"[qc-report-guard] task={args.task_id}", file=sys.stderr)
    print(f"  qc-result JSON verdict : {result['json_verdict']}", file=sys.stderr)
    rv_display = result['report_verdict'] if result['report_verdict'] else "(없음)"
    print(f"  report verdict         : {rv_display}  ← 출처 ({source_label})", file=sys.stderr)

    if result["ok"]:
        print(f"  매칭 결과              : OK", file=sys.stderr)
    else:
        for v in result["violations"]:
            print(f"  매칭 결과              : {v}", file=sys.stderr)

    if result["missing_warns"]:
        print(f"  WARN 항목 누락         : {result['missing_warns']}", file=sys.stderr)

    overall = "PASS (rc=0)" if result["ok"] else "FAIL (rc=1)"
    print(f"[qc-report-guard] OVERALL: {overall}", file=sys.stderr)

    sys.exit(0 if result["ok"] else 1)


if __name__ == "__main__":
    main()
