"""Durable pending-notification store — so a worker's "btw…" survives a crash.

WHY (2026-07-16): worker results/flags land on the engine's in-RAM inbox
(asyncio.Queue) and are announced when Ahmed is quiet. But if Jarvis restarts
or crashes BEFORE the master found a moment to say it, the note evaporated —
Jarvis came back remembering the conversation (it resumes the session) but
having forgotten the important flag. That asymmetry is the bug.

This store persists every outbound note the instant it's created and only drops
it once it's been DELIVERED and acknowledged — not merely dequeued. On boot the
un-acked notes are reloaded so the master delivers them after the restart:
"Sir — before the restart, that email turned out wrong."

A note isn't done when it's spoken-attempt-started; it's done when `ack()` runs
after it actually reached Ahmed. Storage is one atomic JSON file
(control/pending.json), same style as the task ledger.

priority:  "fyi"    — weave in at a natural pause (default)
           "urgent" — worth breaking in for (about to do the wrong thing)
The priority is persisted for the delivery layer to use; this module just
guarantees the note is never silently lost.
"""

from __future__ import annotations

import json
import os
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path

PROJECT = Path(__file__).resolve().parent.parent
STORE = PROJECT / "control" / "pending.json"
MAX_KEEP = 100          # cap acked history so the file can't grow forever

_lock = threading.Lock()
_items: list[dict] = []
_loaded = False


def _now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def _persist_locked() -> None:
    try:
        STORE.parent.mkdir(exist_ok=True)
        tmp = STORE.with_suffix(".json.tmp")
        # keep all un-acked + the most recent acked (for a short audit tail)
        unacked = [i for i in _items if not i.get("acked")]
        acked = [i for i in _items if i.get("acked")][-MAX_KEEP:]
        tmp.write_text(json.dumps(unacked + acked, ensure_ascii=False, indent=0))
        os.replace(tmp, STORE)
    except Exception as e:  # noqa: BLE001
        print(f"       [pending: write failed: {str(e)[:120]}]")


def load() -> None:
    global _items, _loaded
    with _lock:
        if _loaded:
            return
        try:
            if STORE.is_file():
                data = json.loads(STORE.read_text() or "[]")
                if isinstance(data, list):
                    _items = data
        except Exception as e:  # noqa: BLE001
            print(f"       [pending: load failed: {str(e)[:120]}]")
            _items = []
        _loaded = True


def add(text: str, priority: str = "fyi") -> str:
    """Persist an outbound note (un-acked). Returns its id."""
    pid = "p_" + uuid.uuid4().hex[:10]
    with _lock:
        _items.append({"id": pid, "text": text, "priority": priority,
                       "created": _now(), "acked": False})
        _persist_locked()
    return pid


def unacked() -> list[dict]:
    """Notes still awaiting delivery — reloaded into the inbox on boot."""
    with _lock:
        return [dict(i) for i in _items if not i.get("acked")]


def ack(texts: list[str]) -> None:
    """Mark notes delivered. Matches by exact text (oldest un-acked first), so
    the same rail that carried plain strings needs no id plumbing."""
    if not texts:
        return
    with _lock:
        changed = False
        for txt in texts:
            for i in _items:
                if not i.get("acked") and i.get("text") == txt:
                    i["acked"] = True
                    i["delivered"] = _now()
                    changed = True
                    break
        if changed:
            _persist_locked()


def ack_all() -> None:
    with _lock:
        changed = False
        for i in _items:
            if not i.get("acked"):
                i["acked"] = True
                i["delivered"] = _now()
                changed = True
        if changed:
            _persist_locked()
