"""
file_touch_ratio_check.py - 보고서 수정 파일 vs 실제 git 변경 파일 비율 검증 verifier

보고서에서 "N개 파일 수정"이라 했는데 실제 git diff 에 해당 파일이 없으면 탐지.

CODE_ROOT recognition (task-2729+22, Option A replacement / fresh origin/main base)
-----------------------------------------------------------------------------------
보고서(report)는 canonical ``workspace_root`` 의 ``memory/reports`` 에서 읽되,
실제 git diff(evidence)는 환경변수로 지정된 **CODE_ROOT** 에서 계산한다.
report-root 와 diff/evidence-root 를 분리해, 워크트리에서 변경한 파일이
canonical diff 기준으로 ratio 0.00 false-negative 가 되는 문제를 제거한다.

설계 원칙(요구 verbatim):
1. CODE_ROOT 인식 우선순위: ``PROJECT_PATH`` → ``WORKTREE_PATH`` →
   ``QC_EVIDENCE_ROOT``. 값이 존재하고 유효한 git 작업 디렉토리일 때 채택하며,
   하나도 없으면 ``workspace_root`` 로 fallback(backward-compat).
2. top-level 정규화: 선택된 CODE_ROOT 가 repo **하위 디렉토리**여도
   ``git rev-parse --show-toplevel`` 로 최상위 루트로 정규화한다. git diff 는
   항상 top-level 상대경로를 돌려주므로, 보고서 경로 prefix strip 기준을
   top-level 과 맞춰 path mismatch 를 제거한다.
3. report-root / diff-root 분리: report 는 canonical ``workspace_root`` 에서
   read, diff 는 정규화된 CODE_ROOT 에서 계산.
4. HEAD~5 fail-safe: 가용 커밋이 5 미만 / shallow / ``HEAD~5`` 부재 시 가용
   범위만큼 bounded 비교하고, 루트 단일 커밋이면 git empty-tree 와 비교한다.
   미정의 crash 없이 graceful(SKIP) 처리.
5. 부수효과 없음: 본 verifier 는 report 를 read 만 하고 어떤 파일도 쓰지 않으며,
   외부 모듈(git_evidence/dispatch/callback)을 import/수정하지 않는다.
"""

import os
import re
import subprocess

# git well-known empty tree object. 루트(단일) 커밋에서 초기 파일 전체를
# 변경분으로 캡처할 때 비교 base 로 사용한다.
_EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"

# CODE_ROOT 인식 환경변수 우선순위 (요구 verbatim)
_CODE_ROOT_ENV_PRIORITY = ("PROJECT_PATH", "WORKTREE_PATH", "QC_EVIDENCE_ROOT")

# HEAD 기준 최대 거슬러 올라갈 커밋 수 (기존 HEAD~5 동작 유지)
_MAX_LOOKBACK = 5

# subprocess 안전장치
_GIT_TIMEOUT = 30


def _git(args, cwd):
    """``git -C <cwd> <args>`` 실행 헬퍼 (timeout 가드 포함)."""
    return subprocess.run(
        ["git", "-C", cwd] + list(args),
        capture_output=True,
        text=True,
        timeout=_GIT_TIMEOUT,
    )


def _is_git_worktree(path):
    """``path`` 가 유효한 git 작업 디렉토리인지 확인.

    일반 repo(``.git`` 디렉토리)와 worktree 링크(``.git`` 일반 파일) 모두 허용.
    최종 판정은 ``git rev-parse --git-dir`` 성공 여부.
    """
    if not path or not os.path.isdir(path):
        return False
    try:
        return _git(["rev-parse", "--git-dir"], path).returncode == 0
    except (subprocess.TimeoutExpired, OSError):
        return False


def _to_toplevel(path):
    """``path`` 를 git 최상위 루트로 정규화. 실패 시 입력값 그대로 반환(graceful)."""
    try:
        out = _git(["rev-parse", "--show-toplevel"], path)
        if out.returncode == 0 and out.stdout.strip():
            return out.stdout.strip()
    except (subprocess.TimeoutExpired, OSError):
        pass
    return path


