#!/usr/bin/env python3
"""
qc_verify.py - post-task 자동 검증 메인 스크립트
불칸(Vulcan) 개발1팀 QC 도구

Usage:
    python3 qc_verify.py --task-id task-xxx [options]
"""

import argparse
import json
import os
import sys
from datetime import datetime

# 모듈 경로를 sys.path에 추가 (직접 실행 시)
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
if _THIS_DIR not in sys.path:
    sys.path.insert(0, _THIS_DIR)

from verifiers import (
    api_health,
    browser_verify,
    critical_gap,
    data_integrity,
    duplicate_check,
    file_check,
    file_touch_ratio_check,
    git_evidence,
    l1_smoketest_check,
    planned_check,
    pyright_check,
    schema_contract,
    scope_check,
    signature_check,
    spec_compliance,
    style_check,
    symbol_existence_check,
    tdd_check,
    test_runner,
    three_docs_check,
)

try:
    from verifiers import png_check  # type: ignore[attr-defined]
except ImportError:
    png_check = None

try:
    from verifiers import claude_md_check  # type: ignore[attr-defined]
except ImportError:
    claude_md_check = None

try:
    from utils.circuit_breaker import create_circuit_breaker, RecoveryAction
    _CB_AVAILABLE = True
except ImportError:
    create_circuit_breaker = None  # type: ignore[assignment]
    RecoveryAction = None  # type: ignore[assignment,misc]
    _CB_AVAILABLE = False


def _get_retry_count(task_id: str, check_name: str) -> int:
    """QC 재시도 카운터 읽기."""
    counter_dir = "/home/jay/workspace/memory/logs/retry-counters"
    counter_file = os.path.join(counter_dir, f"{task_id}.{check_name}.qc_count")
    try:
        with open(counter_file) as f:
            return int(f.read().strip())
    except (FileNotFoundError, ValueError):
        return 0


def _increment_retry_count(task_id: str, check_name: str) -> int:
    """QC 재시도 카운터 증가 후 현재 값 반환."""
    counter_dir = "/home/jay/workspace/memory/logs/retry-counters"
    os.makedirs(counter_dir, exist_ok=True)
    counter_file = os.path.join(counter_dir, f"{task_id}.{check_name}.qc_count")
    count = _get_retry_count(task_id, check_name) + 1
    with open(counter_file, "w") as f:
        f.write(str(count))
    return count


def _get_task_retry_count(task_id: str) -> int:
    """태스크 .done 제출 재시도 카운터 읽기."""
    counter_file = f"/home/jay/workspace/memory/events/{task_id}.retry_count"
    try:
        with open(counter_file) as f:
            return int(f.read().strip())
    except (FileNotFoundError, ValueError):
        return 0


def _increment_task_retry_count(task_id: str) -> int:
    """태스크 .done 제출 재시도 카운터 증가 후 현재 값 반환."""
    counter_file = f"/home/jay/workspace/memory/events/{task_id}.retry_count"
    os.makedirs(os.path.dirname(counter_file), exist_ok=True)
    count = _get_task_retry_count(task_id) + 1
    with open(counter_file, "w") as f:
        f.write(str(count))
    return count


def _append_unresolved_issue(task_id: str, details: list) -> None:
    """보고서에 미해결 이슈 기록."""
    report_path = f"/home/jay/workspace/memory/reports/{task_id}.md"
    if not os.path.isfile(report_path):
        return
    try:
        issue_text = "\n\n## 미해결 이슈 (QC 자동 기록)\n"
        issue_text += f"- pyright_check 3회 연속 FAIL\n"
        for detail in details[:5]:
            issue_text += f"  - {detail}\n"
        with open(report_path, "a") as f:
            f.write(issue_text)
    except OSError:
        pass


