# -*- coding: utf-8 -*-
"""anu_v3.cancel_on_success_live_wiring — cancel-on-success LIVE integration.

task-2553+45 (회장 지시 2 §1~§3). Strict-additive 신규 모듈.

문제 (회장 §2 verbatim):
  +41 / +39 는 normal completion callback 이 성공하고 collector adjudication
  도 끝났지만, fallback/dead-man cron 이 뒤늦게 발화했다. 이는 중복 fallback
  이 안전하게 무시된 사례일 뿐 cancel-on-success **live remove 성공** 사례가
  아니다. +37 에서 wiring 은 mock/격리 검증됐으나, 실제 live normal collector
  path 가 fallback_cron_id 를 **durable** 하게 lookup 해 seam 을 경유하도록
  결선되지 않았다(세션 불연속 — one-shot cron auto-delete + spawn 분리).

목표 (회장 §3 verbatim):
  실제 normal collector 경로가
  ``operational_collector_wiring::run_operational_completion_callback_collector``
  를 경유하게 한다. normal collector durable-success 후
  ``run_operational_cancel_seam(operational=True)`` 1회. fallback_cron_id 는
  **durable 4-tuple registry(+44)** 에서 lookup 한다. binding 부재면
  cron-remove 0·fallback 보존. live cron-state verifier 5조건 AND PASS 일
  때만 bound fallback 1건 제거. mismatch/missing → cron-remove 0. cancel
  -audit JSON 생성. cron-remove 실패/skip/exception 이 normal collector
  success 를 실패로 바꾸지 않게 decouple.

결선 (9-R.1 2-layer):
  * Layer A — 본 모듈은 cron 을 자체 로직으로 추가/제거하지 않는다. durable
    registry(+44) read-only lookup + artifact root(+46) read-only resolve +
    +37 standard wired entrypoint **경유**(이미 +25 → +23 seam → live
    verifier 5조건 → +9a remover 로 결선됨). ZERO ``cokacdir``/
    ``subprocess`` exec. 테스트는 Fake/Spy 격리, 실 운영 cron 무접촉.
  * Layer B — cron-remove 대상은 (i) +44 durable registry 에서 안전 lookup
    된 fallback_callback_cron_id 1건, (ii) registry record 의 chat/role
    정합, (iii) +23 live cron-state verifier 5조건 AND PASS — 이 단일
    bound·verified 1건만. 본 task 의 목표 자체(회장 §1).

선행 게이트 (회장 §4 / 9-R.1):
  fallback_cron_id 를 durable registry 에서 **안전 lookup** 할 수 없는
  상태에서는 live cron-remove 를 시도하지 않는다(보존). 본 모듈은 +44/+46
  산출물을 read-only/additive 재사용한다 (원본 무변).
"""
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional, Union

# +46 artifact root resolver — canonical-first lookup roots (read-only).
from anu_v3.artifact_root_resolver import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    canonical_root,
    resolve_roots,
)

# +44 durable append-only 4-tuple ledger (read-only query 만).
from anu_v3.callback_4tuple_registry import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    NO_LEDGER_RECORD,
    Callback4TupleRegistry,
    default_ledger_path,
)

# +45 cancel-audit 빌더/보장 writer (Layer A — NO-CRON).
from anu_v3.cancel_audit_writer import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    build_cancel_on_success_audit,
    guaranteed_write_audit,
    normalize_audit,
)

# 소유권 하드 경계 (+23 verifier 와 동일값).
from utils.live_cron_state_verifier import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    ANU_CHAT_ID,
    FALLBACK_ROLE,
)

# frozen anchor — READ-ONLY (타입 + 단일 collector 호출, byte-0 무수정).
# lookup-skip 경로에서도 normal completion callback collector 는 MANDATORY
# lifecycle signal 이므로 반드시 1회 실행한다(회장 §3 callback mandatory
# rule 약화 0). seam/cron-remove 만 skip — +37 binding-invalid 경로와 동형.
from utils.anu_delegation_completion_callback import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    CallbackInput,
    Classification,
    PostResultReview,
    no_real_codex_post_result_review,
    run_completion_callback_collector,
)

