# -*- coding: utf-8 -*-
"""dispatch.executor_completion_contract — executor completion callback contract.

task-2553+32 — EXECUTOR COMPLETION CALLBACK MANDATORY RULE 복원 (코드/파일 자동화).

회장 정정 (memory/events/task-2553.normal-callback-mandatory-doctrine-correction
_260518.json):

    executor completion callback = MANDATORY lifecycle signal.
    NO-CRON ≠ executor completion callback 금지.

This standalone module encodes that rule as executable code so that ANY future
executor task cannot omit its normal completion callback (§1/§4).

Standalone, zero-mutation: imports/edits ZERO tracked module. The huge
dispatch/__init__.py body is NOT touched (§8 기존 산출물 수정 0). File-level
contract only — the same pattern as anu_v3.callback_4tuple_index /
anu_v3.result_ready_recovery (+29).

NO-CRON note (9-R.1): this module performs ZERO cron register/remove. It only
*declares and validates* the contract. The executor's normal completion
callback is a designed lifecycle signal — NOT an ad-hoc cron-add by the
registry/checkpoint, and NOT a cron-remove by +32 (회장 금지 준수).

Closeout note (9-R.2): requiring an executor's own normal-completion callback /
result.json / report / .done is a *lifecycle signal*, NOT an escalation of
repository/task-state finalization authority. The latter (closeout 확정 권한)
remains forbidden; the former is REQUIRED.
"""
from __future__ import annotations

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

CONTRACT_SCHEMA = "dispatch.executor_completion_contract.v1"

# ── classifications (mirror anu_v3.result_ready_recovery vocabulary) ──────────
NORMAL_COLLECTOR_COMPLETED = "NORMAL_COLLECTOR_COMPLETED"
# result.json + .done exist but NO normal completion callback was registered.
# §4.6 / §6.8 / §6.9 — this is a RECOVERY state, NOT a normal lifecycle complete.
RESULT_READY_NO_NORMAL_CALLBACK = "RESULT_READY_NO_NORMAL_CALLBACK"
DISPATCH_FAILED = "DISPATCH_FAILED"

# Classifications that are NOT an accepted normal lifecycle completion (§6.9).
NON_NORMAL_LIFECYCLE = frozenset(
    {RESULT_READY_NO_NORMAL_CALLBACK, DISPATCH_FAILED}
)

# ── NO-CRON definition correction (§4.2 / §6.3 / §6.5) ────────────────────────
NO_CRON_CORRECTED_DEFINITION = (
    "NO-CRON == registry/checkpoint(+29/+30/+31) MUST NOT arbitrarily "
    "add/remove cron. It does NOT mean the executor is forbidden from "
    "sending its normal completion callback. The executor normal completion "
    "callback is a MANDATORY lifecycle signal, not a registry-initiated "
    "ad-hoc cron, and is therefore explicitly exempt from the cron-add ban."
)


def is_executor_completion_callback_a_cron_violation() -> bool:
    """§6.5 — executor completion callback is NOT a 'cron 신규 등록 금지' breach.

    Always False: the normal completion callback is a designed lifecycle
    signal, distinct from a registry/checkpoint ad-hoc cron add.
    """
    return False


# ── callback 4-tuple (§4.5 / §6.12) ──────────────────────────────────────────
@dataclass(frozen=True)
class Callback4Tuple:
    """{task_id, dispatch_cron_id, normal_collector_cron_id*, fallback_cron_id}.

    normal_collector_cron_id is MANDATORY (§4.5). A None/empty value makes the
    contract invalid (§6.12) — it is NOT a valid NO-CRON degradation.
    """

    task_id: str
    dispatch_cron_id: str
    normal_collector_cron_id: Optional[str]
    fallback_callback_cron_id: str