def _check_verifiers_integrity() -> None:
    """verifiers 디렉토리가 shared/verifiers를 가리키는 symlink인지 검증.

    INTEGRITY_FAIL 시 모든 verifier 실행 거부.
    dev8은 독립 구조 허용으로 예외.
    2026-04-17 에이전트 미팅 합의.
    """
    verifiers_dir = os.path.join(_THIS_DIR, "verifiers")

    # dev8은 독립 구조 허용
    if "/dev8/" in _THIS_DIR:
        return

    # symlink 체크
    if not os.path.islink(verifiers_dir):
        print(
            "INTEGRITY_FAIL: verifiers 디렉토리가 shared로의 심볼릭 링크가 아닙니다",
            file=sys.stderr,
        )
        sys.exit(2)

    # symlink 대상이 shared/verifiers인지 확인
    real_path = os.path.realpath(verifiers_dir)
    expected_suffix = os.path.join("teams", "shared", "verifiers")
    if not real_path.endswith(expected_suffix):
        print(
            f"INTEGRITY_FAIL: verifiers 심볼릭 링크가 shared/verifiers를 가리키지 않습니다 "
            f"(실제: {real_path})",
            file=sys.stderr,
        )
        sys.exit(2)

    # __pycache__ 존재 시 삭제 (캐시 오염 방지)
    pycache_dir = os.path.join(verifiers_dir, "__pycache__")
    if os.path.isdir(pycache_dir):
        import shutil
        shutil.rmtree(pycache_dir, ignore_errors=True)


ALL_CHECKS = [
    "api_health",
    "file_check",
    "planned_check",
    "data_integrity",
    "test_runner",
    "full_suite_check",
    "tdd_check",
    "schema_contract",
    "pyright_check",
    "style_check",
    "scope_check",
    "critical_gap",
    "spec_compliance",
    "duplicate_check",
    "three_docs_check",
    "claude_md_check",
    "file_touch_ratio_check",
    "git_evidence",
    "l1_smoketest_check",
    "signature_check",
    "symbol_existence_check",
    "gemini_review_check",
    "browser_verify",
]

MANDATORY_CHECKS = {"data_integrity", "file_check", "planned_check", "spec_compliance", "test_runner"}

# ── P2-4: TRUST 5 태그 매핑 ──────────────────────────────────────
TRUST_MAP = {
    "Tested": ["test_runner", "tdd_check", "full_suite_check"],
    "Readable": ["style_check", "pyright_check"],
    "Unified": ["scope_check"],
    "Secured": ["schema_contract"],
    "Trackable": ["data_integrity", "file_check"],
}
TRUST_INDEPENDENT = ["api_health"]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="QC Verifier — post-task 자동 검증 스크립트",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--task-id",
        required=True,
        help="검증할 task ID (예: task-4.4)",
    )
    parser.add_argument(
        "--api-base",
        default="",
        help="API 베이스 URL (예: http://localhost:8000)",
    )
    parser.add_argument(
        "--api-endpoints",
        default="",
        help="콤마 구분 엔드포인트 목록 (예: /api/status,/api/stats)",
    )
    parser.add_argument(
        "--test-dir",
        default="",
        help="pytest 실행 디렉토리",
    )
    parser.add_argument(
        "--check-files",
        default="",
        help="콤마 구분 추가 파일 경로 목록",
    )
    parser.add_argument(
        "--expected-files",
        default="",
        help="scope_check: 콤마 구분 예상 변경 파일 목록",
    )
    parser.add_argument(
        "--expected-files-from",
        default="",
        help="scope_check: 예상 변경 파일 목록 파일 경로 (한 줄에 하나씩)",
    )
    parser.add_argument(
        "--skip",
        default="",
        help="건너뛸 check 이름 목록 (콤마 구분, 예: api_health,test_runner)",
    )
    parser.add_argument(
        "--output",
        default="",
        help="결과를 저장할 JSON 파일 경로",
    )
    parser.add_argument(
        "--workers-dir",
        default="",
        help="Worker 베이스 디렉토리 (기본: /home/jay/workspace/teams 하위 자동 감지)",
    )
    parser.add_argument(
        "--schemas-dir",
        default="",
        help="JSON Schema 파일 디렉토리 (기본: /home/jay/workspace/teams/shared/schemas)",
    )
    parser.add_argument(
        "--gate",
        action="store_true",
        default=False,
        help="QC 게이트 모드: PASS/WARN 시 .done 파일 자동 생성",
    )
    parser.add_argument(
        "--team",
        default="dev1-team",
        help="팀 ID (--gate 모드에서 .done 파일에 기록)",
    )
    return parser.parse_args()


def _parse_list(raw: str) -> list:
    """콤마로 구분된 문자열을 파싱하여 빈 항목 제거."""
    if not raw:
        return []
    return [item.strip() for item in raw.split(",") if item.strip()]