def _select_code_root(workspace_root):
    """env 우선순위로 diff 계산용 CODE_ROOT 를 고른 뒤 top-level 정규화.

    Returns:
        (code_root, source_label)
        source_label: 채택된 env 변수명 또는 ``"workspace_root"``.
    """
    for env_name in _CODE_ROOT_ENV_PRIORITY:
        candidate = os.environ.get(env_name, "").strip()
        if candidate and _is_git_worktree(candidate):
            return (_to_toplevel(candidate), env_name)
    # env 미설정/무효 → canonical fallback (하위 디렉토리/심볼릭 흡수 위해 정규화 시도)
    return (_to_toplevel(workspace_root), "workspace_root")


def _reachable_commit_count(code_root):
    """HEAD 에서 도달 가능한 커밋 수. 빈 repo/비-repo/오류 시 0."""
    try:
        out = _git(["rev-list", "--count", "HEAD"], code_root)
        if out.returncode == 0:
            return int(out.stdout.strip() or "0")
    except (subprocess.TimeoutExpired, OSError, ValueError):
        pass
    return 0


def _pick_base_rev(code_root):
    """HEAD~5 fail-safe: 가용 커밋 수에 맞춰 bounded base rev 를 결정.

    - 커밋 ≥ 6 : ``HEAD~5`` (기존 동작)
    - 2..5     : ``HEAD~(count-1)`` (가용 범위만큼만)
    - 1        : empty-tree (초기 커밋 전체를 변경분으로)
    - 0        : ``None`` (호출부에서 graceful SKIP)

    Returns:
        (base_rev_or_none, commit_count)
    """
    count = _reachable_commit_count(code_root)
    if count <= 0:
        return (None, count)
    lookback = min(_MAX_LOOKBACK, count - 1)
    if lookback == 0:
        return (_EMPTY_TREE_SHA, count)
    return (f"HEAD~{lookback}", count)


def _changed_files(code_root):
    """정규화된 CODE_ROOT 에서 변경 파일(top-level 상대경로) 집합 계산.

    base_rev → working tree 비교 (커밋된 변경 + 미커밋 변경 포함).

    Returns:
        (changed_set, base_label, error_or_none)
    """
    base_rev = _pick_base_rev(code_root)[0]
    if base_rev is None:
        return (set(), "no-commit", "비교 가능한 커밋 없음 (빈 repo/비-repo)")
    try:
        out = _git(["diff", "--name-only", base_rev], code_root)
        if out.returncode != 0:
            return (set(), base_rev, f"git diff 실패: {out.stderr.strip()}")
        changed = {ln.strip() for ln in out.stdout.splitlines() if ln.strip()}
        return (changed, base_rev, None)
    except (subprocess.TimeoutExpired, OSError) as e:
        return (set(), base_rev, f"git diff 예외: {type(e).__name__}: {e}")


def _extract_reported_files(report_content, base_roots):
    """보고서에서 수정 파일 경로를 top-level 상대경로로 추출.

    지원 패턴:
    - 테이블 행: ``| /abs/workspace/... |``
    - 목록 항목: ``- /abs/workspace/...``

    ``base_roots`` 의 각 prefix(report-root, 정규화된 CODE_ROOT 등)를 strip
    후보로 쓴다. 가장 긴 prefix 를 먼저 매칭해, 보고서가 canonical 경로를 쓰든
    워크트리 경로를 쓰든 동일한 top-level 상대경로로 정규화한다.
    """
    # base_root "/" edge(MEDIUM, D안): rstrip 후 빈 문자열이 되는 루트("/")는
    # 제외한다. 빈 prefix 가 남으면 정규식이 ``| /...`` 모든 줄을 catch-all 로
    # 잡아 false-positive 가 되므로, 유효(non-empty) prefix 만 strip 후보로 쓴다.
    bases = sorted(
        {r.rstrip("/") for r in base_roots if r and r.rstrip("/")},
        key=len,
        reverse=True,
    )
    if not bases:
        return []
    alternation = "|".join(re.escape(b) for b in bases)
    pattern = re.compile(
        r"(?:^\|[ \t]*|^-[ \t]+)(?:" + alternation + r")/([^\s|]+)",
        re.MULTILINE,
    )
    seen = set()
    ordered = []
    for m in pattern.findall(report_content):
        m = m.strip()
        if m and m not in seen:
            seen.add(m)
            ordered.append(m)
    return ordered


