# -*- coding: utf-8 -*-
"""dispatch.spawn_verification — dispatch 반환 직전 봇 spawn 검증 결선.

task-2942 (ANU 근본원인 260813) — dispatch false-OK 감지.

배경:
  dispatch() 는 `cokacdir --cron` 등록이 성공하면 status="dispatched" 를 반환하고
  **봇이 실제로 spawn 됐는지 검증하지 않았다**. `_verify_bot_spawn`
  (scripts/harness/v36/terminal_state_classifier.py §5.2.1) 은 구현돼 있었으나
  dispatch 실행 경로에 호출부가 0건이었다(미결선). 그래서 cron 전달 채널이
  간헐 silent-drop 하면 봇은 미spawn 인데 dispatch 는 여전히 ok = **false-OK**.

본 모듈의 역할 (1차 = 감지 + 표면화만):
  1) ``write_spawn_confirmed_marker`` — 봇 세션 시작 시 spawn-confirmed marker 생성.
     이 마커가 있어야 §5.2.1 이 SPAWNED 를 판정할 수 있다 (기존엔 마커 메커니즘
     자체가 미작동이었다).
  2) ``verify_and_annotate`` — dispatch 성공 반환 직전 spawn 검증을 호출하고,
     DISPATCH_FALSE_OK 를 result["status"] 에 명시 + handoff marker 를 disk 박제.

  ★ 자동 재전송(봇별 key fallback)은 본 범위가 아니다(무한루프/중복 위험 → 2차).
    이번엔 "조용히 ok 로 넘어가는 것"만 차단한다.

무손상 원칙 (비차단):
  검증은 dispatch 를 절대 죽이면 안 된다. import 실패(`_verify_bot_spawn is None`),
  예외, 비정상 입력(result 가 dict 아님) 시 **원래 result 를 그대로 반환**한다.
  긴급 시 ``DISPATCH_SPAWN_VERIFY_ENABLED=0`` 으로 코드 변경 없이 무력화 가능하다.

본 모듈은 stdlib 만 import 한다 (봇 시작 CLI 가 무거운 dispatch 패키지를 로드하지
않도록, 파일 직접 실행 `python3 dispatch/spawn_verification.py` 를 지원한다).
"""
from __future__ import annotations

import json
import os
import time
from pathlib import Path
from typing import Optional

# ---------------------------------------------------------------------------
# 상수 (문자열 grep 대상 — verbatim 유지)
# ---------------------------------------------------------------------------
SPAWNED = "SPAWNED"
DISPATCH_FALSE_OK = "DISPATCH_FALSE_OK"
TIMEOUT_BOT_ALIVE_BUT_NO_MARKER = "TIMEOUT_BOT_ALIVE_BUT_NO_MARKER"

# result dict 기록 키
SPAWN_VERIFICATION_KEY = "spawn_verification"
SPAWN_VERIFICATION_REASON_KEY = "spawn_verification_reason"
SPAWN_VERIFICATION_WARNING_KEY = "spawn_verification_warning"

# handoff marker 사유 (§5.2 dispatch inner instrumentation)
SPAWN_FALSE_OK_FAILURE_KIND = "spawn_false_ok"
SPAWN_FALSE_OK_TERMINAL_STATE = "INFRA_DEFECT"

# 검증 자체를 끄는 kill-switch (기본 ON)
ENABLED_ENV = "DISPATCH_SPAWN_VERIFY_ENABLED"
VERIFY_DISABLED = "DISABLED"