def _summarize(checks: dict) -> str:
    """checks 결과에서 PASS/FAIL/SKIP/WARN/MANUAL_SKIP 카운트 요약 생성."""
    counts = {"PASS": 0, "FAIL": 0, "SKIP": 0, "WARN": 0, "MANUAL_SKIP": 0}
    for result in checks.values():
        s = result.get("status", "SKIP")
        counts[s] = counts.get(s, 0) + 1

    parts = []
    for label in ("PASS", "FAIL", "SKIP", "WARN", "MANUAL_SKIP"):
        if counts[label] > 0:
            parts.append(f"{counts[label]} {label}")
    return ", ".join(parts) if parts else "no checks ran"


def _determine_overall(checks: dict, skip_list: list) -> str:
    """전체 결과 결정: 하나라도 FAIL이면 FAIL, WARN/MANUAL_SKIP이면 WARN, 그 외 PASS."""
    statuses = [r.get("status", "SKIP") for r in checks.values()]
    if "FAIL" in statuses:
        return "FAIL"
    if "WARN" in statuses or "MANUAL_SKIP" in statuses:
        return "WARN"
    return "PASS"


def _has_workers(workers_dir: str) -> bool:
    """
    workers_dir 하위에 models.py 가 하나라도 있으면 True.
    schema_contract 자동 활성화 판단에 사용.
    """
    import glob as _glob

    base = workers_dir if workers_dir else "/home/jay/workspace/teams"
    pattern1 = os.path.join(base, "*", "models.py")
    pattern2 = os.path.join(base, "*", "*", "models.py")
    return bool(_glob.glob(pattern1) or _glob.glob(pattern2))


def _infer_test_files(check_files: list[str]) -> list[str]:
    """--check-files에서 관련 테스트 파일을 자동 추론."""
    workspace_root = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
    inferred = []
    for filepath in check_files:
        basename = os.path.basename(filepath)
        name_no_ext = os.path.splitext(basename)[0]

        # 테스트 파일 자체는 건너뛰기
        if basename.startswith("test_"):
            continue
        # Python 파일만 대상
        if not filepath.endswith(".py"):
            continue

        dirpath = os.path.dirname(filepath)

        # 후보 경로 생성
        candidates = []

        # Pattern 1: {workspace_root}/tests/test_{name}.py (프로젝트 루트 tests/)
        candidates.append(os.path.join(workspace_root, "tests", f"test_{name_no_ext}.py"))

        # Pattern 2: {dirpath}/tests/test_{name}.py (같은 디렉토리의 tests/)
        if dirpath:
            candidates.append(os.path.join(dirpath, "tests", f"test_{name_no_ext}.py"))

        # Pattern 3: {parent_dir}/tests/test_{name}.py (부모 디렉토리의 tests/)
        if dirpath:
            parent = os.path.dirname(dirpath)
            if parent:
                candidate3 = os.path.join(parent, "tests", f"test_{name_no_ext}.py")
                if candidate3 not in candidates:
                    candidates.append(candidate3)

        # 존재하는 파일만 추가
        for candidate in candidates:
            if os.path.isfile(candidate) and candidate not in inferred:
                inferred.append(candidate)

    return inferred