def verify(task_id: str, workspace_root: str = "/home/jay/workspace") -> dict:
    """보고서의 수정 파일 목록과 실제 git diff 를 대조해 File-Touch Ratio 계산.

    report 는 canonical ``workspace_root`` 에서 읽고, diff 는 env 로 결정된
    (top-level 정규화된) CODE_ROOT 에서 계산한다.

    Args:
        task_id: 검사 대상 task ID
        workspace_root: canonical report 루트 (기본값 유지 — backward-compat)

    Returns:
        {"status": "PASS"|"FAIL"|"WARN"|"SKIP", "details": [...]}
    """
    # report 는 항상 canonical workspace_root 에서 read (report-root 분리)
    report_path = os.path.join(workspace_root, "memory", "reports", f"{task_id}.md")
    try:
        with open(report_path, "r", encoding="utf-8") as f:
            report_content = f.read()
    except FileNotFoundError:
        return {"status": "SKIP", "details": [f"보고서 파일 없음: {report_path}"]}
    except OSError as e:
        return {
            "status": "SKIP",
            "details": [f"보고서 파일 읽기 실패: {type(e).__name__}: {e}"],
        }

    # diff/evidence 계산용 CODE_ROOT 결정 (env 인식 + top-level 정규화)
    code_root, code_root_source = _select_code_root(workspace_root)

    # 보고서 경로 추출 — report-root 와 정규화된 CODE_ROOT 양쪽 prefix 를 strip 후보로
    reported_files = _extract_reported_files(
        report_content, [code_root, workspace_root]
    )
    if not reported_files:
        return {"status": "SKIP", "details": ["보고서에 수정 파일 섹션 없음"]}

    # CODE_ROOT 가 git repo 인지 확인
    try:
        check = _git(["rev-parse", "--git-dir"], code_root)
        if check.returncode != 0:
            return {"status": "SKIP", "details": [f"git repo 아님: {code_root}"]}
    except FileNotFoundError:
        return {"status": "SKIP", "details": ["git 명령어를 찾을 수 없음"]}
    except (subprocess.TimeoutExpired, OSError) as e:
        return {
            "status": "SKIP",
            "details": [f"git 확인 실패: {type(e).__name__}: {e}"],
        }

    # 실제 변경 파일 (HEAD~5 fail-safe 적용)
    git_changed_files, base_label, diff_error = _changed_files(code_root)
    if diff_error is not None:
        return {
            "status": "SKIP",
            "details": [
                f"git diff 계산 불가 (fail-safe): {diff_error}",
                f"CODE_ROOT: {code_root} (source={code_root_source})",
                f"base: {base_label}",
            ],
        }

    # File-Touch Ratio 계산
    reported_set = set(reported_files)
    intersection = reported_set & git_changed_files
    ratio = len(intersection) / len(reported_set) if reported_set else 0.0

    details = [
        f"CODE_ROOT: {code_root} (source={code_root_source})",
        f"report-root: {workspace_root}",
        f"diff base: {base_label}",
        f"보고서 파일 수: {len(reported_set)}",
        f"실제 변경 파일 수 (git diff {base_label}): {len(git_changed_files)}",
        f"교집합: {len(intersection)}",
        f"File-Touch Ratio: {ratio:.2f}",
    ]

    if ratio == 0:
        for f in sorted(reported_set - git_changed_files)[:10]:
            details.append(f"미변경: {f}")
        return {"status": "FAIL", "details": details + ["보고서에 명시된 파일 변경 없음"]}
    if ratio < 0.5:
        for f in sorted(reported_set - git_changed_files)[:10]:
            details.append(f"미변경: {f}")
        return {
            "status": "WARN",
            "details": details + ["보고서 파일의 50% 이상이 실제 변경 안 됨"],
        }
    return {"status": "PASS", "details": details}


if __name__ == "__main__":
    import json
    import sys

    if len(sys.argv) < 2:
        print(
            json.dumps(
                {
                    "status": "SKIP",
                    "details": ["Usage: file_touch_ratio_check.py <task_id> [workspace_root]"],
                },
                ensure_ascii=False,
                indent=2,
            )
        )
        sys.exit(0)

    _task_id = sys.argv[1]
    _workspace_root = sys.argv[2] if len(sys.argv) > 2 else "/home/jay/workspace"
    print(json.dumps(verify(_task_id, _workspace_root), ensure_ascii=False, indent=2))
