"""
gate_shadow_runner.py — v2.14 S1/S2: §31 Phase-1 gate shadow report-only wrapper.

회장 승인: ANU_V2_14_PHASE1_GATE_SHADOW_S1_S2_IMPLEMENTATION_APPROVED_ACTIVE_FALSE (2026-06-20).

원칙 (엄수):
- report-only / fail-open. 어떤 verdict 도 block/exit/raise 로 연결하지 않는다.
- dispatch 흐름·반환값·기존 부작용·운영 파이프라인 동작을 변경하지 않는다(호출자는 None 만 받는다).
- 로그에는 raw prompt / credential / owner key / secret / env / 토큰 / 민감 args 를 기록하지 않는다.
  verdict / reason / touched_axes / rollback_cost + task_id / goal_id / level 수준 최소 메타만 기록한다.
- runtime_state 로 민감값이 들어와도 그 내용은 로그에 기록하지 않는다(classify_gate 입력으로만 사용).
- log write 실패도 fail-open(무시).
- 실제 차단 0 · production write 0 · callback/wake/fire 0 · systemd/always-on 무관. ACTIVE=false.

이 모듈은 §31 게이트(dispatch/v2_13_gate.classify_gate)를 dispatch 경로에서 report-only 로
1회 관측하기 위한 fail-open 래퍼다. 실제 차단(fail-closed) 전환은 별도 Chair 승인(S4) 대상이다.
"""
from __future__ import annotations

import json
import datetime
from pathlib import Path

# 기록되어도 안전한(비민감) 메타 필드 화이트리스트
_ALLOWED_LOG_KEYS = {
    "ts", "source", "task_id", "goal_id", "level", "task_type",
    "verdict", "reason", "touched_axes", "rollback_cost", "mode", "active",
    "contract_source",
}
_REASON_CAP = 200
# repo root 단일소스. gate_shadow_runner.py 는 <repo>/dispatch/ 안에 있으므로 parent.parent = <repo>.
_REPO_ROOT = Path(__file__).resolve().parent.parent
_DEFAULT_LOG_DIR = _REPO_ROOT / "memory" / "state" / "gate_shadow_log"

# strict_template goal_contract — 회장 승인 결정론 매핑만(자유 서술 0). provenance 고정.
import re as _re
_SAFE_TASK_ID = _re.compile(r"^[A-Za-z0-9._+-]+$")


def build_strict_template_contract(task_id, workspace=None):
    """task md allowed_resources 에서 결정론적으로 strict_template goal_contract 를 구성한다.

    회장 승인 매핑만 사용(자유 서술/추론 0):
      goal_id ← task_id (task_id 에서만)
      expected_files ← allowed_resources.paths (paths 에서만)
      forbidden_operations ← forbidden_paths + commands (결정론 합)
      budget ← ttl_hours (있을 때만; 없으면 "UNSPECIFIED")
      rollback_floor ← dispatch.v2_13_gate.rollback_floor([]) 재사용
      scope · blast_radius · allowed_capability_delta · allowed_runtime_changes · exit_criteria = "UNSPECIFIED"
      provenance = "strict_template" 고정 — chair_injected 는 절대 사용하지 않는다(Trust Root #1).

    allowed_resources / paths 부재(불완전) → None 반환(기존 INVALID_CONTRACT_PROVENANCE 관측 유지).
    어떤 예외도 None 으로 흡수(fail-open). task md 원문/task_desc 는 contract 에 넣지 않는다.
    """
    try:
        tid = str(task_id) if task_id is not None else ""
        if not tid or ".." in tid or "/" in tid or not _SAFE_TASK_ID.match(tid):
            return None
        ws = Path(workspace) if workspace else _REPO_ROOT
        md = ws / "memory" / "tasks" / f"{tid}.md"
        if not md.exists():
            return None
        from dispatch import _parse_allowed_resources
        ar = _parse_allowed_resources(md.read_text(encoding="utf-8"))
        if not isinstance(ar, dict):
            return None
        paths = ar.get("paths") or []
        if not paths:
            return None
        forbidden = list(ar.get("forbidden_paths") or []) + list(ar.get("commands") or [])
        ttl = ar.get("ttl_hours")
        from dispatch.v2_13_gate import rollback_floor
        return {
            "goal_id": tid,
            "scope": "UNSPECIFIED",
            "blast_radius": "UNSPECIFIED",
            "allowed_capability_delta": "UNSPECIFIED",
            "expected_files": list(paths),
            "allowed_runtime_changes": "UNSPECIFIED",
            "forbidden_operations": forbidden,
            "rollback_floor": rollback_floor([]),
            "budget": {"ttl_hours": ttl} if ttl is not None else "UNSPECIFIED",
            "exit_criteria": "UNSPECIFIED",
            "provenance": "strict_template",
        }
    except Exception:
        return None