# ★ marker 대기 예산.
#   FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC (기본 15s) = "봇이 뜬 뒤 마커를 낼 때까지"
#   의 예산이다. 그런데 dispatch 는 cron 을 **현재+10초** 에 등록하므로(=cron 발사
#   자체가 아직 일어나지 않은 상태), 10초는 봇이 존재할 수조차 없는 구간이다. 따라서
#   실제 대기 = cron 발사 offset + 마커 예산 이어야 false positive 를 피한다.
#   총 대기는 DISPATCH_SPAWN_VERIFY_WAIT_SEC 로 직접 override 할 수 있다.
MARKER_TIMEOUT_ENV = "FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC"
TOTAL_WAIT_ENV = "DISPATCH_SPAWN_VERIFY_WAIT_SEC"
DEFAULT_MARKER_TIMEOUT_SEC = 15
DEFAULT_DISPATCH_DELAY_SEC = 10

EVENTS_DIR_ENV = "FAILURE_CALLBACK_2712_EVENTS_DIR"
_REPO_ROOT = Path(__file__).resolve().parent.parent

_FALSEY = frozenset({"0", "false", "no", "off", ""})


# ---------------------------------------------------------------------------
# 설정 helper
# ---------------------------------------------------------------------------
def is_enabled() -> bool:
    """spawn 검증 활성 여부. 기본 ON, ``DISPATCH_SPAWN_VERIFY_ENABLED=0`` 이면 OFF."""
    raw = os.environ.get(ENABLED_ENV)
    if raw is None:
        return True
    return raw.strip().lower() not in _FALSEY


def default_events_dir() -> str:
    """marker 를 읽고/쓰는 events 디렉토리 (dispatch.py 와 동일 규칙)."""
    return os.environ.get(EVENTS_DIR_ENV, str(_REPO_ROOT / "memory" / "events"))


def _env_int(name: str, default: Optional[int] = None) -> Optional[int]:
    """env 정수 파싱. 미설정/파싱불가 시 default (검증이 죽으면 안 되므로 예외 금지)."""
    raw = os.environ.get(name)
    if raw is None:
        return default
    try:
        return int(str(raw).strip())
    except (TypeError, ValueError):
        return default


def resolve_wait_sec(dispatch_delay_sec: int = DEFAULT_DISPATCH_DELAY_SEC) -> int:
    """총 marker 대기 초.

    우선순위: DISPATCH_SPAWN_VERIFY_WAIT_SEC > (dispatch_delay_sec + 마커 예산).
    마커 예산은 FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC (기본 15s) 를 그대로 쓴다.
    """
    override = _env_int(TOTAL_WAIT_ENV)
    if override is not None and override >= 0:
        return override
    marker_budget = _env_int(MARKER_TIMEOUT_ENV, DEFAULT_MARKER_TIMEOUT_SEC)
    if marker_budget is None or marker_budget < 0:
        marker_budget = DEFAULT_MARKER_TIMEOUT_SEC
    try:
        delay = max(0, int(dispatch_delay_sec))
    except (TypeError, ValueError):
        delay = DEFAULT_DISPATCH_DELAY_SEC
    return delay + marker_budget


# ---------------------------------------------------------------------------
# (2) 봇 시작 시 spawn-confirmed marker 생성
# ---------------------------------------------------------------------------
def write_spawn_confirmed_marker(
    task_id: str,
    bot_id: str,
    pid: Optional[int] = None,
    events_dir: Optional[str] = None,
) -> Optional[str]:
    """``{events_dir}/{task_id}.spawn-confirmed-<ts>.json`` 을 생성한다.

    봇 세션이 시작될 때 첫 명령으로 호출된다. 이 마커가 있어야 §5.2.1
    ``_verify_bot_spawn`` 이 SPAWNED 를 판정한다.

    최소 내용: task_id / bot_id / pid / ts.

    Returns:
        생성된 marker 경로. 실패 시 None (봇 작업을 죽이지 않는다).
    """
    if not task_id:
        return None
    target_dir = Path(events_dir or default_events_dir())
    ts = time.strftime("%Y%m%dT%H%M%S", time.gmtime())
    payload = {
        "task_id": task_id,
        "bot_id": bot_id or "unknown",
        "pid": int(pid) if pid is not None else os.getpid(),
        "ts": ts,
    }
    try:
        target_dir.mkdir(parents=True, exist_ok=True)
        path = target_dir / f"{task_id}.spawn-confirmed-{ts}.json"
        path.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        return str(path)
    except Exception:
        return None