def run_check(
    name: str,
    skip_list: list,
    task_id: str,
    api_base: str,
    api_endpoints: list,
    test_dir: str,
    file_paths: list,
    workers_dir: str = "",
    schemas_dir: str = "",
    expected_files: list | None = None,
    gate: bool = False,
) -> dict:
    """단일 check를 실행하고 결과 dict 반환."""
    if name in skip_list:
        if name == "test_runner":
            # MANUAL_SKIP: 허용하지만 WARN 기록
            print(
                "[QC] ⚠️ test_runner SKIP 사용됨. QC 원칙 위반 가능성. --check-files 기반 자동 추론을 사용하세요.",
                file=sys.stderr,
            )
            return {
                "status": "MANUAL_SKIP",
                "details": [
                    "⚠️ test_runner: MANUAL_SKIP (경고)",
                    "QC 원칙 위반 가능성. --check-files 기반 자동 추론을 사용하세요.",
                ],
            }
        if name in MANDATORY_CHECKS:
            print(f"[QC] ERROR: '{name}'은 MANDATORY 체크입니다. --skip 불가.", file=sys.stderr)
            return {"status": "FAIL", "details": [f"MANDATORY check '{name}' cannot be skipped"]}
        return {"status": "SKIP", "details": ["Skipped via --skip flag"]}

    try:
        if name == "api_health":
            return api_health.verify(api_base, api_endpoints)

        elif name == "file_check":
            return file_check.verify(task_id=task_id, file_paths=file_paths, skip_done=gate)

        elif name == "data_integrity":
            return data_integrity.verify(task_id)

        elif name == "test_runner":
            # --test-dir 사용 시 WARN 출력
            if test_dir:
                print(
                    "[QC] ⚠️  --test-dir 사용 감지. 가능하면 --check-files 기반 자동 추론을 사용하세요.",
                    file=sys.stderr,
                )
                return test_runner.verify(test_dir)

            # --check-files 기반 자동 추론
            if file_paths:
                inferred_files = _infer_test_files(file_paths)
                if not inferred_files:
                    return {
                        "status": "SKIP",
                        "details": ["--check-files 기반 자동 추론 결과: 관련 테스트 파일 0개. 정당한 SKIP."],
                    }
                print(
                    f"[QC] test_runner: --check-files 기반 자동 추론 → {len(inferred_files)}개 테스트 파일 발견",
                    file=sys.stderr,
                )
                for tf in inferred_files:
                    print(f"[QC]   - {tf}", file=sys.stderr)
                return test_runner.verify(test_files=inferred_files)

            # 둘 다 없으면 SKIP
            return {"status": "SKIP", "details": ["No test directory or check-files specified"]}

        elif name == "full_suite_check":
            # full_suite 모드: workspace tests/ 전체 실행
            result = test_runner.verify(full_suite=True)
            # FAIL이면 WARN으로 변환 (기존 깨진 테스트로 인한 차단 방지)
            # 단, 실패 상세는 보존하여 보고서에서 확인 가능
            if result.get("status") == "FAIL":
                result["status"] = "WARN"
                result["details"].insert(0, "⚠️ full_suite FAIL → WARN 변환 (기존 실패 포함 가능)")
            return result

        elif name == "schema_contract":
            # workers/ 디렉토리에 models.py 가 존재할 때만 자동 활성화
            effective_workers_dir = workers_dir if workers_dir else None
            if not _has_workers(workers_dir):
                return {
                    "status": "SKIP",
                    "details": [
                        "No workers found (no models.py detected); "
                        "schema_contract check skipped. "
                        "Use --workers-dir to specify a directory."
                    ],
                }
            sc_kwargs = {}
            if workers_dir:
                sc_kwargs["workers_base_dir"] = workers_dir
            if schemas_dir:
                sc_kwargs["schemas_dir"] = schemas_dir
            return schema_contract.verify(task_id, **sc_kwargs)

        elif name == "tdd_check":
            return tdd_check.verify(task_id, check_files=file_paths or None)

        elif name == "pyright_check":
            result = pyright_check.verify(file_paths=file_paths, timeout=60)
            if _CB_AVAILABLE:
                cb = create_circuit_breaker(  # type: ignore[misc]
                    f"{task_id}_pyright_check",
                    strategy_type="autofix",
                    threshold=3,
                )
                if result.get("status") == "FAIL":
                    action = cb.record_error({
                        "message": "; ".join(result.get("details", [])[:3]),
                        "source": "pyright_check",
                        "details": result.get("details"),
                    })
                    if action == RecoveryAction.ESCALATE:  # type: ignore[union-attr]
                        _append_unresolved_issue(task_id, result.get("details", []))
                elif result.get("status") == "PASS":
                    cb.record_success()
            return result

        elif name == "style_check":
            return style_check.verify(file_paths)

        elif name == "scope_check":
            return scope_check.verify(expected_files or [], task_id)

        elif name == "critical_gap":
            return critical_gap.verify(task_id)

        elif name == "spec_compliance":
            return spec_compliance.verify(task_id)

        elif name == "duplicate_check":
            return duplicate_check.verify(task_id)

        elif name == "three_docs_check":
            return three_docs_check.verify(task_id)

        elif name == "png_check":
            if png_check is None:
                return {"status": "SKIP", "details": ["png_check verifier not available"]}
            output_dir_path = os.path.join(
                os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"),
                "memory",
                "outputs",
                task_id,
            )
            return png_check.verify(task_id, output_dir=output_dir_path)

        elif name == "claude_md_check":
            if claude_md_check is None:
                return {"status": "SKIP", "details": ["claude_md_check verifier not available"]}
            return claude_md_check.verify(task_id)

        elif name == "file_touch_ratio_check":
            return file_touch_ratio_check.verify(task_id)

        elif name == "git_evidence":
            return git_evidence.verify(task_id)

        elif name == "symbol_existence_check":
            return symbol_existence_check.verify(task_id)

        elif name == "planned_check":
            return planned_check.verify(task_id)

        elif name == "signature_check":
            return signature_check.verify(task_id)

        elif name == "l1_smoketest_check":
            return l1_smoketest_check.verify(task_id)

        elif name == "browser_verify":
            return browser_verify.verify(task_id)

        else:
            return {"status": "SKIP", "details": [f"Unknown check: {name}"]}

    except Exception as e:
        return {
            "status": "FAIL",
            "details": [f"Unhandled exception in {name}: {type(e).__name__}: {e}"],
        }