# +37 standard wired entrypoint (PRIMARY 결선 — 이미 +25→+23 seam 경유).
from utils.normal_completion_callback_collector_entrypoint import (  # pyright: ignore[reportMissingImports]  # noqa: E501
    NormalCallbackBinding,
    WiredCollectorResult,
    run_wired_normal_completion_callback_collector,
)

TASK_MARKER_PREFIX = "task-2553+45"


# ── lookup classifications (durable registry binding stage) ───────────────────
LOOKUP_VERIFIED = "LOOKUP_VERIFIED"
LOOKUP_NO_LEDGER_RECORD = "LOOKUP_NO_LEDGER_RECORD"
LOOKUP_NO_FALLBACK_BOUND = "LOOKUP_NO_FALLBACK_BOUND"
LOOKUP_TRACK_MISMATCH = "LOOKUP_TRACK_MISMATCH"
LOOKUP_CHAT_MISMATCH = "LOOKUP_CHAT_MISMATCH"
LOOKUP_ROLE_MISMATCH = "LOOKUP_ROLE_MISMATCH"
LOOKUP_ERROR = "LOOKUP_ERROR"


@dataclass
class DurableFallbackLookup:
    """durable 4-tuple registry lookup 결과 (Layer B 안전 lookup).

    ``ok=True`` AND ``status==LOOKUP_VERIFIED`` 일 때만 binding 진행.
    그 외 모든 경로(no record / no fallback / track·chat·role mismatch /
    error) → cron-remove 0, fallback 보존(회장 §3).
    """

    ok: bool
    status: str
    task_id: str
    fallback_cron_id: Optional[str]
    dispatch_cron_id: Optional[str]
    normal_collector_cron_id: Optional[str]
    chat_id: Optional[str] = None
    role: Optional[str] = None
    ledger_path: Optional[str] = None
    canonical_root: Optional[str] = None
    reason: str = ""

    def to_dict(self) -> dict:
        return {
            "schema": "task-2553+45.durable_fallback_lookup_v1",
            "ok": self.ok,
            "status": self.status,
            "task_id": self.task_id,
            "fallback_cron_id": self.fallback_cron_id,
            "dispatch_cron_id": self.dispatch_cron_id,
            "normal_collector_cron_id": self.normal_collector_cron_id,
            "chat_id": self.chat_id,
            "role": self.role,
            "ledger_path": self.ledger_path,
            "canonical_root": self.canonical_root,
            "reason": self.reason,
        }


@dataclass
class CancelOnSuccessLiveResult:
    """live cancel-on-success wiring 1회 실행 결과.

    ``collector_result`` 는 +37/+25 가 반환한 값 그대로(디커플 — registry
    lookup / seam / cron-remove 결과로 변경 0).
    """

    durable_success: bool
    lookup: DurableFallbackLookup
    wired_via_operational_collector_wiring: bool
    seam_invoked: bool
    cron_remove_invoked: bool
    fallback_preserved: bool
    normal_success_unchanged: bool
    event_id: str
    cancel_audit: dict
    cancel_audit_path: Optional[str]
    collector_result: Optional[object] = None
    wired_result: Optional[WiredCollectorResult] = None
    notes: list = field(default_factory=list)