# ---------------------------------------------------------------------------
# (1) dispatch 결선 — 판정 기록 + false-OK 표면화
# ---------------------------------------------------------------------------
def annotate_spawn_verification(result: dict, verdict: Optional[str]) -> dict:
    """dispatch 반환 dict 에 spawn 검증 판정을 기록한다 (같은 dict 반환).

    - SPAWNED → 판정만 기록, status 유지 (정상 경로 무손상).
    - DISPATCH_FALSE_OK → status 를 DISPATCH_FALSE_OK 로 명시 + 사유 기록.
    - TIMEOUT_BOT_ALIVE_BUT_NO_MARKER → 경고 필드만 기록, status 는 유지
      (봇은 살아있으므로 false-OK 가 아니다).
    - verdict 가 None(검증 불가) → 원본 무손상.
    """
    if not isinstance(result, dict) or verdict is None:
        return result

    result[SPAWN_VERIFICATION_KEY] = verdict

    if verdict == DISPATCH_FALSE_OK:
        result["status"] = DISPATCH_FALSE_OK
        result[SPAWN_VERIFICATION_REASON_KEY] = SPAWN_FALSE_OK_FAILURE_KIND
    elif verdict == TIMEOUT_BOT_ALIVE_BUT_NO_MARKER:
        result[SPAWN_VERIFICATION_WARNING_KEY] = TIMEOUT_BOT_ALIVE_BUT_NO_MARKER

    return result


def _load_verify_fn():
    """``_verify_bot_spawn`` 을 지연 import. 실패 시 None (비차단)."""
    try:
        from terminal_state_classifier import _verify_bot_spawn  # type: ignore

        return _verify_bot_spawn
    except Exception:
        pass
    try:
        import sys

        harness = str(_REPO_ROOT / "scripts" / "harness" / "v36")
        if harness not in sys.path:
            sys.path.insert(0, harness)
        from terminal_state_classifier import _verify_bot_spawn  # type: ignore

        return _verify_bot_spawn
    except Exception:
        return None


def _emit_false_ok_handoff(task_id: str, events_dir: str, handoff_fn=None) -> Optional[dict]:
    """false-OK 를 disk handoff marker 로 박제 (best-effort · 비차단)."""
    fn = handoff_fn
    if fn is None:
        try:
            import sys

            harness = str(_REPO_ROOT / "scripts" / "harness" / "v36")
            if harness not in sys.path:
                sys.path.insert(0, harness)
            from failure_callback_dispatcher import write_handoff_marker  # type: ignore

            fn = write_handoff_marker
        except Exception:
            return None
    try:
        return fn(
            task_id,
            SPAWN_FALSE_OK_TERMINAL_STATE,
            failure_kind=SPAWN_FALSE_OK_FAILURE_KIND,
            events_dir=events_dir,
        )
    except Exception:
        return None


