#!/usr/bin/env python3
"""validate_handoff.py — handoff evidence 검증 CLI + 모듈

사용법:
    ./scripts/validate_handoff.py --task <task-id> [--branch <branch-name>] [--check-head-sha]

종료 코드:
    0   PASS (stdout에 valid JSON 출력)
    1   FAIL (stderr에 사유 list 출력)
"""
from __future__ import annotations

import argparse
import fnmatch
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any

# ---------------------------------------------------------------------------
# jsonschema import — 없으면 안내 후 exit
# ---------------------------------------------------------------------------

try:
    from jsonschema import Draft202012Validator
except ImportError:
    print(
        "[validate_handoff] ERROR: 'jsonschema' 패키지가 설치되어 있지 않습니다.\n"
        "  설치: pip install jsonschema",
        file=sys.stderr,
    )
    sys.exit(1)


# ---------------------------------------------------------------------------
# 경로 헬퍼
# ---------------------------------------------------------------------------

def _workspace_root(provided: Path | None = None) -> Path:
    """WORKSPACE_ROOT 환경변수 우선, 없으면 스크립트 기준 부모 디렉토리."""
    if provided is not None:
        return provided
    env = os.environ.get("WORKSPACE_ROOT")
    if env:
        return Path(env)
    # 스크립트 파일의 부모(scripts/)의 부모 = workspace root
    return Path(__file__).resolve().parent.parent


def _handoff_path(task_id: str, workspace: Path) -> Path:
    return workspace / "memory" / "handoffs" / f"{task_id}.json"


def _schema_path(workspace: Path) -> Path:
    return workspace / "memory" / "specs" / "handoff-schema.json"


# ---------------------------------------------------------------------------
# glob 매칭 헬퍼
# ---------------------------------------------------------------------------

def _matches_allowed(path: str, patterns: list[str]) -> bool:
    """path 가 patterns 중 하나에 매칭되면 True.

    - fnmatch 기반으로 **, * glob 지원.
    - 디렉토리 prefix 매칭: allowed 패턴이 'tests/handoff/**' 이면
      'tests/handoff/test_x.py' 도 허용.
    """
    for pattern in patterns:
        # 직접 fnmatch
        if fnmatch.fnmatch(path, pattern):
            return True
        # ** 를 포함한 경우 — 디렉토리 계층 포함 매칭
        # fnmatch 자체는 '/' 를 특별 취급하지 않으므로 ** 는 동작함
        # 추가로 prefix 매칭 (pattern이 dir/**로 끝나지 않아도 prefix 허용)
        # e.g. allowed = "scripts/" → path = "scripts/foo.py" 허용
        if pattern.endswith("/") and path.startswith(pattern):
            return True
        # pattern에 ** 없을 때, prefix 디렉토리 매칭
        # e.g. allowed = "scripts" (no trailing slash)
        if not any(c in pattern for c in ("*", "?")):
            # 정확 매칭 또는 디렉토리 prefix
            if path == pattern or path.startswith(pattern + "/"):
                return True
    return False


# ---------------------------------------------------------------------------
# task 파일 allowed_resources YAML 파싱 (capability snapshot 교차검증용)
# ---------------------------------------------------------------------------

def _parse_task_allowed_paths(task_file: Path) -> list[str]:
    """task md 파일에서 allowed_resources.paths 목록을 파싱한다.

    create_handoff.py의 parse_task_paths 와 동일한 단순 파서.
    실패 시 빈 리스트 반환.
    """
    try:
        content = task_file.read_text(encoding="utf-8")
    except OSError:
        return []
    allowed: list[str] = []
    in_paths = False
    in_allowed_section = False
    for line in content.splitlines():
        stripped = line.strip()
        if stripped.startswith("allowed_resources:"):
            in_allowed_section = True
            in_paths = False
            continue
        if in_allowed_section and stripped.startswith("paths:"):
            in_paths = True
            continue
        if in_allowed_section and stripped.startswith("forbidden_paths:"):
            # forbidden 섹션 진입 — paths 종료
            in_paths = False
            continue
        if in_allowed_section and stripped.startswith("commands:"):
            in_paths = False
            continue
        if stripped.startswith("```") and in_allowed_section:
            in_allowed_section = False
            in_paths = False
            continue
        if in_paths and stripped.startswith("-"):
            item = stripped[1:].strip().strip('"').strip("'")
            if item:
                allowed.append(item)
    return allowed


# ---------------------------------------------------------------------------
# 핵심 검증 함수 (모듈로도 사용 가능)
# ---------------------------------------------------------------------------