def lookup_fallback_from_durable_registry(
    *,
    task_id: str,
    expected_chat_id: Union[int, str] = ANU_CHAT_ID,
    expected_role: str = FALLBACK_ROLE,
    ledger_path: Optional[Union[str, Path]] = None,
    autoset_cwd: Optional[Union[str, Path]] = None,
) -> DurableFallbackLookup:
    """+44 durable 4-tuple registry 에서 fallback_cron_id 안전 lookup.

    회장 §3/§4: fallback_cron_id 를 **durable** 하게 lookup 하지 못하면
    (no ledger record / no fallback bound / track·chat·role mismatch /
    error) live cron-remove 를 시도하지 않는다 — 보존.

    +46 canonical-first 로 ledger root 를 resolve 한다 (autoset-cwd 단독
    false-negative 금지 — +39 계열 원인 해소).
    """
    roots = resolve_roots(autoset_cwd=autoset_cwd)
    croot = canonical_root()
    lp = (
        Path(ledger_path)
        if ledger_path is not None
        else default_ledger_path(croot)
    )
    try:
        registry = Callback4TupleRegistry(lp)
        # registry-first verdict (track ownership; unrelated 미인용).
        # expected_chat_id 는 classify 에 넘기지 않는다 — chat mismatch 를
        # TRACK_MISMATCH 로 합치지 않고 본 모듈이 명시 LOOKUP_CHAT_MISMATCH
        # 로 분류하기 위함(회장 §3 — chat_id mismatch → cron-remove 0).
        verdict = registry.classify(
            task_id=task_id,
            expected_task_id=task_id,
        )
        rec = registry.latest_for(task_id)
    except Exception as exc:  # noqa: BLE001 - 디커플: lookup 예외 비전파
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_ERROR,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=None,
            normal_collector_cron_id=None,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=f"durable registry lookup 예외 → 보존, cron-remove 0: {exc}",
        )

    if rec is None or verdict == NO_LEDGER_RECORD:
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_NO_LEDGER_RECORD,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=None,
            normal_collector_cron_id=None,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=(
                "durable 4-tuple ledger 에 record 부재 → fallback_cron_id "
                "안전 lookup 불가, cron-remove 0, fallback 보존 (회장 §4)"
            ),
        )

    # track mismatch (회장 §3 — task_id mismatch → cron-remove 0).
    if verdict == "TRACK_MISMATCH" or rec.task_id != task_id:
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_TRACK_MISMATCH,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=rec.dispatch_cron_id,
            normal_collector_cron_id=rec.normal_collector_cron_id,
            chat_id=rec.chat_id,
            role=rec.role,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=(
                "durable record track mismatch (task_id) → unrelated cron "
                "미인용, cron-remove 0, fallback 보존"
            ),
        )

    # chat ownership mismatch (회장 §3 — chat_id mismatch → cron-remove 0).
    if str(rec.chat_id) != str(expected_chat_id):
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_CHAT_MISMATCH,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=rec.dispatch_cron_id,
            normal_collector_cron_id=rec.normal_collector_cron_id,
            chat_id=rec.chat_id,
            role=rec.role,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=(
                f"durable record chat_id={rec.chat_id} != ANU "
                f"{expected_chat_id} → cron-remove 0, fallback 보존"
            ),
        )

    # role mismatch (회장 §3 — role not fallback → cron-remove 0).
    if (rec.role or "") != expected_role:
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_ROLE_MISMATCH,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=rec.dispatch_cron_id,
            normal_collector_cron_id=rec.normal_collector_cron_id,
            chat_id=rec.chat_id,
            role=rec.role,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=(
                f"durable record role={rec.role!r} != {expected_role!r} → "
                "cron-remove 0, fallback 보존"
            ),
        )

    # no fallback bound (회장 §3 — binding 부재 → cron-remove 0·보존).
    if not rec.fallback_callback_cron_id:
        return DurableFallbackLookup(
            ok=False,
            status=LOOKUP_NO_FALLBACK_BOUND,
            task_id=task_id,
            fallback_cron_id=None,
            dispatch_cron_id=rec.dispatch_cron_id,
            normal_collector_cron_id=rec.normal_collector_cron_id,
            chat_id=rec.chat_id,
            role=rec.role,
            ledger_path=str(lp),
            canonical_root=roots.canonical_root,
            reason=(
                "durable record 에 fallback_callback_cron_id binding 부재 "
                "(no_fallback 계약) → cron-remove 0, fallback 보존 (회장 §3)"
            ),
        )

    return DurableFallbackLookup(
        ok=True,
        status=LOOKUP_VERIFIED,
        task_id=task_id,
        fallback_cron_id=rec.fallback_callback_cron_id,
        dispatch_cron_id=rec.dispatch_cron_id,
        normal_collector_cron_id=rec.normal_collector_cron_id,
        chat_id=rec.chat_id,
        role=rec.role,
        ledger_path=str(lp),
        canonical_root=roots.canonical_root,
        reason="durable 4-tuple registry lookup 성공 — bound fallback 1건",
    )