def _shadow_contract(task_id, level):
    """dispatch 시점엔 표준 goal_contract 가 없으므로 best-effort 로 구성한다.

    provenance 를 비표준 표지(dispatch_runtime_shadow)로 둬, classify_gate 가
    INVALID_CONTRACT_PROVENANCE 를 관측하게 한다 — 이는 '현재 dispatch 가 표준
    goal_contract 를 만들지 않는다'는 사실의 가시화일 뿐 차단이 아니다.
    """
    return {
        "goal_id": task_id or "unknown",
        "scope": level or "unknown",
        "blast_radius": None,
        "allowed_capability_delta": None,
        "expected_files": None,
        "allowed_runtime_changes": None,
        "forbidden_operations": None,
        "rollback_floor": None,
        "budget": None,
        "exit_criteria": None,
        "provenance": "dispatch_runtime_shadow",
    }


def _safe_reason(r):
    try:
        return str(r)[:_REASON_CAP]
    except Exception:
        return ""


def _append_log(rec, log_dir=None):
    """최소 메타 1줄을 jsonl 로 append. 실패해도 fail-open(무시)."""
    try:
        # 화이트리스트 외 키 차단(이중 안전: 민감 키 유입 방지)
        safe = {k: v for k, v in rec.items() if k in _ALLOWED_LOG_KEYS}
        d = Path(log_dir) if log_dir else _DEFAULT_LOG_DIR
        d.mkdir(parents=True, exist_ok=True)
        day = datetime.datetime.now().strftime("%Y%m%d")
        with open(d / f"gate_shadow_{day}.jsonl", "a", encoding="utf-8") as f:
            f.write(json.dumps(safe, ensure_ascii=False) + "\n")
    except Exception:
        return None


def shadow_evaluate(*, task_id=None, level=None, task_type=None,
                    runtime_state=None, log_dir=None, source="dispatch",
                    contract=None) -> None:
    """§31 게이트를 report-only 로 1회 평가하고 최소 메타만 기록한다.

    항상 None 을 반환하며, 어떤 예외도 호출자에게 전파하지 않는다(fail-open).
    호출자(dispatch / finish_task)의 흐름·반환값·부작용을 변경하지 않는다.

    source: 관측 경로 표지("dispatch" 기본 / "finish_task" 등). 로그 메타에만 기록.
    contract: optional. 명시 시 사용(단 provenance="strict_template" 만 허용 — chair_injected
            자칭은 거부). 미지정이면 task md 에서 strict_template contract 빌드를 시도하고,
            만들 수 없으면 best-effort(_shadow_contract)로 기존 INVALID_CONTRACT_PROVENANCE 관측 유지.
            ★ 실제 차단 0 — provenance 통과로 다른 verdict 가 나와도 report-only 일 뿐이다.
    """
    try:
        from dispatch.v2_13_gate import classify_gate

        # contract 결정: explicit(strict_template만) → strict_template 빌드 → best-effort
        c = contract
        contract_source = "explicit"
        if isinstance(c, dict) and c.get("provenance") != "strict_template":
            # chair_injected 등 자칭 provenance 거부(Trust Root #1) → strict_template 경로만 허용.
            c = None
            contract_source = "explicit_rejected"
        if c is None:
            c = build_strict_template_contract(task_id)
            contract_source = "strict_template" if isinstance(c, dict) else None
        if c is None:
            c = _shadow_contract(task_id, level)
            contract_source = "best_effort"

        rs = runtime_state if isinstance(runtime_state, dict) else {
            "source": source, "task_type": task_type, "level": level,
        }
        res = classify_gate(c, "", [], rs) or {}
        _append_log({
            "ts": datetime.datetime.now().astimezone().isoformat(),
            "source": source,
            "task_id": task_id,
            "goal_id": task_id,
            "level": level,
            "task_type": task_type,
            "verdict": res.get("verdict"),
            "reason": _safe_reason(res.get("reason")),
            "touched_axes": res.get("touched_axes"),
            "rollback_cost": res.get("rollback_cost"),
            "contract_source": contract_source,
            "mode": "report_only_shadow",
            "active": False,
        }, log_dir)
    except Exception:
        # fail-open: 어떤 예외도 dispatch 흐름에 전파하지 않는다.
        return None
    return None