def validate_handoff(
    task_id: str,
    branch: str | None = None,
    check_head_sha: bool = False,
    workspace_root: Path | None = None,
) -> tuple[bool, list[str], dict]:
    """handoff JSON을 다단계 검증한다.

    반환:
        (pass: bool, errors: list[str], handoff_data: dict)
        pass=True이면 errors는 빈 리스트, handoff_data에 파싱된 dict.
        pass=False이면 errors에 실패 사유 리스트, handoff_data는 파싱 성공 시 채워짐.
    """
    errors: list[str] = []
    workspace = _workspace_root(workspace_root)

    # 1. handoff 파일 존재 확인
    hpath = _handoff_path(task_id, workspace)
    if not hpath.exists():
        errors.append(f"handoff 파일 없음: {hpath}")
        return False, errors, {}

    # 2. JSON 파싱
    try:
        raw = hpath.read_text(encoding="utf-8")
        data: dict[str, Any] = json.loads(raw)
    except Exception as exc:
        errors.append(f"JSON 파싱 실패: {exc}")
        return False, errors, {}

    # 3. JSON Schema 검증 (Draft 2020-12)
    schema_p = _schema_path(workspace)
    if not schema_p.exists():
        errors.append(f"스키마 파일 없음: {schema_p}")
        return False, errors, data
    try:
        schema = json.loads(schema_p.read_text(encoding="utf-8"))
    except Exception as exc:
        errors.append(f"스키마 JSON 파싱 실패: {exc}")
        return False, errors, data

    validator = Draft202012Validator(schema)
    schema_errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path))
    if schema_errors:
        for se in schema_errors:
            path_str = " -> ".join(str(p) for p in se.absolute_path) or "(root)"
            errors.append(f"Schema 위반 [{path_str}]: {se.message}")
        return False, errors, data

    # 4. task_id 값 일치
    if data.get("task_id") != task_id:
        errors.append(
            f"task_id 불일치: JSON에는 '{data.get('task_id')}', 인자는 '{task_id}'"
        )

    # 5. --branch 지정 시 current_branch 일치
    if branch is not None:
        if data.get("current_branch") != branch:
            errors.append(
                f"current_branch 불일치: JSON='{data.get('current_branch')}', "
                f"--branch='{branch}'"
            )

    # 6. --check-head-sha: head_sha == git rev-parse <branch>
    #    --branch 지정 시 그 branch의 HEAD를, 미지정 시 JSON current_branch HEAD를 사용
    if check_head_sha:
        cb = branch if branch is not None else data.get("current_branch")
        if not cb:
            errors.append("check-head-sha: 검증 대상 branch 미지정")
        else:
            try:
                result = subprocess.run(
                    ["git", "rev-parse", cb],
                    capture_output=True,
                    text=True,
                    timeout=15,
                    cwd=str(workspace),
                )
                if result.returncode != 0:
                    errors.append(
                        f"check-head-sha: git rev-parse '{cb}' 실패 — "
                        f"{result.stderr.strip()}"
                    )
                else:
                    actual_sha = result.stdout.strip()
                    recorded_sha = data.get("head_sha", "")
                    # SHA는 prefix 매칭 허용 (short vs full)
                    if not (
                        actual_sha == recorded_sha
                        or actual_sha.startswith(recorded_sha)
                        or recorded_sha.startswith(actual_sha)
                    ):
                        errors.append(
                            f"head_sha 불일치: JSON='{recorded_sha}', "
                            f"git rev-parse='{actual_sha}'"
                        )
            except subprocess.TimeoutExpired:
                errors.append("check-head-sha: git rev-parse 타임아웃")
            except Exception as exc:
                errors.append(f"check-head-sha: git 실행 오류 — {exc}")

    # 7. changed_paths ⊆ allowed_paths (allowed_paths가 비어있지 않을 때만)
    allowed_paths: list[str] = data.get("allowed_paths", [])
    changed_paths: list[str] = data.get("changed_paths", [])
    if allowed_paths:
        violations = [
            cp for cp in changed_paths
            if not _matches_allowed(cp, allowed_paths)
        ]
        if violations:
            errors.append(
                f"changed_paths가 allowed_paths 범위 초과: {violations}"
            )

    # 7b. capability snapshot 교차검증 — task 파일의 allowed_resources와 일치 확인
    #     (handoff 작성자가 allowed_paths를 임의로 넓혀서 forge하는 것 방지)
    task_file = workspace / "memory" / "tasks" / f"{task_id}.md"
    if task_file.exists():
        try:
            spec_allowed = _parse_task_allowed_paths(task_file)
            if spec_allowed:
                # handoff allowed_paths 가 task spec 범위 내인지 확인
                # (handoff는 task spec의 부분집합이어야 함)
                forged = [
                    ap for ap in allowed_paths
                    if not _matches_allowed(ap, spec_allowed)
                ]
                if forged:
                    errors.append(
                        f"handoff allowed_paths가 task 파일 allowed_resources를 초과 "
                        f"(forgery 의심): {forged}"
                    )
        except Exception as exc:
            # task 파일 파싱 실패는 warning 수준 — 기존 동작 유지
            errors.append(
                f"task 파일 allowed_resources 파싱 실패 (선택 검증): {exc}"
            )

    # 8. 4000자 초과 검사
    for field in ("pending_work", "known_failures"):
        val = data.get(field)
        if val is not None and len(val) > 4000:
            errors.append(
                f"'{field}' 필드가 4000자 초과 ({len(val)}자) — "
                f"외부 파일 '{field}_path' 사용 필요"
            )

    if errors:
        return False, errors, data
    return True, [], data


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="validate_handoff",
        description="handoff evidence JSON 다단계 검증 (task-2458 Phase 2-B)",
    )
    p.add_argument("--task", required=True, metavar="TASK_ID",
                   help="task ID (예: task-2458)")
    p.add_argument("--branch", default=None, metavar="BRANCH",
                   help="current_branch 값 검증 (예: task/task-2458-dev4)")
    p.add_argument("--check-head-sha", action="store_true",
                   help="head_sha 와 실제 git HEAD 일치 여부 검증")
    return p


def main(argv: list[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    ok, errors, data = validate_handoff(
        task_id=args.task,
        branch=args.branch,
        check_head_sha=args.check_head_sha,
    )

    if ok:
        # PASS: stdout에 valid JSON 출력
        print(json.dumps(data, ensure_ascii=False, indent=2))
        return 0
    else:
        # FAIL: stderr에 사유 list (각 항목 줄바꿈)
        for err in errors:
            print(f"FAIL: {err}", file=sys.stderr)
        return 1


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