def run_cancel_on_success_live_wiring(
    inp: CallbackInput,
    ack_path: Union[str, Path],
    *,
    dispatch_fired_marker_path: Union[str, Path],
    result_json_path: Union[str, Path],
    report_path: Union[str, Path],
    collector_result_marker_path: Union[str, Path],
    claim_dir: Union[str, Path],
    ledger_path: Optional[Union[str, Path]] = None,
    autoset_cwd: Optional[Union[str, Path]] = None,
    cancel_audit_path: Optional[Union[str, Path]] = None,
    cron_lister: Optional[Callable[[], dict]] = None,
    remover: Optional[Callable[..., object]] = None,
    fallback_cancelled_marker_path: Optional[Union[str, Path]] = None,
    cancel_lock_path: Optional[Union[str, Path]] = None,
    seam_audit_path: Optional[Union[str, Path]] = None,
    callback_contract: Optional[dict] = None,
    expected_chat_id: Union[int, str] = ANU_CHAT_ID,
    expected_role: str = FALLBACK_ROLE,
    post_result_review_fn: Callable[
        [dict, Classification], PostResultReview
    ] = no_real_codex_post_result_review,
    duplicate_callback_seen: Optional[list] = None,
    evidence_paths: Optional[list] = None,
    post_result_review_marker_path: Optional[Union[str, Path]] = None,
) -> CancelOnSuccessLiveResult:
    """live cancel-on-success wiring 진입점 (회장 §3 결선).

    절차:
      1. +44 durable 4-tuple registry 에서 fallback_cron_id 안전 lookup
         (+46 canonical-first root). lookup 불가/mismatch/no-binding →
         seam 미진입, cron-remove 0, fallback 보존, cancel-audit 기록.
      2. lookup VERIFIED → durable record 의 4-tuple 로
         ``NormalCallbackBinding`` 구성.
      3. +37 ``run_wired_normal_completion_callback_collector`` **경유**
         (PRIMARY — 이미 +25 → +23 seam(operational=True) 1회 → live
         cron-state verifier 5조건 AND → bound fallback cron-remove).
      4. cancel-audit JSON 생성·보장 기록. seam/cron-remove 의 어떤 예외·
         skip·실패도 collector_result 를 변경하지 않는다 (디커플 — +37/+25
         권위 그대로 + 본 모듈도 collector_result 무변 반환).
    """
    dfm = Path(dispatch_fired_marker_path)
    lookup = lookup_fallback_from_durable_registry(
        task_id=inp.task_id,
        expected_chat_id=expected_chat_id,
        expected_role=expected_role,
        ledger_path=ledger_path,
        autoset_cwd=autoset_cwd,
    )

    # ── 1. durable lookup 불가/mismatch → seam/cron-remove 만 skip ─────────
    # 회장 §3 callback mandatory rule 약화 0 (Codex HIGH 수용): lookup
    # 실패가 normal completion callback collector 자체를 우회하면 안 된다.
    # +37 binding-invalid 경로와 동형으로 frozen collector 를 **1회**
    # read-only 호출(byte-0)해 mandatory lifecycle signal 을 적출하고,
    # cancel-on-success seam/cron-remove 만 미진입한다(디커플).
    if not lookup.ok:
        target = lookup.fallback_cron_id or "<UNBOUND>"
        collector_result = run_completion_callback_collector(
            inp,
            ack_path,
            post_result_review_fn=post_result_review_fn,
            duplicate_callback_seen=duplicate_callback_seen,
            evidence_paths=evidence_paths,
            post_result_review_marker_path=post_result_review_marker_path,
        )
        durable_success = bool(
            collector_result.classification == Classification.PASS
            and collector_result.closeout_candidate is True
            and collector_result.ack_acquired is True
        )
        event_id = (
            f"{inp.task_id}|{target}|{lookup.status}"
        )
        audit = normalize_audit(
            build_cancel_on_success_audit(
                event_id=event_id,
                task_id=inp.task_id,
                target_cron_id=target,
                lookup_status=lookup.status,
                seam_invoked=False,
                remove_attempted=False,
                remove_result="NOT_ATTEMPTED",
                skip_reason=lookup.reason,
                already_removed_or_missing=False,
                canonical_root=lookup.canonical_root,
                ledger_path=lookup.ledger_path,
                notes=[
                    "durable 4-tuple registry lookup 불가/mismatch → "
                    "seam 미진입, cron-remove 0, fallback 보존 (회장 §3/§4)",
                    "디커플: lookup 실패는 normal collector 성공과 무관",
                ],
            ),
            event_id=event_id,
            task_id=inp.task_id,
            target_cron_id=target,
        )
        audit_written = _write_audit(
            audit, claim_dir=claim_dir, cancel_audit_path=cancel_audit_path,
            event_id=event_id,
        )
        return CancelOnSuccessLiveResult(
            durable_success=durable_success,
            lookup=lookup,
            wired_via_operational_collector_wiring=True,
            seam_invoked=False,
            cron_remove_invoked=False,
            fallback_preserved=True,
            normal_success_unchanged=True,
            event_id=event_id,
            cancel_audit=audit,
            cancel_audit_path=audit_written,
            collector_result=collector_result,  # mandatory — 디커플
            wired_result=None,
            notes=[
                f"durable lookup {lookup.status}: {lookup.reason}",
                "callback mandatory rule 유지: lookup 실패에도 normal "
                "completion collector 1회 실행(frozen byte-0) — seam/"
                "cron-remove 만 skip (회장 §3, Codex HIGH 수용)",
            ],
        )

    # ── 2. lookup VERIFIED → durable 4-tuple 로 binding 구성 ────────────────
    binding = NormalCallbackBinding(
        task_id=inp.task_id,
        dispatch_cron_id=lookup.dispatch_cron_id or "",
        normal_collector_cron_id=lookup.normal_collector_cron_id,
        fallback_cron_id=lookup.fallback_cron_id,
        chat_id=int(expected_chat_id),
    )

    # ── 3. +37 standard wired entrypoint 경유 (PRIMARY, +25→+23 seam) ───────
    # 디커플 권위 = +37/+25: 어떤 seam/cron-remove 예외·skip·실패도 내부
    # 흡수, collector_result 그대로. 본 모듈은 그것을 변경 없이 전달.
    wired = run_wired_normal_completion_callback_collector(
        inp,
        ack_path,
        binding=binding,
        dispatch_fired_marker_path=dfm,
        result_json_path=Path(result_json_path),
        report_path=Path(report_path),
        collector_result_marker_path=Path(collector_result_marker_path),
        claim_dir=claim_dir,
        cron_lister=cron_lister,
        remover=remover,
        fallback_cancelled_marker_path=fallback_cancelled_marker_path,
        cancel_lock_path=cancel_lock_path,
        seam_audit_path=seam_audit_path,
        callback_contract=callback_contract,
    )

    # ── 4. cancel-audit (live verifier 5조건 결과 합성) ─────────────────────
    so = wired.wiring_result.seam_outcome if wired.wiring_result else None
    five = None
    remove_result = "NOT_ATTEMPTED"
    already = False
    if so is not None:
        lv = so.live_verification or {}
        five = lv.get("checks")
        sc = so.seam_classification or ""
        already = sc in (
            "SKIP_LIVE_SKIP_ALREADY_REMOVED",
            "SKIP_LIVE_SKIP_ALREADY_FIRED",
            "SKIP_LIVE_SKIP_NOT_FOUND",
        )
        if so.cron_remove_invoked:
            remove_result = (
                "CANCELLED" if so.fallback_cancelled else "REMOVE_NONFATAL"
            )
    audit = normalize_audit(
        build_cancel_on_success_audit(
            event_id=wired.event_id,
            task_id=inp.task_id,
            target_cron_id=lookup.fallback_cron_id or "",
            lookup_status=LOOKUP_VERIFIED,
            seam_invoked=wired.seam_invoked,
            remove_attempted=bool(so.cron_remove_invoked) if so else False,
            remove_result=remove_result,
            fallback_cancelled=(
                bool(so.fallback_cancelled) if so else False
            ),
            five_condition_results=five,
            skip_reason=(so.skip_reason if so else ""),
            already_removed_or_missing=already,
            canonical_root=lookup.canonical_root,
            ledger_path=lookup.ledger_path,
            notes=[
                "live collector path → durable 4-tuple registry lookup → "
                "+37 wired entrypoint → +25 → +23 seam(operational=True) "
                "→ live verifier 5조건 → bound fallback cron-remove",
                "디커플: seam/cron-remove 결과와 무관하게 collector_result "
                "불변, normal_success_unchanged=True",
            ],
        ),
        event_id=wired.event_id,
        task_id=inp.task_id,
        target_cron_id=lookup.fallback_cron_id or "",
    )
    audit_written = _write_audit(
        audit, claim_dir=claim_dir, cancel_audit_path=cancel_audit_path,
        event_id=wired.event_id,
    )

    durable_success = bool(
        wired.collector_result.classification == Classification.PASS
        and wired.collector_result.closeout_candidate is True
        and wired.collector_result.ack_acquired is True
    )
    return CancelOnSuccessLiveResult(
        durable_success=durable_success,
        lookup=lookup,
        wired_via_operational_collector_wiring=(
            wired.wired_via_operational_collector_wiring
        ),
        seam_invoked=wired.seam_invoked,
        cron_remove_invoked=wired.cron_remove_invoked,
        fallback_preserved=wired.fallback_preserved,
        normal_success_unchanged=True,
        event_id=wired.event_id,
        cancel_audit=audit,
        cancel_audit_path=audit_written,
        collector_result=wired.collector_result,  # 디커플 — +37/+25 그대로
        wired_result=wired,
        notes=[
            "COLLECTOR_PATH live-wired: durable 4-tuple registry lookup "
            "(+44) + canonical-first root (+46) → +37 → +25 → +23 seam",
            "Layer A: 본 모듈 임의 cron 조작 0 / Layer B: durable lookup "
            "+ 5조건 verified single bound cancel 1건 (회장 §1 목표 자체)",
        ],
    )


