"""Passive memory extractor — the background 'remember' brain.

Runs on EVERY user utterance in a background thread, independent of which
conversational brain answered. It decides if what Ahmed said contains a durable
fact/event/preference worth keeping, and if so saves it to the shared Railway
brain. So saving ALWAYS works — even when the tool-less local model handled the
turn, even for casual mentions Ahmed never explicitly asked to save.

Uses the local model (Ollama) for the extraction — fast, free, off the
conversation's critical path. MEM_AUTOSAVE=0 disables.
"""
from __future__ import annotations

import json
import os
import urllib.request

from voice import memory_control

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 = """You pull durable long-term memories out of what Ahmed tells his \
assistant, so they're remembered for weeks.

From his message, output ANY fact worth keeping — an event/meeting (with its \
time and people), a decision, a plan, a person, a number, a preference, a \
commitment — as ONE concise third-person sentence beginning with "Ahmed". \
Keep the specifics (names, times, places, numbers).

If the message is small talk, a command, a question, or has nothing durable, \
output exactly: NONE

Output ONLY the one sentence, or NONE. Nothing else.

Ahmed said: \"{text}\""""


def _extract(text: str) -> str:
    body = json.dumps({
        "model": MODEL, "stream": False,
        "messages": [{"role": "user", "content": _PROMPT.format(text=text)}],
        "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()


def save_if_durable(text: str, tone: str | None = None) -> None:
    """Blocking — call in a background thread. Extracts and saves a durable
    fact from `text` if there is one; otherwise does nothing. `tone` is the
    vocal delivery this was said with (e.g. 'slow, flat'); it's attached to the
    saved memory so the nightly reflection can weigh HOW he said it."""
    text = (text or "").strip()
    if os.environ.get("MEM_AUTOSAVE", "1") == "0":
        return
    if not memory_control.enabled() or len(text) < 8:
        return
    try:
        fact = _extract(text)
    except Exception as e:  # noqa: BLE001 — never disrupt the conversation
        print(f"  [memory-extract] skipped ({str(e)[:80]})")
        return
    fact = fact.strip().strip('"').strip()
    # tolerate a model that wraps or explains NONE
    if not fact or "NONE" in fact.upper()[:8] or len(fact) < 8:
        return
    memory_control.fire_save(fact, tone=tone)
    print(f"  [memory: auto-saved \"{fact[:70]}\"" +
          (f" (said {tone})]" if tone else "]"))
