"""
gemini_cli_gate_check.py — G4 Pre-PR Gemini CLI Gate (task-2562)

회장 §명시 2026-05-12 Track C 박제:
- Pre-PR Gemini CLI 단발 gate (OAuth-personal). 공식 merge gate 아님.
- GitHub Gemini App은 PR open 후 자동 호출되는 공식 merge gate로 별도 유지.
- 본 모듈은 PR open 전 single shot 호출로 code-changing issue를 사전 감지한다.

핵심 결정 박제:
- fix_loop_count max = 2 (회장 §명시)
- Lv 기반 mixed gate
    Lv.1~2  : soft (warning + PR open 허용)
    Lv.2    : risk-trigger hard (보안 민감 파일 / affected_files > 5 / danger 키워드)
    Lv.3+   : hard (.done 차단 + PR open 금지)
- scope_violation = True → 즉시 ESCALATED (fix_loop 진입 0)
- API key 사용 금지 (GEMINI_API_KEY env var 감지 시 즉시 abort)
- codex_gate_check.py 60% 재사용 (helper utilities)

OAuth-personal 사용 범위:
- gemini CLI 0.31.0 binary 호출 (~/.config/gemini/oauth_token.json auto-refresh)
- 단발 호출 (per-task 1회 입력 → JSON 결과 1회 출력)
- long polling 0 (max wait < 60s)
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import re
import subprocess
import sys as _sys
from datetime import datetime, timezone
from typing import Any

_sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

# codex_gate_check helpers 재사용 (회장 §명시 60% 재사용)
try:
    from scripts.codex_gate_check import (  # type: ignore[import-not-found]
        _extract_json_from_output,
        _normalize_affected_item,
        _read_file_safe,
        _detect_workspace_root,
    )
except ImportError:  # codex_gate_check가 sys.path에 없을 때 fallback
    from codex_gate_check import (  # type: ignore[no-redef]
        _extract_json_from_output,
        _normalize_affected_item,
        _read_file_safe,
        _detect_workspace_root,
    )


logger = logging.getLogger("gemini_cli_gate_check")

GEMINI_BIN_DEFAULT = "gemini"
GEMINI_TIMEOUT_S = 60  # 단발 호출 max wait < 60s (long polling 금지)
FIX_LOOP_MAX = 2  # 회장 §명시 hard cap
DANGER_KEYWORDS = (
    "인증",
    "결제",
    "보안",
    "권한",
    "PII",
    "개인정보",
    "API키",
    "토큰",
    "auth",
    "payment",
    "secret",
    "token",
    "credential",
)
SECURITY_SENSITIVE_FRAGMENTS = (
    "auth/",
    "payment/",
    "dispatch/",
    "owner_trigger",
    ".env",
    "g3_independent_verifier",
    "executor_scheduler",
)


def _assert_oauth_personal_only() -> None:
    """API key 사용을 즉시 차단 (회장 §명시 1:1)."""
    api_key = os.environ.get("GEMINI_API_KEY", "").strip()
    if api_key:
        raise RuntimeError(
            "FORBIDDEN_CAPABILITY_USE: GEMINI_API_KEY env var detected — "
            "G4 gate는 OAuth-personal 강제 (회장 §명시 박제)."
        )


def _read_fix_loop_count(events_dir: str, task_id: str) -> int:
    path = os.path.join(events_dir, f"{task_id}.g4-fix-loop-count")
    if not os.path.isfile(path):
        return 0
    try:
        with open(path, "r", encoding="utf-8") as f:
            return int(f.read().strip() or "0")
    except (OSError, ValueError):
        return 0


def _write_fix_loop_count(events_dir: str, task_id: str, count: int) -> None:
    os.makedirs(events_dir, exist_ok=True)
    path = os.path.join(events_dir, f"{task_id}.g4-fix-loop-count")
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        f.write(str(count))
    os.replace(tmp, path)


def _detect_level_from_task_file(task_content: str) -> str:
    """task md에서 레벨 추출. 'lv1'/'lv2'/'lv3plus' 반환."""
    if not task_content:
        return "lv1"
    m = re.search(r"##\s*레벨\s*\n([^\n]+)", task_content)
    if not m:
        return "lv1"
    raw = m.group(1).lower()
    if re.search(r"lv\.?\s*[34]|level\s*[34]|critical|security", raw):
        return "lv3plus"
    if "lv.2" in raw or "lv2" in raw or "level 2" in raw:
        return "lv2"
    return "lv1"


def _has_security_sensitive_files(affected_files: list[Any]) -> bool:
    for item in affected_files:
        path, _ = _normalize_affected_item(item)
        low = path.lower()
        for frag in SECURITY_SENSITIVE_FRAGMENTS:
            if frag in low:
                return True
    return False


def _has_danger_keywords(task_content: str) -> list[str]:
    if not task_content:
        return []
    found: list[str] = []
    for kw in DANGER_KEYWORDS:
        if kw in task_content:
            found.append(kw)
    return found


def _classify_gate_mode(
    level: str,
    affected_files: list[Any],
    task_content: str,
) -> dict[str, Any]:
    """
    Lv 기반 mixed gate 분기 결정.

    Returns dict with keys: mode ("soft"|"hard"), trigger (str), level (str).
    """
    if level == "lv3plus":
        return {"mode": "hard", "trigger": "lv3+_required", "level": level}

    triggers: list[str] = []
    if level == "lv2":
        if _has_security_sensitive_files(affected_files):
            triggers.append("security_sensitive_file")
        if len(affected_files) > 5:
            triggers.append(f"affected_files>5 (={len(affected_files)})")
        kws = _has_danger_keywords(task_content)
        if kws:
            triggers.append(f"danger_keywords={','.join(kws[:3])}")

    if triggers:
        return {"mode": "hard", "trigger": "lv2_risk_trigger:" + "+".join(triggers), "level": level}

    return {"mode": "soft", "trigger": "lv1_or_lv2_no_risk", "level": level}


def _detect_scope_violation(
    affected_files: list[Any],
    expected_files: list[str] | None,
) -> tuple[bool, list[str]]:
    """expected_files 외 파일이 affected_files에 포함되면 scope_violation."""
    if not expected_files:
        return False, []
    expected_set = {p.strip() for p in expected_files if p.strip()}
    extras: list[str] = []
    for item in affected_files:
        path, _ = _normalize_affected_item(item)
        if path not in expected_set:
            extras.append(path)
    return bool(extras), extras


def _run_gemini_cli(
    prompt: str,
    target_dir: str,
    gemini_bin: str = GEMINI_BIN_DEFAULT,
) -> tuple[dict | None, str | None]:
    """Gemini CLI 0.31.0 binary 단발 호출 (OAuth-personal, stdin).

    Returns (parsed_json or None, error_reason or None).
    """
    env = os.environ.copy()
    # 안전망: API key 환경변수 제거 (OAuth-personal 강제)
    env.pop("GEMINI_API_KEY", None)
    env.pop("GOOGLE_API_KEY", None)

    try:
        result = subprocess.run(
            [gemini_bin],
            input=prompt,
            capture_output=True,
            text=True,
            timeout=GEMINI_TIMEOUT_S,
            cwd=target_dir,
            env=env,
        )
        if result.returncode != 0:
            return None, f"gemini_cli_nonzero_exit rc={result.returncode} stderr={result.stderr[:200]}"
        parsed = _extract_json_from_output(result.stdout)
        if parsed is None:
            return None, "gemini_cli_json_parse_fail"
        return parsed, None
    except FileNotFoundError:
        return None, "gemini_cli_binary_not_found"
    except subprocess.TimeoutExpired:
        return None, f"gemini_cli_timeout (>{GEMINI_TIMEOUT_S}s)"
    except Exception as e:  # noqa: BLE001 — gate fallback이 핵심
        return None, f"gemini_cli_exception:{type(e).__name__}:{e}"


def _gemini_fallback_check(
    task_file: str,
    affected_files: list[Any],
    workspace_root: str,
    fallback_reason: str,
    target_dir: str,
) -> dict[str, Any]:
    """Gemini CLI 호출 실패 시 규칙 기반 fallback (codex maat fallback과 동질)."""
    # workspace_root: 진단 로그용 컨텍스트 (자세한 사유 추적 대비)
    logger.debug("gemini fallback: workspace_root=%s reason=%s", workspace_root, fallback_reason)
    risks: list[dict[str, str]] = []
    suggestions: list[str] = []

    if not os.path.isfile(task_file):
        risks.append(
            {
                "severity": "critical",
                "description": f"설계 문서 누락: {task_file}",
            }
        )
    else:
        content = _read_file_safe(task_file)
        if not content.strip():
            risks.append(
                {
                    "severity": "critical",
                    "description": f"설계 문서 비어있음: {task_file}",
                }
            )

    for item in affected_files:
        file_path, is_new = _normalize_affected_item(item)
        resolved = file_path if os.path.isabs(file_path) else os.path.join(target_dir, file_path)
        if not os.path.isfile(resolved):
            if is_new:
                risks.append(
                    {
                        "severity": "info",
                        "description": f"신규 파일 (아직 미생성): {file_path}",
                    }
                )
            else:
                risks.append(
                    {
                        "severity": "high",
                        "description": f"파일 누락 또는 오타: {file_path}",
                    }
                )

    if not risks:
        suggestions.append("Gemini CLI 폴백: 정적 검증 통과")

    has_critical = any(r["severity"] == "critical" for r in risks)
    return {
        "pass": not has_critical,
        "risks": risks,
        "suggestions": suggestions,
        "source": "gemini_fallback_static",
        "fallback_reason": fallback_reason,
    }


def _save_g4_marker(
    events_dir: str,
    task_id: str,
    marker_kind: str,
    payload: dict[str, Any],
) -> str:
    """
    G4 marker 파일 생성 (atomic write — _write_fix_loop_count 와 동일 패턴).

    marker_kind:
        "soft"  → memory/events/<task_id>.g4-soft-warning.json
        "fail"  → memory/events/<task_id>.g4-failed
        "scope" → memory/events/<task_id>.g4-scope-violation.json
        "cap"   → memory/events/<task_id>.g4-fix-loop-cap.json
    """
    os.makedirs(events_dir, exist_ok=True)
    suffix_map = {
        "soft": ".g4-soft-warning.json",
        "fail": ".g4-failed",
        "scope": ".g4-scope-violation.json",
        "cap": ".g4-fix-loop-cap.json",
    }
    suffix = suffix_map.get(marker_kind, f".g4-{marker_kind}.json")
    path = os.path.join(events_dir, f"{task_id}{suffix}")
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(
            {
                **payload,
                "task_id": task_id,
                "marker_kind": marker_kind,
                "ts_utc": datetime.now(timezone.utc).isoformat(),
            },
            f,
            ensure_ascii=False,
            indent=2,
        )
    os.replace(tmp, path)
    return path


def gemini_cli_gate_check(
    task_file: str,
    affected_files: list[Any],
    workspace_root: str | None = None,
    task_id: str | None = None,
    expected_files: list[str] | None = None,
    target_dir: str | None = None,
    gemini_bin: str = GEMINI_BIN_DEFAULT,
) -> dict[str, Any]:
    """
    G4 Pre-PR Gemini CLI gate 메인 함수.

    Returns dict:
        pass: bool
        gate_mode: "soft" | "hard"
        gate_trigger: str
        level: "lv1" | "lv2" | "lv3plus"
        risks: list
        suggestions: list
        source: "gemini_cli" | "gemini_fallback_static"
        fallback_reason: str | None
        fix_loop_count: int
        fix_loop_max: int
        scope_violation: bool
        scope_violation_extras: list[str]
        action: "PR_OPEN_ALLOWED" | "PR_OPEN_BLOCKED" | "ESCALATED_OWNER_DECISION"
        marker_path: str | None
    """
    _assert_oauth_personal_only()

    if workspace_root is None:
        workspace_root = _detect_workspace_root(task_file)
    assert isinstance(workspace_root, str)
    events_dir = os.path.join(workspace_root, "memory", "events")
    effective_target = target_dir or workspace_root

    task_content = _read_file_safe(task_file)
    level = _detect_level_from_task_file(task_content)
    gate_class = _classify_gate_mode(level, affected_files, task_content)
    gate_mode = gate_class["mode"]
    gate_trigger = gate_class["trigger"]

    # 1) scope_violation 우선 검사 → fix_loop 0
    scope_v, scope_extras = _detect_scope_violation(affected_files, expected_files)
    fix_loop_now = _read_fix_loop_count(events_dir, task_id) if task_id else 0

    if scope_v:
        marker = None
        if task_id:
            marker = _save_g4_marker(
                events_dir,
                task_id,
                "scope",
                {
                    "scope_violation_extras": scope_extras,
                    "expected_files": list(expected_files or []),
                    "fix_loop_count": fix_loop_now,
                    "rationale": "scope_violation → 즉시 ESCALATED, fix_loop 진입 0",
                },
            )
        return {
            "pass": False,
            "gate_mode": "hard",
            "gate_trigger": "scope_violation",
            "level": level,
            "risks": [
                {
                    "severity": "critical",
                    "description": f"scope_violation: expected_files 외 파일 {len(scope_extras)}개 — {scope_extras[:5]}",
                }
            ],
            "suggestions": ["expected_files 외 파일 제거 후 재시도 또는 owner_decision_required"],
            "source": "static_pre_check",
            "fallback_reason": None,
            "fix_loop_count": fix_loop_now,
            "fix_loop_max": FIX_LOOP_MAX,
            "scope_violation": True,
            "scope_violation_extras": scope_extras,
            "action": "ESCALATED_OWNER_DECISION",
            "marker_path": marker,
        }

    # 2) fix_loop_count cap 우선 검사
    if task_id and fix_loop_now >= FIX_LOOP_MAX:
        marker = _save_g4_marker(
            events_dir,
            task_id,
            "cap",
            {
                "fix_loop_count": fix_loop_now,
                "fix_loop_max": FIX_LOOP_MAX,
                "rationale": "FIX_LOOP_CAP_VIOLATION → OWNER_DECISION_REQUIRED",
            },
        )
        return {
            "pass": False,
            "gate_mode": "hard",
            "gate_trigger": "fix_loop_cap",
            "level": level,
            "risks": [
                {
                    "severity": "critical",
                    "description": f"fix_loop_count={fix_loop_now} >= max={FIX_LOOP_MAX} — OWNER_DECISION_REQUIRED",
                }
            ],
            "suggestions": ["회장 결정 필요 (fix_loop hard cap 도달)"],
            "source": "static_pre_check",
            "fallback_reason": None,
            "fix_loop_count": fix_loop_now,
            "fix_loop_max": FIX_LOOP_MAX,
            "scope_violation": False,
            "scope_violation_extras": [],
            "action": "ESCALATED_OWNER_DECISION",
            "marker_path": marker,
        }

    # 3) Gemini CLI 단발 호출
    code_parts: list[str] = []
    for item in affected_files:
        file_path, _ = _normalize_affected_item(item)
        resolved = file_path if os.path.isabs(file_path) else os.path.join(effective_target, file_path)
        if not os.path.isfile(resolved):
            # 디렉토리 또는 미생성 신규 파일 — fallback path 와 동일 정책으로 skip
            code_parts.append(f"--- {file_path} ---\n(파일 없음 또는 디렉토리)")
            continue
        content = _read_file_safe(resolved)
        code_parts.append(f"--- {file_path} ---\n{content}")
    code_content = "\n\n".join(code_parts) if code_parts else "(영향받는 파일 없음)"

    prompt = (
        "다음 설계 문서와 코드를 Pre-PR 검증으로 리뷰하세요. JSON으로만 응답하세요.\n\n"
        "응답 형식:\n"
        '{"risks": [{"severity": "critical|high|medium|low", "description": "..."}], "suggestions": ["..."]}\n\n'
        f"설계 문서:\n{task_content}\n\n영향받는 코드:\n{code_content}\n"
    )

    parsed, err = _run_gemini_cli(prompt, effective_target, gemini_bin=gemini_bin)

    if parsed is None:
        fallback_result = _gemini_fallback_check(
            task_file, affected_files, workspace_root, err or "unknown", effective_target
        )
        gate_pass = fallback_result["pass"]
        risks = fallback_result["risks"]
        suggestions = fallback_result["suggestions"]
        source = fallback_result["source"]
        fallback_reason = fallback_result["fallback_reason"]
    else:
        risks = parsed.get("risks", [])
        suggestions = parsed.get("suggestions", [])
        has_critical = any(
            (r.get("severity") or "").lower() == "critical" for r in risks
        )
        gate_pass = not has_critical
        source = "gemini_cli"
        fallback_reason = None

    # 4) gate 정책 적용 + marker + fix_loop 증가
    marker_path: str | None = None
    action = "PR_OPEN_ALLOWED"

    new_count = fix_loop_now
    if task_id:
        new_count = fix_loop_now + 1
        _write_fix_loop_count(events_dir, task_id, new_count)

    if not gate_pass:
        if gate_mode == "soft":
            action = "PR_OPEN_ALLOWED"
            if task_id:
                marker_path = _save_g4_marker(
                    events_dir,
                    task_id,
                    "soft",
                    {
                        "risks": risks,
                        "suggestions": suggestions,
                        "gate_trigger": gate_trigger,
                        "fix_loop_count": new_count,
                    },
                )
        else:
            action = "PR_OPEN_BLOCKED"
            if task_id:
                marker_path = _save_g4_marker(
                    events_dir,
                    task_id,
                    "fail",
                    {
                        "risks": risks,
                        "suggestions": suggestions,
                        "gate_trigger": gate_trigger,
                        "fix_loop_count": new_count,
                    },
                )

    return {
        "pass": gate_pass,
        "gate_mode": gate_mode,
        "gate_trigger": gate_trigger,
        "level": level,
        "risks": risks,
        "suggestions": suggestions,
        "source": source,
        "fallback_reason": fallback_reason,
        "fix_loop_count": new_count,
        "fix_loop_max": FIX_LOOP_MAX,
        "scope_violation": False,
        "scope_violation_extras": [],
        "action": action,
        "marker_path": marker_path,
    }


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="G4 Pre-PR Gemini CLI Gate (OAuth-personal, single shot)"
    )
    parser.add_argument("--task-id", default=None)
    parser.add_argument("--task-file", default=None)
    parser.add_argument("--affected-files", nargs="*", default=[])
    parser.add_argument("--expected-files", nargs="*", default=None)
    parser.add_argument("--workspace-root", "--workspace", dest="workspace_root", default=None)
    parser.add_argument("--target-dir", default=None)
    parser.add_argument("--gemini-bin", default=GEMINI_BIN_DEFAULT)
    args = parser.parse_args(argv)

    logging.basicConfig(
        level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
    )

    # API key 사전 차단 (CLI entry)
    try:
        _assert_oauth_personal_only()
    except RuntimeError as exc:
        print(json.dumps({"pass": False, "action": "ABORTED", "error": str(exc)}))
        return 2

    task_file = args.task_file
    if task_file is None and args.task_id:
        ws = args.workspace_root or "/home/jay/workspace"
        task_file = os.path.join(ws, "memory", "tasks", f"{args.task_id}.md")
    if task_file is None:
        parser.error("--task-file 또는 --task-id 필요")

    result = gemini_cli_gate_check(
        task_file=task_file,
        affected_files=args.affected_files,
        workspace_root=args.workspace_root,
        task_id=args.task_id,
        expected_files=args.expected_files,
        target_dir=args.target_dir,
        gemini_bin=args.gemini_bin,
    )
    print(json.dumps(result, ensure_ascii=False, indent=2))
    # hard FAIL → exit 1, scope_violation/fix_loop_cap → exit 2, soft FAIL → exit 0
    if result.get("action") == "ESCALATED_OWNER_DECISION":
        return 2
    if result.get("action") == "PR_OPEN_BLOCKED":
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
