#!/bin/bash
# task-scope-guard.sh — capability snapshot 기반 scope 검증 (task-2364 P0)
#
# 사용법: bash task-scope-guard.sh <task_id> <git_diff_files_path>
#
# 종료 코드:
#   0 — PASS (위반 없음)
#   1 — FAIL (위반 있거나 snapshot 없음)
#
# 환경변수:
#   WORKSPACE — workspace root (기본: /home/jay/workspace)

set -euo pipefail

if [ $# -lt 2 ]; then
    echo "[scope-guard] ERROR: 사용법: task-scope-guard.sh <task_id> <git_diff_files_path>" >&2
    exit 1
fi

TASK_ID="$1"
DIFF_FILE="$2"
WORKSPACE="${WORKSPACE:-/home/jay/workspace}"
SNAPSHOT_FILE="${WORKSPACE}/memory/capabilities/${TASK_ID}.json"
EVENTS_DIR="${WORKSPACE}/memory/events"

# ── 1. snapshot 파일 존재 확인 ──────────────────────────────────────────────
if [ ! -f "$SNAPSHOT_FILE" ]; then
    ALLOW_NO_SCOPE_MARKER="${EVENTS_DIR}/${TASK_ID}.allow-no-scope.log"
    if [ -f "$ALLOW_NO_SCOPE_MARKER" ]; then
        echo "[scope-guard] WARN: legacy task (no capability), --allow-no-scope grace period" >&2
        exit 0
    else
        echo "[scope-guard] ERROR: snapshot 없음 — dispatch 미수행? (${SNAPSHOT_FILE})" >&2
        echo "[scope-guard] 신규 task는 allowed_resources YAML 블록을 task 파일에 추가 후 dispatch 하세요." >&2
        echo "[scope-guard] legacy 호환이 필요하면 --allow-no-scope 플래그로 dispatch 후 재실행하세요." >&2
        exit 1
    fi
fi

# ── 2~7. Python에서 snapshot 파싱 + glob 매칭 + 위반 감지 ──────────────────
export _SCOPE_SNAPSHOT_FILE="$SNAPSHOT_FILE"
export _SCOPE_TASK_ID="$TASK_ID"
export _SCOPE_DIFF_FILE="$DIFF_FILE"
export _SCOPE_EVENTS_DIR="$EVENTS_DIR"

python3 - <<'PYEOF'
import json, sys, os, re, fnmatch
from datetime import datetime

task_id      = os.environ["_SCOPE_TASK_ID"]
snapshot_file= os.environ["_SCOPE_SNAPSHOT_FILE"]
diff_file    = os.environ["_SCOPE_DIFF_FILE"]
events_dir   = os.environ["_SCOPE_EVENTS_DIR"]

# ── snapshot 파싱 ──────────────────────────────────────────────────────────
try:
    with open(snapshot_file, encoding="utf-8") as f:
        snap = json.load(f)
except Exception as e:
    print(f"[scope-guard] ERROR: snapshot 파싱 실패: {e}", file=sys.stderr)
    sys.exit(1)

ar             = snap.get("allowed_resources", {})
paths          = list(ar.get("paths") or [])
# cross-key fallback (task-2709): paths가 비면 expected_files ∪ allowed_existing_file_edits로 채운다
if not paths:
    _seen = set()
    for _k in ("expected_files", "allowed_existing_file_edits"):
        for _p in (ar.get(_k) or []):
            if _p not in _seen:
                _seen.add(_p)
                paths.append(_p)
forbidden_paths= ar.get("forbidden_paths", [])
source_sha256  = snap.get("source_sha256", "")

# ── 시스템 자동 파일 무시 패턴 ───────────────────────────────────────────────
SYSTEM_IGNORE_PATTERN = re.compile(
    r"^(memory/heartbeats/|memory/daily/|memory/logs/|memory/reports/"
    r"|logs/|whisper/|memory/whisper/)"
    r"|bot-activity\.json$|token-ledger\.json$"
    r"|^memory/pipeline-status\.json$|^memory/preview-state\.json$"
    r"|^memory/merge-log\.json$|^memory/bot_settings_sync\.json$"
    r"|^memory/memory-check-log\.json$|^memory/task-timers\.json$"
    r"|^memory/canary-status\.json$|^\.heartbeat$|^memory/\.task-counter$"
    r"|^config/constants\.json$|^scripts/gemini_rate_tracker\.json$"
    r"|^tests/coverage-report\.txt$"
    r"|^memory/tasks/" + re.escape(task_id) + r"\.md$"
    r"|^memory/reports/" + re.escape(task_id) + r"\.md$"
    # memory/events/ 하위 파일 중 .scope-*, .allow-no-scope*, .qc-*, .done, .merge-done 등
    # 시스템 자동 이벤트는 무시하되, forbidden_paths로 보호된 파일은 forbidden 체크에서 잡힘
    r"|^memory/events/[^/]*\.(done|merge-done|qc-done|qc-result|failed|cancelled|scope-guard-done|scope-diff\.txt)$"
)

# ── glob 매칭 헬퍼 ───────────────────────────────────────────────────────────
def glob_match(pattern: str, path: str) -> bool:
    """fnmatch 기반 glob 매칭. ** 패턴 지원."""
    # "/**" 로 끝나는 패턴: 하위 디렉토리 전체 prefix 매칭
    if pattern.endswith("/**"):
        prefix = pattern[:-3]
        return path == prefix or path.startswith(prefix + "/")
    if pattern == "**":
        return True
    if "**" in pattern:
        # ** → 와일드카드로 치환 후 fnmatch
        if fnmatch.fnmatch(path, pattern.replace("**", "*")):
            return True
        # 파일명만 매칭 시도
        filename = os.path.basename(path)
        pat_simple = re.sub(r"\*\*/", "", pattern).replace("/**", "")
        if fnmatch.fnmatch(filename, pat_simple):
            return True
        return False
    return fnmatch.fnmatch(path, pattern)

# ── diff 파일 읽기 ───────────────────────────────────────────────────────────
try:
    with open(diff_file, encoding="utf-8") as f:
        diff_lines = f.readlines()
except Exception as e:
    print(f"[scope-guard] ERROR: diff 파일 읽기 실패: {e}", file=sys.stderr)
    sys.exit(1)

changed_files = [line.strip() for line in diff_lines if line.strip()]
violations    = []
checked_count = 0

for file_path in changed_files:
    # forbidden_paths 우선 매칭 (시스템 무시보다 먼저 — forbidden은 항상 강제)
    forbidden_match = next((fp for fp in forbidden_paths if glob_match(fp, file_path)), None)
    if forbidden_match:
        violations.append({
            "path": file_path,
            "reason": "forbidden_paths 위반",
            "matched_forbidden": forbidden_match,
        })
        continue

    # 시스템 자동 파일 무시 (forbidden 통과 후)
    if SYSTEM_IGNORE_PATTERN.search(file_path):
        continue
    checked_count += 1

    # paths 매칭
    if not any(glob_match(p, file_path) for p in paths):
        violations.append({
            "path": file_path,
            "reason": "paths 미포함 (scope 외 파일)",
            "not_in_paths": True,
        })

# ── 결과 처리 ────────────────────────────────────────────────────────────────
if violations:
    violation_event = {
        "task_id":          task_id,
        "violations":       violations,
        "timestamp":        datetime.now().isoformat(),
        "snapshot_sha256":  source_sha256,
        "reason":           "scope_guard_violation",
    }
    os.makedirs(events_dir, exist_ok=True)
    violation_file = os.path.join(events_dir, f"{task_id}.scope-violation.json")
    with open(violation_file, "w", encoding="utf-8") as f:
        json.dump(violation_event, f, ensure_ascii=False, indent=2)

    print(f"[scope-guard] FAIL: {len(violations)}건 위반 — {violation_file}", file=sys.stderr)
    for v in violations:
        print(f"  VIOLATION: {v['path']}: {v['reason']}", file=sys.stderr)
    sys.exit(1)
else:
    print(f"[scope-guard] PASS: {checked_count} files in scope")
    sys.exit(0)
PYEOF
