# -*- coding: utf-8 -*-
"""anu_v3.result_ready_recovery — RESULT_READY recovery + fallback-fire classifier.

Standalone module for task-2553+29 (NO-CRON variant). Encodes the rule that
RESULT_READY_NO_NORMAL_CALLBACK is a *collector recovery target*, NOT a task
failure (구현목표 3·4·6), and the deterministic fallback-fire taxonomy
(구현목표 7, regression 3·6).

NO-CRON note (9-R.1): this module performs zero cron register/remove. It only
classifies observed state; recovery == reading result.json / .done existence
(dogfooding self-recognition), never re-arming a collector cron.
"""
from __future__ import annotations

from dataclasses import dataclass

# classifications
RESULT_READY_NO_NORMAL_CALLBACK = "RESULT_READY_NO_NORMAL_CALLBACK"
NORMAL_COLLECTOR_COMPLETED = "NORMAL_COLLECTOR_COMPLETED"
WAIT_FOR_FALLBACK = "WAIT_FOR_FALLBACK"
RESULT_MISSING_BOT_STALE = "RESULT_MISSING_BOT_STALE"
DISPATCH_FAILED = "DISPATCH_FAILED"

# fallback-fire outcomes
DUPLICATE_CALLBACK_IGNORED = "DUPLICATE_CALLBACK_IGNORED"
RESULT_READY_ALREADY_COLLECTED = "RESULT_READY_ALREADY_COLLECTED"


@dataclass(frozen=True)
class RuntimeObservation:
    dispatch_ok: bool
    result_present: bool
    done_present: bool
    normal_collector_executed: bool
    by_design_no_normal_collector: bool
    fallback_state: str  # PENDING | FIRED | CANCELLED | NONE


def classify_runtime(obs: RuntimeObservation) -> str:
    """Primary state classifier (구현목표 3·4·6, regression 1·4·5)."""
    if not obs.dispatch_ok:
        return DISPATCH_FAILED
    has_result = obs.result_present or obs.done_present
    if obs.normal_collector_executed:
        return NORMAL_COLLECTOR_COMPLETED
    if has_result:
        # result/.done arrived first and no normal callback fired.
        # by-design (fallback-only) tracks land here too — still benign.
        return RESULT_READY_NO_NORMAL_CALLBACK
    # no result yet
    if obs.fallback_state == "PENDING":
        return WAIT_FOR_FALLBACK
    if obs.fallback_state == "FIRED":
        return RESULT_MISSING_BOT_STALE
    return WAIT_FOR_FALLBACK


def is_recovery_eligible(classification: str) -> bool:
    """RESULT_READY_NO_NORMAL_CALLBACK is a recovery target, not a failure."""
    return classification == RESULT_READY_NO_NORMAL_CALLBACK


def recovery_note(classification: str, by_design: bool) -> str:
    if classification != RESULT_READY_NO_NORMAL_CALLBACK:
        return ""
    if by_design:
        return (
            "by-design fallback-only track: result/.done present, normal "
            "collector intentionally absent — benign, no recovery action."
        )
    return (
        "collector recovery target: result is complete; normal-collector cron "
        "self-registration was missed. NOT a task failure. Recovery == "
        "registry reconcile-read of result/.done (NO-CRON, 9-R.1)."
    )


def can_recover_now(obs: RuntimeObservation) -> bool:
    """Is immediate collector recovery possible? (구현목표 6)

    Possible iff a complete result/.done exists for the track so the registry
    can reconcile it by read alone (no cron needed). by-design tracks count.
    """
    return obs.result_present or obs.done_present


def classify_fallback_fire(
    obs: RuntimeObservation,
    *,
    track_mismatch: bool = False,
) -> str:
    """Classify a fallback callback firing against registry truth.

    구현목표 7 / regression 3·6:
      - 4-tuple mismatch                         -> TRACK_MISMATCH
      - result ready + normal already collected  -> RESULT_READY_ALREADY_COLLECTED
      - result ready + no normal collector       -> DUPLICATE_CALLBACK_IGNORED
      - no result                                -> RESULT_MISSING_BOT_STALE
    """
    if track_mismatch:
        return "TRACK_MISMATCH"
    has_result = obs.result_present or obs.done_present
    if has_result:
        if obs.normal_collector_executed:
            return RESULT_READY_ALREADY_COLLECTED
        return DUPLICATE_CALLBACK_IGNORED
    return RESULT_MISSING_BOT_STALE
