#!/usr/bin/env python3
"""terminal_state_classifier.py — task-2712 FAILURE_CALLBACK_BEFORE_EXIT_GUARD.

10 terminal state + 11 signal taxonomy 분류 + spawn verification.

회장 verbatim doctrine (task-2712 §3): 봇이 어떤 terminal state 로 종료되든 exit
전에 disk marker 박제가 가능하도록, exit_code / signal 을 10 terminal state 중
하나로 분류한다. CRASH_NO_EXIT_CODE 는 SIGKILL/OOM/kernel panic 등 정상 exit
code 가 없는 경우의 sentinel 이다.

본 모듈은 §3.1 / §3.1.1 / §5.2.1 의 locked spec 을 1:1 mirror 한다.
"""

from __future__ import annotations

import glob
import os
import time
from typing import Optional

# ── ANU collector key (회장 verbatim · self-key 금지) ──────────────────────
ANU_KEY = "c119085addb0f8b7"

# ── §3.1 10 terminal state ────────────────────────────────────────────────
TERMINAL_STATES = (
    "SUCCESS",
    "FAILURE",
    "BLOCKED",
    "SCOPE_GUARD_FAIL",
    "QC_FAIL",
    "INFRA_DEFECT",
    "PERMISSION_FAIL",
    "API_FAIL",
    "CRITICAL_ESCALATION",
    "CRASH_NO_EXIT_CODE",
)

# envelope-write fail 등 분류 불능 시의 fallback state (§4.3 / F-7)
UNCLASSIFIED_TERMINAL_STATE = "UNCLASSIFIED_TERMINAL_STATE"

# ── §3.1.1 Signal taxonomy 11 ─────────────────────────────────────────────
# exit_code(음수 = -signal) → signal 이름. crash sentinel 은 -1/-9/-15.
SIGNAL_TAXONOMY = {
    -9: "SIGKILL",
    -15: "SIGTERM",
    -2: "SIGINT",
    -11: "SIGSEGV",
    -7: "SIGBUS",
    -1: "OOM_OR_KERNEL_PANIC",
}

# crash 로 간주하는 signal exit_code 집합 (정상 exit code 없음)
_CRASH_SIGNAL_CODES = frozenset(SIGNAL_TAXONOMY.keys())


def classify_signal(exit_code: int) -> Optional[str]:
    """exit_code(음수 signal sentinel) → signal 이름. 비-signal 이면 None.

    §3.1.1 의 11 signal taxonomy 중 sentinel 매핑을 반환한다.
    container OOM-kill / kernel panic / docker stop cascade 는 모두 -1/-9/-15
    sentinel 로 수렴하므로 본 매핑으로 커버된다.
    """
    return SIGNAL_TAXONOMY.get(int(exit_code))


def classify_terminal_state(exit_code: int, hint: Optional[str] = None) -> str:
    """exit_code(+optional hint) → 10 terminal state 중 하나.

    규칙 (§3.1 / §5):
      - exit_code == 0  → SUCCESS
      - exit_code in {-1,-2,-7,-9,-11,-15} (signal sentinel) → CRASH_NO_EXIT_CODE
      - 그 외 비정상 exit_code → hint 가 valid terminal_state 면 hint, 아니면 FAILURE

    `hint` 는 inner-script hook 이 명시한 terminal_state (예: SCOPE_GUARD_FAIL,
    QC_FAIL) 를 그대로 보존하기 위한 통로다. `set -e` cascade / `trap EXIT`
    에서도 original terminal_state 를 보존한다 (§3.1.1).
    """
    code = int(exit_code)
    if code == 0:
        return "SUCCESS"
    if code in _CRASH_SIGNAL_CODES:
        return "CRASH_NO_EXIT_CODE"
    if hint and hint in TERMINAL_STATES:
        return hint
    return "FAILURE"


def is_terminal_state(value: str) -> bool:
    """value 가 10 terminal state(또는 UNCLASSIFIED fallback) 인지."""
    return value in TERMINAL_STATES or value == UNCLASSIFIED_TERMINAL_STATE


def _pid_exists(pid: int) -> bool:
    """psutil 없이도 동작하는 pid 존재 확인 (psutil 있으면 우선 사용)."""
    try:
        import psutil  # type: ignore

        return psutil.pid_exists(pid)
    except Exception:
        pass
    try:
        os.kill(pid, 0)
    except ProcessLookupError:
        return False
    except PermissionError:
        # 프로세스는 존재하나 권한 없음 → alive 로 간주
        return True
    except Exception:
        return False
    return True


def _verify_bot_spawn(task_id, expected_bot_id, child_pid=None, events_dir="memory/events"):
    """§5.2.1 spawn verification.

    return: SPAWNED / DISPATCH_FALSE_OK / TIMEOUT_BOT_ALIVE_BUT_NO_MARKER

    spawn-confirmed marker 가 timeout 안에 나타나면 SPAWNED. timeout 후에도
    marker 없는데 child 가 살아있으면 TIMEOUT_BOT_ALIVE_BUT_NO_MARKER, child 도
    없으면 DISPATCH_FALSE_OK (등록은 OK 였지만 실제 spawn 0 인 false-OK).
    """
    DEFAULT_TIMEOUT_SEC = 15
    timeout_sec = int(
        os.environ.get("FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC", DEFAULT_TIMEOUT_SEC)
    )
    polling = 1
    elapsed = 0
    pattern = os.path.join(events_dir, f"{task_id}.spawn-confirmed-*.json")
    while elapsed < timeout_sec:
        if glob.glob(pattern):
            if child_pid is None or _pid_exists(int(child_pid)):
                return "SPAWNED"
        time.sleep(polling)
        elapsed += polling
    child_alive = (child_pid is not None) and _pid_exists(int(child_pid))
    if child_alive:
        return "TIMEOUT_BOT_ALIVE_BUT_NO_MARKER"
    return "DISPATCH_FALSE_OK"


if __name__ == "__main__":  # pragma: no cover - CLI smoke
    import json
    import sys

    if len(sys.argv) >= 2:
        ec = int(sys.argv[1])
        print(
            json.dumps(
                {
                    "exit_code": ec,
                    "terminal_state": classify_terminal_state(
                        ec, sys.argv[2] if len(sys.argv) > 2 else None
                    ),
                    "signal": classify_signal(ec),
                },
                ensure_ascii=False,
            )
        )
