# -*- coding: utf-8 -*-
"""anu_v3.cancel_audit_writer — cancel-on-success audit record builder/writer.

task-2553+45 (회장 지시 2 §3/§5). Standalone, pure stdlib, strict-additive.

회장 §3 verbatim: "cancel-audit JSON 을 생성한다." 본 모듈은 live
cancel-on-success 경로(``anu_v3.cancel_on_success_live_wiring``)가 산출한
관측을 ``schemas/cancel_on_success_audit.schema.json`` 에 정합하는 단일
레코드로 빌드하고, 단일 디렉터리 IO 실패가 산출 손실로 이어지지 않도록
다중 후보에 보장 기록한다 (+37 ``_guaranteed_write_audit`` /
``_normalize_cancel_audit`` 의 회장 승인·frozen 동일 패턴을 standalone
신규 모듈로 재현 — 기존 +25/+37 산출물 무수정).

Layer A / NO-CRON (9-R.1): 본 모듈은 ZERO cron register/remove, ZERO
dispatch, ZERO merge, ZERO ``cokacdir``/``subprocess`` exec. audit dict 를
빌드하고 파일로 쓰기만 한다. 감사기록 파일 IO 실패는 raise 로 전파하지
않는다 — 디커플 의무(§3: "cron-remove 실패/skip/exception 이 normal
collector success 를 실패로 바꾸지 않게 decouple") 및 +25/+37 동일 계약.
"""
from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional

#: ``schemas/cancel_on_success_audit.schema.json`` 의 schema const.
CANCEL_ON_SUCCESS_AUDIT_SCHEMA = "task-2553+45.cancel-on-success-audit_v1"

#: 단일 레코드가 항상 보유해야 하는 필수 필드 (schema required 와 동치).
REQUIRED_AUDIT_FIELDS = (
    "schema",
    "event_id",
    "task_id",
    "target_cron_id",
    "lookup_source",
    "lookup_status",
    "five_condition_results",
    "remove_attempted",
    "remove_result",
    "seam_invoked",
    "skip_reason",
    "already_removed_or_missing",
    "normal_success_unchanged",
    "wired_via_operational_collector_wiring",
)

_FIVE_CONDITION_KEYS = (
    "c1_task_id_match",
    "c2_chat_id_owned",
    "c3_role_fallback",
    "c4_marker_id_crosscheck",
    "c5_pending_not_fired_not_removed",
)

_EMPTY_FIVE = {k: None for k in _FIVE_CONDITION_KEYS}


def _now_utc() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def normalize_five_conditions(five: Optional[dict]) -> dict:
    """5조건 결과를 schema 정합 형태로 정규화 (누락 키 → None)."""
    out = dict(_EMPTY_FIVE)
    if isinstance(five, dict):
        for k in _FIVE_CONDITION_KEYS:
            if k in five:
                out[k] = five[k]
    return out


def build_cancel_on_success_audit(
    *,
    event_id: str,
    task_id: str,
    target_cron_id: str,
    lookup_status: str,
    lookup_source: str = "durable_4tuple_registry",
    seam_invoked: bool = False,
    remove_attempted: bool = False,
    remove_result: str = "NOT_ATTEMPTED",
    fallback_cancelled: bool = False,
    five_condition_results: Optional[dict] = None,
    skip_reason: str = "",
    already_removed_or_missing: bool = False,
    canonical_root: Optional[str] = None,
    ledger_path: Optional[str] = None,
    wired_via_operational_collector_wiring: bool = True,
    notes: Optional[List[str]] = None,
    ts_utc: Optional[str] = None,
) -> dict:
    """단일 cancel-on-success audit 레코드 (schema 정합).

    ``normal_success_unchanged`` 는 디커플 절대불변 — 무엇이 와도 True
    (회장 §3: cancel 실패/skip/exception 이 normal success 를 뒤집지 않음).
    """
    return {
        "schema": CANCEL_ON_SUCCESS_AUDIT_SCHEMA,
        "event_id": event_id,
        "task_id": task_id,
        "target_cron_id": target_cron_id,
        "lookup_source": lookup_source,
        "lookup_status": lookup_status,
        "five_condition_results": normalize_five_conditions(
            five_condition_results
        ),
        "remove_attempted": bool(remove_attempted),
        "remove_result": remove_result,
        "fallback_cancelled": bool(fallback_cancelled),
        "seam_invoked": bool(seam_invoked),
        "skip_reason": skip_reason,
        "already_removed_or_missing": bool(already_removed_or_missing),
        # 디커플 절대불변 — 회장 §3.
        "normal_success_unchanged": True,
        "wired_via_operational_collector_wiring": bool(
            wired_via_operational_collector_wiring
        ),
        "canonical_root": canonical_root,
        "ledger_path": ledger_path,
        "notes": list(notes or []),
        "ts_utc": ts_utc or _now_utc(),
    }


