"""Auto-supersession — temporal versioning of memory.

When a new durable fact UPDATES one already stored (a client's ticket size
changed, a plan moved, a status flipped), we don't want two contradictory facts
sitting side by side — the latest should win while the old is kept as history
(queryable, never deleted). This asks the LOCAL model whether the new fact
replaces one of the few most-similar existing facts and returns that fact's id
(else None); the engine then calls /supersede instead of /remember. Free (local
model). MEM_SUPERSEDE=0 disables.

FALLBACK PATH ONLY: the memory service now owns dedup/supersede inside its own
server-side write pipeline, so this client-side round-trip runs only when
MEM_SERVER_PIPELINE=0 is set (see voice/memory_control.py fire_save). Kept
importable for that escape hatch.
"""
from __future__ import annotations

import json
import os
import urllib.request

OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.environ.get("MEM_EXTRACT_MODEL",
                       os.environ.get("LOCAL_MODEL", "qwen3:4b-instruct"))

_PROMPT = """A new fact about Ahmed just arrived. Decide if it is an UPDATE that \
REPLACES one of the existing facts below — i.e. it describes the SAME thing (same \
person/company and same attribute) but with a CHANGED or newer value or status. \
It is NOT a replacement if it is merely related, additional, or about something \
else — only say it replaces one when the new value genuinely supersedes the old.

New fact: "{new}"

Existing facts (id: text):
{cands}

If the new fact replaces exactly ONE existing fact, output ONLY that fact's id.
If it replaces none, output exactly: NONE
Output only the id or NONE — nothing else."""


def _ask(prompt: str) -> str:
    """Judge with DeepSeek V4 Flash (smart + cheap) if a key is set, else the
    local model. Judging supersession needs more nuance than qwen3:4b has."""
    try:
        from voice import deepseek
        if deepseek.available():
            return deepseek.chat(
                prompt, model=os.environ.get("MEM_JUDGE_MODEL", deepseek.FLASH),
                max_tokens=256, timeout=30)   # V4 needs room to reason first
    except Exception:  # noqa: BLE001 — fall back to local
        pass
    try:
        body = json.dumps({
            "model": MODEL, "stream": False,
            "messages": [{"role": "user", "content": prompt}],
            "options": {"temperature": 0},
        }).encode()
        req = urllib.request.Request(f"{OLLAMA}/api/chat", data=body,
                                     headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=30) as r:
            o = json.loads(r.read())
        return ((o.get("message") or {}).get("content") or "").strip()
    except Exception:  # noqa: BLE001
        return ""


def find_superseded(new_text: str, candidates: list[dict]) -> str | None:
    """Return the id of the existing fact the new one replaces, or None."""
    if os.environ.get("MEM_SUPERSEDE", "1") == "0":
        return None
    new_text = (new_text or "").strip()
    cands = [c for c in (candidates or []) if c.get("id") and c.get("fact")][:6]
    if len(new_text) < 8 or not cands:
        return None
    ids = {c["id"] for c in cands}
    listing = "\n".join(f'{c["id"]}: {c["fact"]}' for c in cands)
    prompt = _PROMPT.format(new=new_text, cands=listing)
    ans = _ask(prompt)
    if not ans:
        return None
    # tolerate the model wrapping the id in quotes or a sentence
    for tok in ans.replace('"', " ").replace("'", " ").replace(":", " ").split():
        if tok in ids:
            return tok
    return None