def validate_4tuple(t: Callback4Tuple) -> List[str]:
    """Return invalidity reasons (empty == valid). §6.12."""
    reasons: List[str] = []
    if not t.task_id:
        reasons.append("task_id empty")
    if not t.dispatch_cron_id:
        reasons.append("dispatch_cron_id empty")
    if not t.normal_collector_cron_id:
        reasons.append(
            "normal_collector_cron_id missing — MANDATORY lifecycle signal "
            "(§4.5/§6.12). NO-CRON does NOT exempt this (§6.3/§6.5)."
        )
    if not t.fallback_callback_cron_id:
        reasons.append("fallback_callback_cron_id empty (safety path, §6.6)")
    return reasons


def tuple_is_valid(t: Callback4Tuple) -> bool:
    return not validate_4tuple(t)


# ── executor closeout checklist (§4.4 / 9-R.2) ───────────────────────────────
# Each item is a lifecycle-signal requirement for the executor's OWN task.
# Requiring these is NOT a finalization-authority escalation (9-R.2).
EXECUTOR_CLOSEOUT_CHECKLIST = (
    "result_json_present",
    "done_marker_present",
    "report_present",
    "normal_callback_registration_evidence",  # ★ §4.4 mandatory
)


@dataclass
class ExecutorCloseoutEvidence:
    result_json_present: bool = False
    done_marker_present: bool = False
    report_present: bool = False
    # ★ §4.4 — proof the executor registered its normal completion callback
    # (e.g. the normal_collector_cron_id + cron-add ack/marker).
    normal_callback_registration_evidence: bool = False
    normal_collector_cron_id: Optional[str] = None
    # 9-R.2 guard: this evidence proves a lifecycle signal, never a claim of
    # repository/task-state finalization authority.
    claims_finalization_authority: bool = False
    notes: List[str] = field(default_factory=list)


def validate_closeout_evidence(ev: ExecutorCloseoutEvidence) -> List[str]:
    """FAIL reasons for an executor closeout (§4.4 / §4.6 / 9-R.2).

    Missing normal callback registration evidence == FAIL (not a valid
    closeout) even when result.json/.done/report all exist.
    """
    reasons: List[str] = []
    if not ev.result_json_present:
        reasons.append("result.json missing")
    if not ev.done_marker_present:
        reasons.append(".done marker missing")
    if not ev.report_present:
        reasons.append("report missing")
    if not ev.normal_callback_registration_evidence:
        reasons.append(
            "normal callback registration evidence missing — executor "
            "normal completion callback is MANDATORY (§4.4/§4.6). Not a "
            "valid normal lifecycle completion."
        )
    if not ev.normal_collector_cron_id:
        reasons.append(
            "normal_collector_cron_id absent in closeout evidence (§4.5)"
        )
    if ev.claims_finalization_authority:
        reasons.append(
            "closeout evidence claims repository/task-state finalization "
            "authority — forbidden (9-R.2/§6.15). Lifecycle signal only."
        )
    return reasons


def closeout_is_valid(ev: ExecutorCloseoutEvidence) -> bool:
    return not validate_closeout_evidence(ev)


def classify_completion(
    *,
    dispatch_ok: bool,
    result_present: bool,
    done_present: bool,
    normal_callback_registered: bool,
) -> str:
    """§4.6 / §6.8 / §6.9 completion classifier.

    result.json + .done exist but normal callback never registered ->
    RESULT_READY_NO_NORMAL_CALLBACK (a recovery state, NOT a normal
    lifecycle complete).
    """
    if not dispatch_ok:
        return DISPATCH_FAILED
    has_result = result_present or done_present
    if normal_callback_registered:
        return NORMAL_COLLECTOR_COMPLETED
    if has_result:
        return RESULT_READY_NO_NORMAL_CALLBACK
    return DISPATCH_FAILED


def is_accepted_normal_lifecycle(classification: str) -> bool:
    """§6.9 — RESULT_READY_NO_NORMAL_CALLBACK is NOT a normal completion."""
    return classification == NORMAL_COLLECTOR_COMPLETED


def is_recovery_state(classification: str) -> bool:
    """§6.6/§6.9 — recovery target, not a task failure."""
    return classification == RESULT_READY_NO_NORMAL_CALLBACK