def _handle_gate(result: dict, task_id: str, team: str, skipped: list | None = None) -> None:
    """--gate 모드: .qc-result 생성. .done은 finish-task.sh가 담당."""
    overall = result.get("overall", "FAIL")
    workspace = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")

    if overall in ("PASS", "WARN"):
        # .qc-result 파일만 생성 (.done은 finish-task.sh가 생성)
        qc_result_path = os.path.join(workspace, "memory", "events", f"{task_id}.qc-result")
        os.makedirs(os.path.dirname(qc_result_path), exist_ok=True)
        qc_result_data = {
            "task_id": task_id,
            "team": team,
            "qc_result": overall,
            "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
            "checks_summary": {k: v.get("status", "SKIP") for k, v in result.get("checks", {}).items()},
            "skipped_checks": skipped or [],
        }
        with open(qc_result_path, "w", encoding="utf-8") as f:
            json.dump(qc_result_data, f, ensure_ascii=False, indent=2)
        print(f"[QC] Gate {overall} — .qc-result 파일 생성: {qc_result_path}", file=sys.stderr)
    else:
        # FAIL 시 재시도 카운터 증가 (기존 로직 유지)
        retry_count = _increment_task_retry_count(task_id)
        if retry_count >= 3:
            escalate_path = os.path.join(workspace, "memory", "events", f"{task_id}.escalate")
            os.makedirs(os.path.dirname(escalate_path), exist_ok=True)
            escalate_data = {
                "task_id": task_id,
                "team": team,
                "reason": "재시도 3회 초과",
                "retry_count": retry_count,
                "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
            }
            with open(escalate_path, "w", encoding="utf-8") as f:
                json.dump(escalate_data, f, ensure_ascii=False, indent=2)
            print(f"[QC] ⚠️ {task_id} 재시도 {retry_count}회 초과. 아누 판단 필요.", file=sys.stderr)
            print(f"[QC] Gate ESCALATE — .done 파일 미생성. 에스컬레이션 파일: {escalate_path}", file=sys.stderr)
            result["overall"] = "ESCALATE"
        else:
            print(f"[QC] Gate FAIL (재시도 {retry_count}/3) — .done 파일 미생성. 수정 후 재실행 필요.", file=sys.stderr)


def _build_trust_summary(checks: dict) -> dict:
    """TRUST 5차원 요약 생성. 각 차원 내 모든 verifier가 통과해야 passed=True."""
    summary = {}
    for dimension, verifiers in TRUST_MAP.items():
        dim_passed = True
        for v in verifiers:
            result = checks.get(v, {})
            status = result.get("status", "SKIP")
            if status == "FAIL":
                dim_passed = False
        summary[dimension] = {
            "verifiers": verifiers,
            "passed": dim_passed,
        }
    summary["_independent"] = {
        "verifiers": TRUST_INDEPENDENT,
        "note": "TRUST 외 독립 실행",
    }
    return summary


def build_result(task_id: str, checks: dict) -> dict:
    """최종 출력 JSON 구조 생성."""
    overall = _determine_overall(checks, [])
    result = {
        "task_id": task_id,
        "verified_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
        "overall": overall,
        "checks": checks,
        "summary": _summarize(checks),
    }

    # P2-4: TRUST 5 태그
    _ws = os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace")
    _ff_path = os.path.join(_ws, ".claude", "feature_flags.json")
    try:
        with open(_ff_path, "r", encoding="utf-8") as _f:
            _ff_data = json.load(_f)
        if _ff_data.get("flags", {}).get("trust5_tagging_enabled", False):
            result["trust_summary"] = _build_trust_summary(checks)
    except (FileNotFoundError, json.JSONDecodeError):
        pass

    result["retry_count"] = _get_task_retry_count(task_id)
    return result


