# -*- coding: utf-8 -*-
"""utils.anu_callback_fallback — fallback cancel-on-success signal decider.

task-2635 — normal callback registration enforcement (Core hardening).
task-2635+1 — 5축 schema 인식 (registration_result_status 우선, legacy alias fallback).

Spec: memory/specs/system_normal_callback_registration_implementation_spec_260523.md
sha256: 0fbd1dad1e110c49474dfbdf13a21fb3bdd9c7f094128004dba8472840bb832d

회장 verbatim (task-2635 §3.4):
    ANU fallback cron 의 cancel-on-success signal =
      ``normal_callback_registration_status == REGISTERED`` +
      collector durable-success 마커 생성
    단순 envelope sendfile 또는 result.json 존재 만으로는 cancel 안 함
    (false cancel 차단)

ANCHOR-4: fallback cancel-on-success = REGISTERED status +
collector durable-success 마커.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, List, Optional

from utils.callback_envelope_schema import (
    NormalCallbackRegistrationStatus,
    RegistrationResultStatus,
    is_callback_complete,
    is_success_status,
    normalize_status,
)

FALLBACK_DECIDER_SCHEMA = "utils.anu_callback_fallback.v2"  # v2 = 5축 인식


# spec §3.4 — collector durable-success marker shape.
DURABLE_SUCCESS_MARKER_KEYS = (
    "collector_durable_success_marker",
    "collector_done_marker_path",
)


@dataclass
class FallbackDecision:
    cancel_fallback: bool
    reason: str
    durable_success_marker_present: bool
    status: str
    schedule_id: Optional[str] = None


def _read_axis_3(envelope: Any) -> str:
    """task-2635+1 — read axis-3 with backward-compat fallback."""
    if not isinstance(envelope, dict):
        return RegistrationResultStatus.NOT_REGISTERED.value
    if "registration_result_status" in envelope:
        return normalize_status(envelope.get("registration_result_status"))
    return normalize_status(envelope.get("registration_status"))


def has_durable_success_marker(envelope: Any) -> bool:
    """True iff the envelope carries an authoritative collector durable-success
    marker. Either an explicit boolean (preferred) or a non-empty path.
    """
    if not isinstance(envelope, dict):
        return False
    if envelope.get("collector_durable_success_marker") is True:
        return True
    path = envelope.get("collector_done_marker_path")
    if isinstance(path, str) and path.strip():
        return True
    return False


def decide_fallback_cancel(envelope: Any) -> FallbackDecision:
    """Decide whether to cancel the fallback safety-net cron.

    Returns ``cancel_fallback=True`` ONLY when:
      * registration_result_status == REGISTERED   (real cron fired)
      * cron_schedule_id present                   (recovered from registrar)
      * collector durable-success marker present
    """
    if not isinstance(envelope, dict):
        return FallbackDecision(
            cancel_fallback=False,
            reason="envelope is not a dict",
            durable_success_marker_present=False,
            status=NormalCallbackRegistrationStatus.NOT_REGISTERED.value,
        )

    status = _read_axis_3(envelope)
    schedule_id = envelope.get("cron_schedule_id") or None
    durable = has_durable_success_marker(envelope)

    if status == RegistrationResultStatus.SKIPPED_WITH_EXPLICIT_REASON.value:
        if envelope.get("explicit_skip_reason") and envelope.get(
            "explicit_skip_authorizes_fallback_cancel"
        ) is True:
            return FallbackDecision(
                cancel_fallback=True,
                reason="skipped with explicit reason + pre-approved fallback cancel",
                durable_success_marker_present=durable,
                status=status,
                schedule_id=schedule_id,
            )
        return FallbackDecision(
            cancel_fallback=False,
            reason=(
                "skipped status but explicit_skip_authorizes_fallback_cancel "
                "not set — fallback stays armed"
            ),
            durable_success_marker_present=durable,
            status=status,
            schedule_id=schedule_id,
        )

    if status != RegistrationResultStatus.REGISTERED.value:
        return FallbackDecision(
            cancel_fallback=False,
            reason=f"status {status!r} != REGISTERED (fail-closed)",
            durable_success_marker_present=durable,
            status=status,
            schedule_id=schedule_id,
        )

    if not schedule_id:
        return FallbackDecision(
            cancel_fallback=False,
            reason="REGISTERED but cron_schedule_id missing — false cancel blocked",
            durable_success_marker_present=durable,
            status=status,
            schedule_id=None,
        )

    if not durable:
        return FallbackDecision(
            cancel_fallback=False,
            reason="REGISTERED + schedule_id present but no durable-success marker",
            durable_success_marker_present=False,
            status=status,
            schedule_id=schedule_id,
        )

    return FallbackDecision(
        cancel_fallback=True,
        reason="REGISTERED + schedule_id + collector durable-success marker",
        durable_success_marker_present=True,
        status=status,
        schedule_id=schedule_id,
    )


def expected_collector_spawn(envelope: Any) -> bool:
    """True iff a real normal callback was registered (REGISTERED + schedule_id).
    SKIPPED / NOT_REGISTERED / SENDFILE_ONLY / REGISTER_FAILED → False.
    """
    return bool(is_callback_complete(envelope))


def explain_fail_closed(envelope: Any) -> List[str]:
    if not isinstance(envelope, dict):
        return ["envelope is not a dict"]
    reasons: List[str] = []
    status = _read_axis_3(envelope)
    if not is_success_status(status):
        reasons.append(f"status {status!r} is fail-closed")
    if status == RegistrationResultStatus.REGISTERED.value and not envelope.get(
        "cron_schedule_id"
    ):
        reasons.append("REGISTERED but cron_schedule_id missing")
    if not has_durable_success_marker(envelope):
        reasons.append("no collector durable-success marker yet")
    return reasons