def verify_and_annotate(
    result: dict,
    task_id: str,
    expected_bot_id: str,
    *,
    child_pid: Optional[int] = None,
    events_dir: Optional[str] = None,
    dispatch_delay_sec: int = DEFAULT_DISPATCH_DELAY_SEC,
    verify_fn=None,
    handoff_fn=None,
    logger=None,
) -> dict:
    """dispatch 성공 반환 직전 spawn 검증 결선 진입점 (비차단).

    cron 방식은 child_pid 가 없으므로 marker 기반으로만 판정한다.

    ★ 무손상 계약: 아래 어떤 경우에도 원래 result 를 그대로 돌려준다.
      - result 가 dict 가 아님
      - kill-switch OFF (``DISPATCH_SPAWN_VERIFY_ENABLED=0``)
      - ``_verify_bot_spawn`` import 실패 (None)
      - 검증 중 예외 발생
    """
    if not isinstance(result, dict):
        return result

    if not is_enabled():
        result[SPAWN_VERIFICATION_KEY] = VERIFY_DISABLED
        return result

    fn = verify_fn if verify_fn is not None else _load_verify_fn()
    if fn is None:
        # 모듈 부재(fresh checkout 등) → 기존 동작(ok 반환) 유지
        if logger is not None:
            try:
                logger.warning("[spawn-verify] _verify_bot_spawn 미탑재 — 검증 스킵 (무손상)")
            except Exception:
                pass
        return result

    _events_dir = events_dir or default_events_dir()
    try:
        verdict = fn(
            task_id,
            expected_bot_id,
            child_pid,
            events_dir=_events_dir,
            timeout_sec=resolve_wait_sec(dispatch_delay_sec),
        )
    except TypeError:
        # timeout_sec 미지원(구버전 classifier) → env 기본값으로 재시도
        try:
            verdict = fn(task_id, expected_bot_id, child_pid, events_dir=_events_dir)
        except Exception as exc:  # pragma: no cover - 방어
            if logger is not None:
                try:
                    logger.warning(f"[spawn-verify] 검증 실패 (무시): {exc}")
                except Exception:
                    pass
            return result
    except Exception as exc:
        if logger is not None:
            try:
                logger.warning(f"[spawn-verify] 검증 실패 (무시): {exc}")
            except Exception:
                pass
        return result

    result = annotate_spawn_verification(result, verdict)

    if verdict == DISPATCH_FALSE_OK:
        marker = _emit_false_ok_handoff(task_id, _events_dir, handoff_fn=handoff_fn)
        if marker:
            result["spawn_verification_handoff"] = marker
        if logger is not None:
            try:
                logger.error(
                    f"[spawn-verify] {task_id}: {DISPATCH_FALSE_OK} — cron 등록은 OK 였으나 "
                    f"spawn-confirmed marker 미확인 (봇 미spawn 의심). handoff marker 박제."
                )
            except Exception:
                pass
    elif verdict == TIMEOUT_BOT_ALIVE_BUT_NO_MARKER and logger is not None:
        try:
            logger.warning(
                f"[spawn-verify] {task_id}: {TIMEOUT_BOT_ALIVE_BUT_NO_MARKER} — "
                f"봇은 살아있으나 marker 미생성 (status 유지)"
            )
        except Exception:
            pass

    return result


# ---------------------------------------------------------------------------
# 봇 시작 CLI — `python3 dispatch/spawn_verification.py --task-id X --bot-id Y`
# ---------------------------------------------------------------------------
def _main(argv=None) -> int:  # pragma: no cover - CLI
    import argparse

    parser = argparse.ArgumentParser(description="spawn-confirmed marker 생성 (봇 세션 시작 시)")
    parser.add_argument("--task-id", required=True)
    parser.add_argument("--bot-id", default="unknown")
    parser.add_argument("--pid", type=int, default=None)
    parser.add_argument("--events-dir", default=None)
    args = parser.parse_args(argv)

    path = write_spawn_confirmed_marker(
        args.task_id, args.bot_id, pid=args.pid, events_dir=args.events_dir
    )
    print(
        json.dumps(
            {"status": "ok" if path else "error", "marker": path, "task_id": args.task_id},
            ensure_ascii=False,
        )
    )
    return 0 if path else 1


__all__ = [
    "SPAWNED",
    "DISPATCH_FALSE_OK",
    "TIMEOUT_BOT_ALIVE_BUT_NO_MARKER",
    "SPAWN_VERIFICATION_KEY",
    "SPAWN_FALSE_OK_FAILURE_KIND",
    "ENABLED_ENV",
    "is_enabled",
    "default_events_dir",
    "resolve_wait_sec",
    "write_spawn_confirmed_marker",
    "annotate_spawn_verification",
    "verify_and_annotate",
]


if __name__ == "__main__":  # pragma: no cover - CLI
    raise SystemExit(_main())
