# -*- coding: utf-8 -*-
"""anu_v3.runtime_next_action_resolver — classification → next_action recommendation.

task-2553+31 ANU_RUNTIME_RECONCILE_CHECKPOINT leaf module (구현목표 16).

CRITICAL design boundary (§10 / regression 15): next_action is a
*recommendation only*. This resolver NEVER executes merge / cron / dispatch /
closeout. It maps a classification to a recommended action string + a blocking
flag + a one-line rationale. Execution authority stays with the chair / ANU.
"""
from __future__ import annotations

from dataclasses import dataclass

# --- terminal taxonomy (9-R.2/9-R.4, §3 / §5 / §6 정합) -------------------
NO_CRON_TASK_DONE = "NO_CRON_TASK_DONE"
RESULT_READY_NO_NORMAL_CALLBACK = "RESULT_READY_NO_NORMAL_CALLBACK"
NORMAL_COLLECTOR_COMPLETED = "NORMAL_COLLECTOR_COMPLETED"
DUPLICATE_CALLBACK_IGNORED = "DUPLICATE_CALLBACK_IGNORED"
TRACK_MISMATCH = "TRACK_MISMATCH"

# --- nonterminal taxonomy (9-R.4) ----------------------------------------
RUNNING = "RUNNING"
WAIT_FOR_RESULT = "WAIT_FOR_RESULT"
FALLBACK_PENDING = "FALLBACK_PENDING"
STALE_OR_BOT_STUCK_CANDIDATE = "STALE_OR_BOT_STUCK_CANDIDATE"

TERMINAL = frozenset({
    NO_CRON_TASK_DONE,
    RESULT_READY_NO_NORMAL_CALLBACK,
    NORMAL_COLLECTOR_COMPLETED,
    DUPLICATE_CALLBACK_IGNORED,
    TRACK_MISMATCH,
})
NONTERMINAL = frozenset({
    RUNNING,
    WAIT_FOR_RESULT,
    FALLBACK_PENDING,
    STALE_OR_BOT_STUCK_CANDIDATE,
})
ALL_CLASSIFICATIONS = TERMINAL | NONTERMINAL


@dataclass(frozen=True)
class NextAction:
    classification: str
    recommendation: str
    blocking: bool
    terminal: bool
    rationale: str
    execution_authority: str = "CHAIR_OR_ANU"  # never the checkpoint itself

    def to_json(self) -> dict:
        return {
            "classification": self.classification,
            "recommendation": self.recommendation,
            "blocking": self.blocking,
            "terminal": self.terminal,
            "rationale": self.rationale,
            "execution_authority": self.execution_authority,
            "is_execution": False,  # regression 15 — recommendation only
        }


# classification -> (recommendation, blocking, rationale)
_TABLE = {
    NO_CRON_TASK_DONE: (
        "PROPOSE_BATCH_ACCEPT_RECONCILE_READ_COMPLETE",
        False,
        "NO-CRON task: result.json + .done present, dispatch ok, no normal "
        "callback, no fallback armed. Self-completion observed by read alone "
        "(dogfooding, §15). Recommend batch_state ACCEPT proposal — no cron, "
        "no merge, no write by checkpoint.",
    ),
    RESULT_READY_NO_NORMAL_CALLBACK: (
        "RECOMMEND_COLLECTOR_RECONCILE_READ",
        False,
        "Result complete but normal-collector cron self-registration was "
        "missed. NOT a task failure — recovery == read-only reconcile of "
        "result/.done (NO-CRON, 9-R.1). Non-blocking; fallback (if pending) "
        "will dedupe.",
    ),
    NORMAL_COLLECTOR_COMPLETED: (
        "RECOMMEND_LOOP_ADVANCE",
        False,
        "Primary path intact: normal collector executed. Recommend loop / "
        "next-phase advance. Callback primary path NOT disabled (§2).",
    ),
    DUPLICATE_CALLBACK_IGNORED: (
        "RECOMMEND_NO_OP_DEDUP",
        False,
        "Fallback fired after result already ready / collected. Benign "
        "dedup — recommend no-op. Fallback safety path NOT disabled (§2); "
        "pending fallback never force-deleted (§9).",
    ),
    TRACK_MISMATCH: (
        "RECOMMEND_HOLD_FOR_CHAIR_TRACK_MISMATCH",
        True,
        "4-tuple binding mismatch (task_id / dispatch_cron_id / collector / "
        "fallback). Callback 4-tuple contamination — §12 HOLD_FOR_CHAIR "
        "candidate. Quarantine recommended; checkpoint performs zero "
        "remediation (read-only).",
    ),
    RUNNING: (
        "RECOMMEND_CONTINUE_WAIT",
        True,
        "Dispatch ok, result not yet present, not stale. In-flight — "
        "recommend continue waiting (no re-dispatch).",
    ),
    WAIT_FOR_RESULT: (
        "RECOMMEND_CONTINUE_WAIT",
        True,
        "Dispatch ok, awaiting result, not stale. Recommend continue "
        "waiting (no re-dispatch, no cron).",
    ),
    FALLBACK_PENDING: (
        "RECOMMEND_CONTINUE_WAIT_FALLBACK_ARMED",
        True,
        "Result not yet present; fallback safety net armed/pending. "
        "Recommend continue waiting — safety path intact, never force "
        "deleted (§9).",
    ),
    STALE_OR_BOT_STUCK_CANDIDATE: (
        "RECOMMEND_STALE_REVIEW_NO_REDISPATCH",
        True,
        "Result missing past stale threshold (or dispatch not ok). Bot may "
        "be stuck. Recommend human/ANU review — checkpoint does NOT "
        "re-dispatch / re-arm cron (§9/§10).",
    ),
}


def resolve(classification: str) -> NextAction:
    """Map a classification to a recommendation (recommendation only)."""
    if classification not in ALL_CLASSIFICATIONS:
        # Unknown -> HOLD candidate (taxonomy 그 외 분류 0, §3).
        return NextAction(
            classification=classification,
            recommendation="RECOMMEND_HOLD_FOR_CHAIR_UNKNOWN_CLASSIFICATION",
            blocking=True,
            terminal=False,
            rationale=(
                "Classification outside enumerated taxonomy — §3 '그 외 "
                "분류 0' → HOLD_FOR_CHAIR candidate. No checkpoint action."
            ),
        )
    rec, blocking, rationale = _TABLE[classification]
    return NextAction(
        classification=classification,
        recommendation=rec,
        blocking=blocking,
        terminal=classification in TERMINAL,
        rationale=rationale,
    )
