# -*- coding: utf-8 -*-
"""utils.dispatch_spawn_verifier — task-2645 spawn verification gate.

회장 verbatim (2026-05-24 task-2644 사고 박제 §17.11):
spawn verification 은 4 항목으로 구성된다.

  1) workspace dir 존재 : ``/home/jay/.cokacdir/workspace/<cron_id>/`` 가 생성됐는가
  2) executor process or first activity signal : ``ps -ef | grep claude`` 또는
     workspace dir 내부 활동 흔적 (파일/디렉토리 ≥1)
  3) schedule visibility OR schedule accepted marker : cron-list 에 노출되거나
     1회성 자동삭제 schedule 의 경우 ``schedule_history`` 에 accepted marker
  4) schedule_history 부재 = 3 상태 분리 (in-progress / 종료 / silent drop)
     단독 "log 미존재" 단정 금지 — 다른 신호와 교차 판정

silent drop 확정 조건 (회장 verbatim 4신호 모두 hit):
  - workspace dir 빈 채 (생성됐어도 활동 0)
  - schedule_history log 부재
  - 봇 process 없음
  - cron-list 0 개

본 모듈은 가급적 stdlib 만 사용 + pure helper. dispatch.py / regression 양쪽에서
공유 가능하도록 dataclass 결과 + JSON dump 인터페이스를 제공한다.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Sequence

from utils.dispatch_status_enum import (
    DISPATCH_SILENT_DROP_HOLD,
    DISPATCH_SUBMITTED_UNVERIFIED,
    DISPATCH_VERIFIED_SPAWN,
)


# ── 박제 상수 (회장 verbatim — paraphrase 금지) ─────────────────────────────
COKACDIR_WORKSPACE_ROOT_DEFAULT = "/home/jay/.cokacdir/workspace"
SCHEDULE_HISTORY_ROOT_DEFAULT = "/home/jay/.cokacdir/schedule_history"

# 3 상태 분리 라벨 (schedule_history 부재 의미)
HISTORY_ABSENT_IN_PROGRESS = "HISTORY_ABSENT_IN_PROGRESS"
HISTORY_ABSENT_AFTER_COMPLETION = "HISTORY_ABSENT_AFTER_COMPLETION"
HISTORY_ABSENT_SILENT_DROP = "HISTORY_ABSENT_SILENT_DROP"
HISTORY_PRESENT = "HISTORY_PRESENT"


@dataclass(frozen=True)
class SpawnSignals:
    """spawn verifier 입력 신호 (회장 verbatim 4 항목)."""

    workspace_dir_exists: bool                       # (1)
    workspace_dir_has_activity: bool                 # (1) 보강 — 빈 디렉토리 분리
    executor_process_present: bool                   # (2)
    schedule_visible_in_cron_list: bool              # (3)
    schedule_accepted_marker_present: bool           # (3) — 1회성 자동삭제 분리
    schedule_history_log_present: bool               # (4)

    def to_json(self) -> dict:
        return {
            "workspace_dir_exists": self.workspace_dir_exists,
            "workspace_dir_has_activity": self.workspace_dir_has_activity,
            "executor_process_present": self.executor_process_present,
            "schedule_visible_in_cron_list": self.schedule_visible_in_cron_list,
            "schedule_accepted_marker_present": self.schedule_accepted_marker_present,
            "schedule_history_log_present": self.schedule_history_log_present,
        }


@dataclass(frozen=True)
class SpawnVerificationResult:
    """spawn 확인 결과 + 천이 후보 status."""

    cron_id: str
    signals: SpawnSignals
    verified: bool
    silent_drop: bool
    history_state: str           # HISTORY_PRESENT | HISTORY_ABSENT_{IN_PROGRESS|AFTER_COMPLETION|SILENT_DROP}
    next_status: str             # dispatch_status_enum 상태 중 하나
    reasons: List[str] = field(default_factory=list)

    def to_json(self) -> dict:
        return {
            "schema": "dispatch.spawn_verification_result.v1",
            "cron_id": self.cron_id,
            "signals": self.signals.to_json(),
            "verified": self.verified,
            "silent_drop": self.silent_drop,
            "history_state": self.history_state,
            "next_status": self.next_status,
            "reasons": list(self.reasons),
        }


def _classify_history_state(signals: SpawnSignals) -> str:
    """schedule_history 부재의 3 상태 분리 판정.

    회장 verbatim: "schedule_history 부재는 작업 중/종료 후/미발사(silent drop) 로
    분리 판정 — 단독 'log 미존재' 단정 금지".

    교차 신호:
      - log present                                                 => HISTORY_PRESENT
      - log absent + workspace activity + executor process          => IN_PROGRESS
      - log absent + workspace dir exists + no activity + no proc   => SILENT_DROP candidate
      - log absent + cron 비가시 + workspace 부재                    => SILENT_DROP candidate
      - log absent + 부분적 신호                                     => AFTER_COMPLETION
        (1회성 자동삭제 마커 hit 면 종료 후로 본다)
    """
    if signals.schedule_history_log_present:
        return HISTORY_PRESENT
    # 작업 중 — workspace 활동 + executor 살아있음.
    if signals.workspace_dir_has_activity and signals.executor_process_present:
        return HISTORY_ABSENT_IN_PROGRESS
    # silent drop 후보 — workspace 활동 0 + executor 없음.
    if (
        not signals.workspace_dir_has_activity
        and not signals.executor_process_present
    ):
        return HISTORY_ABSENT_SILENT_DROP
    # 그 외 — 종료 후로 본다 (accepted marker hit 또는 cron 미가시).
    if signals.schedule_accepted_marker_present and not signals.schedule_visible_in_cron_list:
        return HISTORY_ABSENT_AFTER_COMPLETION
    return HISTORY_ABSENT_AFTER_COMPLETION


def evaluate_signals(cron_id: str, signals: SpawnSignals) -> SpawnVerificationResult:
    """4 항목 + 4신호 silent drop 판정.

    silent drop 확정 (회장 verbatim) — 4신호 모두 hit:
      ① workspace dir 빈 채 (exists True + activity False)  ← 또는 exists False
      ② schedule_history log 부재
      ③ 봇 process 없음
      ④ cron-list 0 개 (visible False) 또는 accepted marker 부재

    위 4 신호 동시 hit ⇒ DISPATCH_SILENT_DROP_HOLD.
    workspace 활동 또는 executor process ≥1 hit ⇒ DISPATCH_VERIFIED_SPAWN.
    그 외 (부분 신호 + 판정 보류) ⇒ DISPATCH_SUBMITTED_UNVERIFIED (재폴링).
    """
    reasons: List[str] = []
    history_state = _classify_history_state(signals)

    # silent drop 4신호: workspace_empty + history_absent + no_process + not_visible_or_no_marker
    workspace_empty = (not signals.workspace_dir_exists) or (not signals.workspace_dir_has_activity)
    history_absent = not signals.schedule_history_log_present
    no_process = not signals.executor_process_present
    not_listed = (
        not signals.schedule_visible_in_cron_list
        and not signals.schedule_accepted_marker_present
    )

    silent_drop = workspace_empty and history_absent and no_process and not_listed

    if silent_drop:
        reasons.append(
            "silent drop 확정: workspace 빈 채 + history log 부재 + executor process 부재 "
            "+ cron 비가시/accepted marker 부재 — 4신호 모두 hit."
        )
        return SpawnVerificationResult(
            cron_id=cron_id,
            signals=signals,
            verified=False,
            silent_drop=True,
            history_state=history_state,
            next_status=DISPATCH_SILENT_DROP_HOLD,
            reasons=reasons,
        )

    # spawn verified — 활동 또는 process 한 신호 이상 hit.
    if signals.workspace_dir_has_activity or signals.executor_process_present:
        reasons.append(
            "spawn verified: "
            + (
                "workspace activity hit"
                if signals.workspace_dir_has_activity
                else "executor process hit"
            )
        )
        if signals.schedule_history_log_present:
            reasons.append("schedule_history log present (보강)")
        return SpawnVerificationResult(
            cron_id=cron_id,
            signals=signals,
            verified=True,
            silent_drop=False,
            history_state=history_state,
            next_status=DISPATCH_VERIFIED_SPAWN,
            reasons=reasons,
        )

    # 부분 신호 — 판정 보류.
    reasons.append(
        "verification 부족: 활동/process 신호 둘 다 0 — submitted_unverified 재폴링 필요."
    )
    if signals.schedule_visible_in_cron_list:
        reasons.append("cron-list 가시 — 발사 직후 가능.")
    if signals.schedule_accepted_marker_present:
        reasons.append("schedule accepted marker hit — 등록 인정.")
    return SpawnVerificationResult(
        cron_id=cron_id,
        signals=signals,
        verified=False,
        silent_drop=False,
        history_state=history_state,
        next_status=DISPATCH_SUBMITTED_UNVERIFIED,
        reasons=reasons,
    )


# ── 실측 helper (filesystem / pure stdlib) ─────────────────────────────────
def collect_signals(
    cron_id: str,
    *,
    workspace_root: str = COKACDIR_WORKSPACE_ROOT_DEFAULT,
    schedule_history_root: str = SCHEDULE_HISTORY_ROOT_DEFAULT,
    cron_list_ids: Optional[Sequence[str]] = None,
    executor_process_ids: Optional[Sequence[str]] = None,
    accepted_marker_ids: Optional[Sequence[str]] = None,
) -> SpawnSignals:
    """live 파일시스템 (옵션) + caller 제공 정보로 신호 수집.

    Pure 함수가 아닌 부분: workspace_root / schedule_history_root 디렉토리를 stat.
    caller 가 의존성을 모두 주입하면 (모든 인자 명시) 순수 함수처럼 동작 — regression
    fixture 에서 그대로 활용 가능.

    Args:
        cron_id: cokacdir cron schedule id (예: ``"673AA5A6"``).
        workspace_root: cokacdir workspace 루트 디렉토리.
        schedule_history_root: schedule_history 루트 디렉토리.
        cron_list_ids: ``cokacdir --cron-list`` 결과의 id 목록. None 이면 측정 불가
            로 보고 visible=False 처리.
        executor_process_ids: cron_id 와 매칭되는 process id 목록 (외부 수집).
        accepted_marker_ids: 1회성 자동삭제 schedule 의 accepted marker id 목록.
    """
    ws_dir = Path(workspace_root) / cron_id
    workspace_dir_exists = ws_dir.is_dir()
    workspace_dir_has_activity = False
    if workspace_dir_exists:
        try:
            workspace_dir_has_activity = any(ws_dir.iterdir())
        except OSError:
            workspace_dir_has_activity = False

    hist_log = Path(schedule_history_root) / f"{cron_id}.log"
    schedule_history_log_present = hist_log.is_file()

    executor_process_present = bool(executor_process_ids) and (cron_id in executor_process_ids)
    schedule_visible = bool(cron_list_ids) and (cron_id in cron_list_ids)
    accepted_marker_present = bool(accepted_marker_ids) and (cron_id in accepted_marker_ids)

    return SpawnSignals(
        workspace_dir_exists=workspace_dir_exists,
        workspace_dir_has_activity=workspace_dir_has_activity,
        executor_process_present=executor_process_present,
        schedule_visible_in_cron_list=schedule_visible,
        schedule_accepted_marker_present=accepted_marker_present,
        schedule_history_log_present=schedule_history_log_present,
    )


def verify_spawn(
    cron_id: str,
    *,
    workspace_root: str = COKACDIR_WORKSPACE_ROOT_DEFAULT,
    schedule_history_root: str = SCHEDULE_HISTORY_ROOT_DEFAULT,
    cron_list_ids: Optional[Sequence[str]] = None,
    executor_process_ids: Optional[Sequence[str]] = None,
    accepted_marker_ids: Optional[Sequence[str]] = None,
) -> SpawnVerificationResult:
    """signal 수집 + 평가 합성 entry-point."""
    signals = collect_signals(
        cron_id,
        workspace_root=workspace_root,
        schedule_history_root=schedule_history_root,
        cron_list_ids=cron_list_ids,
        executor_process_ids=executor_process_ids,
        accepted_marker_ids=accepted_marker_ids,
    )
    return evaluate_signals(cron_id, signals)


def is_silent_drop_confirmed(result: SpawnVerificationResult) -> bool:
    """silent drop 확정 여부 — 회장 보고 hold 게이트."""
    return result.silent_drop and result.next_status == DISPATCH_SILENT_DROP_HOLD


def dispatched_dict_is_unverified(dispatched_dict: object) -> bool:
    """``dispatch.py`` 반환 dict 가 SUBMITTED_UNVERIFIED 단계인지 확인.

    회장 verbatim ANCHOR-1: cron_response.status="ok" 만으로 final success 보지 않는다.
    legacy dict {"status": "dispatched", "cron_response": {"status":"ok",...}} 도
    spawn verification 전까지는 UNVERIFIED 로 간주한다.
    """
    if not isinstance(dispatched_dict, dict):
        return False
    status = dispatched_dict.get("status")
    if status == DISPATCH_SUBMITTED_UNVERIFIED:
        return True
    # legacy "dispatched" 도 verification 없이는 동치로 처리한다.
    if status == "dispatched":
        if "spawn_verification" not in dispatched_dict:
            return True
    return False


__all__ = [
    "COKACDIR_WORKSPACE_ROOT_DEFAULT",
    "SCHEDULE_HISTORY_ROOT_DEFAULT",
    "HISTORY_PRESENT",
    "HISTORY_ABSENT_IN_PROGRESS",
    "HISTORY_ABSENT_AFTER_COMPLETION",
    "HISTORY_ABSENT_SILENT_DROP",
    "SpawnSignals",
    "SpawnVerificationResult",
    "evaluate_signals",
    "collect_signals",
    "verify_spawn",
    "is_silent_drop_confirmed",
    "dispatched_dict_is_unverified",
]
