"""NORMAL_CALLBACK_NOT_REGISTERED marker emitter.

task-2694+1 NORMAL_CALLBACK_REGISTRATION_ENFORCEMENT MT-2.

Chair verbatim (#8): ".done.escalated 대체 정책 — memory/events/<task_id>.normal-callback-not-registered.json 자동 생성".

When 4-source normal callback registration validator returns FAIL, this module
emits a marker file under memory/events/. The marker carries hold_for_chair=True
which downstream consumers (finish-task.sh, qc_verify) honor to block .done
creation until the chair clears the hold.

Schema: utils.callback_registration_marker.v1
Compat: escalation_marker.v1 (parallel consumer surface).
"""

from __future__ import annotations

import json
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

MARKER_SCHEMA = "utils.callback_registration_marker.v1"
MARKER_KIND_NOT_REGISTERED = "NORMAL_CALLBACK_NOT_REGISTERED"

DEFAULT_EVENTS_DIR = "/home/jay/workspace/memory/events"


@dataclass
class MarkerEmission:
    schema: str
    task_id: str
    marker_kind: str
    marker_path: str
    payload: Dict[str, Any]
    emitted: bool
    reason: str = ""


def _marker_path_for(task_id: str, events_dir: str) -> str:
    return os.path.join(events_dir, f"{task_id}.normal-callback-not-registered.json")


def emit_not_registered_marker(
    *,
    task_id: str,
    reason: str,
    sources_checked: Optional[Dict[str, str]] = None,
    envelope_path: Optional[str] = None,
    evidence: Optional[Dict[str, Any]] = None,
    events_dir: str = DEFAULT_EVENTS_DIR,
    hold_for_chair: bool = True,
) -> MarkerEmission:
    """Emit a NORMAL_CALLBACK_NOT_REGISTERED marker file.

    Idempotent: overwrites existing marker so the latest FAIL reason is preserved.
    Atomic: write to .tmp sibling then os.replace.
    """
    if not task_id:
        raise ValueError("task_id is required")
    if not reason:
        raise ValueError("reason is required")

    # Gemini PR #155 round-2 medium: events_dir 빈값 방어 (FileNotFoundError 회피).
    _events_dir = events_dir or DEFAULT_EVENTS_DIR
    os.makedirs(_events_dir, exist_ok=True)
    marker_path = _marker_path_for(task_id, _events_dir)

    payload: Dict[str, Any] = {
        "schema": MARKER_SCHEMA,
        "task_id": task_id,
        "marker_kind": MARKER_KIND_NOT_REGISTERED,
        "verdict": "FAIL",
        "reason": reason,
        "sources_checked": dict(sources_checked) if sources_checked else {},
        "envelope_path": envelope_path,
        "evidence": dict(evidence) if evidence else {},
        "emitted_at": datetime.now(timezone.utc).isoformat(),
        "blocking": "normal_callback_not_registered",
        "hold_for_chair": bool(hold_for_chair),
        "compat_with": ["escalation_marker.v1"],
    }

    tmp_path = marker_path + ".tmp"
    try:
        with open(tmp_path, "w", encoding="utf-8") as fp:
            json.dump(payload, fp, ensure_ascii=False, indent=2)
            fp.write("\n")
            fp.flush()
            os.fsync(fp.fileno())
        os.replace(tmp_path, marker_path)
        emitted = True
        err = ""
    except OSError as exc:
        emitted = False
        err = f"write_failed: {exc!r}"
        try:
            if os.path.exists(tmp_path):
                os.unlink(tmp_path)
        except OSError:
            pass

    return MarkerEmission(
        schema=MARKER_SCHEMA,
        task_id=task_id,
        marker_kind=MARKER_KIND_NOT_REGISTERED,
        marker_path=marker_path,
        payload=payload,
        emitted=emitted,
        reason=err,
    )


def has_not_registered_marker(
    task_id: str, events_dir: str = DEFAULT_EVENTS_DIR
) -> bool:
    """Return True iff marker file exists AND payload.hold_for_chair is True.

    Corrupted marker (unparseable JSON) returns False — fail-safe behavior so
    a malformed marker cannot indefinitely block downstream gates without an
    observable parse error elsewhere.
    """
    if not task_id:
        return False
    # Gemini PR #155 round-2 medium: events_dir 빈값 방어.
    _events_dir = events_dir or DEFAULT_EVENTS_DIR
    marker_path = _marker_path_for(task_id, _events_dir)
    if not os.path.isfile(marker_path):
        return False
    try:
        with open(marker_path, "r", encoding="utf-8") as fp:
            payload = json.load(fp)
    except (OSError, json.JSONDecodeError):
        return False
    if not isinstance(payload, dict):
        return False
    return bool(payload.get("hold_for_chair", False))


def main(argv: Optional[List[str]] = None) -> int:
    """CLI: emit (FAIL marker 발행) / check (marker 존재 확인)"""
    import argparse

    ap = argparse.ArgumentParser(prog="callback_registration_marker")
    sub = ap.add_subparsers(dest="cmd", required=True)

    ep = sub.add_parser("emit")
    ep.add_argument("--task-id", required=True)
    ep.add_argument("--reason", required=True)
    ep.add_argument("--envelope-path", default=None)
    ep.add_argument("--events-dir", default=DEFAULT_EVENTS_DIR)
    ep.add_argument(
        "--sources-json",
        default=None,
        help="JSON string with sources_checked dict",
    )

    cp = sub.add_parser("check")
    cp.add_argument("--task-id", required=True)
    cp.add_argument("--events-dir", default=DEFAULT_EVENTS_DIR)

    args = ap.parse_args(argv)
    if args.cmd == "emit":
        sources = json.loads(args.sources_json) if args.sources_json else None
        em = emit_not_registered_marker(
            task_id=args.task_id,
            reason=args.reason,
            sources_checked=sources,
            envelope_path=args.envelope_path,
            events_dir=args.events_dir,
        )
        print(
            json.dumps(
                {
                    "emitted": em.emitted,
                    "marker_path": em.marker_path,
                    "payload": em.payload,
                },
                ensure_ascii=False,
                indent=2,
            )
        )
        return 0 if em.emitted else 1
    elif args.cmd == "check":
        present = has_not_registered_marker(args.task_id, args.events_dir)
        print(
            json.dumps(
                {"task_id": args.task_id, "marker_present": present},
                ensure_ascii=False,
            )
        )
        return 0 if present else 1
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