def _write_audit(
    audit: dict,
    *,
    claim_dir: Union[str, Path],
    cancel_audit_path: Optional[Union[str, Path]],
    event_id: str,
) -> Optional[str]:
    """cancel-audit 다중 후보 보장 기록 (Layer A — NO-CRON)."""
    primary = (
        Path(cancel_audit_path)
        if cancel_audit_path is not None
        else Path(claim_dir) / f"{TASK_MARKER_PREFIX}.cancel-audit.json"
    )
    safe_eid = "".join(c if c.isalnum() else "_" for c in event_id)[:80]
    return guaranteed_write_audit(
        audit,
        primary=primary,
        fallbacks=[
            Path(claim_dir)
            / f"{TASK_MARKER_PREFIX}.cancel-audit.{safe_eid}.json"
        ],
    )


__all__ = [
    "TASK_MARKER_PREFIX",
    "LOOKUP_VERIFIED",
    "LOOKUP_NO_LEDGER_RECORD",
    "LOOKUP_NO_FALLBACK_BOUND",
    "LOOKUP_TRACK_MISMATCH",
    "LOOKUP_CHAT_MISMATCH",
    "LOOKUP_ROLE_MISMATCH",
    "LOOKUP_ERROR",
    "DurableFallbackLookup",
    "CancelOnSuccessLiveResult",
    "lookup_fallback_from_durable_registry",
    "run_cancel_on_success_live_wiring",
]