def save_output(result: dict, output_path: str) -> None:
    """결과를 JSON 파일로 저장."""
    try:
        parent = os.path.dirname(output_path)
        if parent and not os.path.exists(parent):
            os.makedirs(parent, exist_ok=True)
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=2)
        print(f"[QC] Result saved to: {output_path}", file=sys.stderr)
    except OSError as e:
        print(
            f"[QC] ERROR: 결과 파일 저장 실패: {output_path}: {e}. "
            f"디렉토리 존재 여부와 쓰기 권한을 확인하세요. "
            f"'ls -ld {os.path.dirname(output_path) or '.'}' 명령으로 확인할 수 있습니다.",
            file=sys.stderr,
        )


def main() -> int:
    args = parse_args()

    task_id = args.task_id.strip()
    api_base = args.api_base.strip()
    api_endpoints = _parse_list(args.api_endpoints)
    test_dir = args.test_dir.strip()
    file_paths = _parse_list(args.check_files)
    skip_list = _parse_list(args.skip)

    # MANDATORY 체크 skip 시도 감지 및 차단
    mandatory_in_skip = [s for s in skip_list if s in MANDATORY_CHECKS]
    if mandatory_in_skip:
        print(f"[QC] ⚠️  MANDATORY 체크 skip 시도 차단: {', '.join(mandatory_in_skip)}", file=sys.stderr)
        print(
            f"[QC] MANDATORY 체크({', '.join(sorted(MANDATORY_CHECKS))})는 skip할 수 없습니다.",
            file=sys.stderr,
        )
        skip_list = [s for s in skip_list if s not in MANDATORY_CHECKS]

    if skip_list or mandatory_in_skip:
        log_entry = {
            "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
            "task_id": task_id,
            "team": args.team,
            "skipped_checks": skip_list,
            "blocked_mandatory": mandatory_in_skip,
            "gate_mode": args.gate,
        }
        log_dir = os.path.join(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"), "memory", "logs")
        os.makedirs(log_dir, exist_ok=True)
        log_path = os.path.join(log_dir, "qc-skip-log.jsonl")
        with open(log_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
        print(f"[QC] Skip 사용 기록: {log_path}", file=sys.stderr)

    output_path = args.output.strip()
    workers_dir = args.workers_dir.strip()
    schemas_dir = args.schemas_dir.strip()

    # scope_check: expected_files 파싱 (--expected-files 또는 --expected-files-from)
    expected_files = _parse_list(args.expected_files)
    expected_files_from = args.expected_files_from.strip()
    if not expected_files and expected_files_from:
        try:
            with open(expected_files_from, "r", encoding="utf-8") as ef:
                expected_files = [line.strip() for line in ef if line.strip()]
        except OSError as e:
            print(
                f"[QC] WARNING: --expected-files-from 파일을 읽을 수 없습니다: {expected_files_from}: {e}. "
                f"파일 경로와 읽기 권한을 확인하세요. "
                f"'ls {expected_files_from}' 명령으로 파일 존재 여부를 확인하거나 "
                f"--expected-files 옵션으로 직접 파일 목록을 전달하세요.",
                file=sys.stderr,
            )

    # ── Verifiers 무결성 검증 (2026-04-17 미팅 합의 항목 2) ──
    _check_verifiers_integrity()

    print(f"[QC] Starting verification for: {task_id}", file=sys.stderr)
    if skip_list:
        print(f"[QC] Skipping checks: {', '.join(skip_list)}", file=sys.stderr)

    checks = {}
    for check_name in ALL_CHECKS:
        print(f"[QC] Running check: {check_name} ...", file=sys.stderr)
        checks[check_name] = run_check(
            name=check_name,
            skip_list=skip_list,
            task_id=task_id,
            api_base=api_base,
            api_endpoints=api_endpoints,
            test_dir=test_dir,
            file_paths=file_paths,
            workers_dir=workers_dir,
            schemas_dir=schemas_dir,
            expected_files=expected_files,
            gate=args.gate,
        )
        status = checks[check_name]["status"]
        print(f"[QC]   -> {status}", file=sys.stderr)

    result = build_result(task_id, checks)

    # stdout에 JSON 출력
    print(json.dumps(result, ensure_ascii=False, indent=2))

    if output_path:
        save_output(result, output_path)

    if args.gate:
        _handle_gate(result, task_id, args.team, skipped=skip_list)

    # 종료 코드: FAIL이면 1, 그 외 0
    return 1 if result["overall"] == "FAIL" else 0


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