def normalize_audit(
    audit: dict, *, event_id: str, task_id: str, target_cron_id: str
) -> dict:
    """필수 필드 전수 보장 — 누락 시 안전 기본값 backfill.

    디커플 불변: ``normal_success_unchanged`` 는 무엇이 와도 True.
    """
    a = dict(audit) if isinstance(audit, dict) else {}
    a.setdefault("schema", CANCEL_ON_SUCCESS_AUDIT_SCHEMA)
    a.setdefault("event_id", event_id)
    a.setdefault("task_id", task_id)
    a.setdefault("target_cron_id", target_cron_id)
    a.setdefault("lookup_source", "durable_4tuple_registry")
    a.setdefault("lookup_status", "NOT_INVOKED")
    a["five_condition_results"] = normalize_five_conditions(
        a.get("five_condition_results")
    )
    a.setdefault("remove_attempted", False)
    a.setdefault("remove_result", "NOT_ATTEMPTED")
    a.setdefault("fallback_cancelled", False)
    a.setdefault("seam_invoked", False)
    a.setdefault("skip_reason", "")
    a.setdefault("already_removed_or_missing", False)
    a["normal_success_unchanged"] = True  # 디커플 절대불변
    a.setdefault("wired_via_operational_collector_wiring", True)
    a.setdefault("canonical_root", None)
    a.setdefault("ledger_path", None)
    a.setdefault("notes", [])
    a.setdefault("ts_utc", _now_utc())
    return a


def audit_is_complete(audit: dict) -> bool:
    """필수 필드 누락 0 여부 (regression 8 로컬 강제)."""
    return isinstance(audit, dict) and all(
        k in audit for k in REQUIRED_AUDIT_FIELDS
    )


def guaranteed_write_audit(
    audit: dict,
    *,
    primary: Optional[Path],
    fallbacks: Optional[List[Path]] = None,
) -> Optional[str]:
    """cancel-audit JSON on-disk 산출 보장 (다중 후보 순차 시도).

    단일 디렉터리 실패가 artifact 손실로 이어지지 않게 primary →
    fallback 후보에 순차 기록을 시도한다. 반환 = 최초 성공 경로(전부
    실패해도 in-memory audit dict 는 호출자 result/decision JSON 으로
    영속되어 산출 보장). OSError 를 raise 로 전파하지 않음 = 디커플 의무.
    """
    body = json.dumps(audit, ensure_ascii=False, indent=2)
    cands = [p for p in [primary, *(fallbacks or [])] if p is not None]
    for cand in cands:
        try:
            cand.parent.mkdir(parents=True, exist_ok=True)
            cand.write_text(body, encoding="utf-8")
            return str(cand)
        except OSError:
            continue
    return None


__all__ = [
    "CANCEL_ON_SUCCESS_AUDIT_SCHEMA",
    "REQUIRED_AUDIT_FIELDS",
    "normalize_five_conditions",
    "build_cancel_on_success_audit",
    "normalize_audit",
    "audit_is_complete",
    "guaranteed_write_audit",
]
