"""
Geteiltes Gedächtnis (die „Verfassung") — jetzt auto-lernend & semantisch über Mem0.

Dieser Service ist nur noch ein dünner HTTP-Client auf den Mem0-Sidecar (mem0_service/app.py,
läuft im ~/.mem0/venv unter Python 3.12). Die `/api/memory`-API-Form bleibt unverändert, damit
UI und MCP-Server kompatibel bleiben. Neu gegenüber der alten flachen SQLite:

- search (q gesetzt) ist SEMANTISCH (Vektor/Embeddings) statt LIKE-Textsuche, mit Relevanz-Score.
- learn() reicht Gesprächs-Turns durch → Mem0 EXTRAHIERT Fakten selbst (Auto-Lernen).
- Dedup macht Mem0 beim Auto-Lernen selbst; der manuelle Kurator unten bleibt als Komfort.

5 Kategorien (user · instruction · stable · versioned · ephemeral) bleiben als Metadaten erhalten.
"""

import asyncio
import logging
import os
import re
from difflib import SequenceMatcher

import httpx

from config import MEM0_SERVICE_URL

log = logging.getLogger(__name__)

CATEGORIES = ("identity", "knowledge", "rules", "events")

_TIMEOUT = httpx.Timeout(60.0, connect=5.0)  # LLM-Extraktion kann ein paar Sekunden dauern


def _get(path: str, **params) -> list | dict:
    r = httpx.get(f"{MEM0_SERVICE_URL}{path}", params=params, timeout=_TIMEOUT)
    r.raise_for_status()
    return r.json()


def _post(path: str, data: dict) -> dict:
    r = httpx.post(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
    r.raise_for_status()
    return r.json()


def _put(path: str, data: dict) -> dict:
    r = httpx.put(f"{MEM0_SERVICE_URL}{path}", json=data, timeout=_TIMEOUT)
    r.raise_for_status()
    return r.json()


def _delete(path: str) -> dict:
    r = httpx.delete(f"{MEM0_SERVICE_URL}{path}", timeout=_TIMEOUT)
    r.raise_for_status()
    return r.json()


def list_memories(q: str = "", category: str = "") -> list[dict]:
    """Alle Fakten oder — wenn q gesetzt — die semantisch ähnlichsten (mit `score`)."""
    return _get("/memory", **{k: v for k, v in (("q", q), ("category", category)) if v})


def add_memory(content: str, category: str = "stable", source: str = "manual") -> dict:
    """Einen Fakt VERBATIM speichern (keine LLM-Umformung). Auto-Lernen → learn()."""
    return _post("/memory", {"content": content.strip(), "category": category, "source": source})


def update_memory(mid: str, content: str | None = None, category: str | None = None) -> dict | None:
    try:
        return _put(f"/memory/{mid}", {"content": content, "category": category})
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 404:
            return None
        raise


def delete_memory(mid: str) -> bool:
    try:
        _delete(f"/memory/{mid}")
        return True
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 404:
            return False
        raise


def learn(text: str | None = None, messages: list[dict] | None = None,
          source: str = "auto", category: str = "stable") -> dict:
    """Auto-Lernen: Text/Gesprächs-Turns durchreichen → Mem0 extrahiert die Fakten selbst."""
    return _post("/learn", {"text": text, "messages": messages,
                            "source": source, "category": category})


def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
    """Fakten als Ähnlichkeits-Graph (Knoten + semantische Kanten) für die UI-Visualisierung."""
    return _get("/graph", min_score=min_score, top_k=top_k)


def export_text() -> dict:
    rows = sorted(list_memories(), key=lambda r: (r.get("category", ""), r.get("updated_at", "")))
    lines = ["# Mission Control — Gedächtnis\n"]
    current = ""
    for r in rows:
        if r.get("category") != current:
            current = r.get("category", "")
            lines.append(f"\n## {current}\n")
        src = r.get("source", "")
        when = (r.get("updated_at") or "")[:10]
        lines.append(f"- {r.get('content', '')}  _(Quelle: {src}, {when})_")
    return {"text": "\n".join(lines), "count": len(rows)}


# --- Manueller Kurator (deterministisch, kein LLM) ---------------------------
def _norm(s: str) -> str:
    s = re.sub(r"[^\w\s]", " ", s.lower(), flags=re.UNICODE)
    return re.sub(r"\s+", " ", s).strip()


def dedupe(apply: bool = False, threshold: float = 0.85) -> dict:
    """Findet Dubletten (exakt/enthalten/ähnlich) je Kategorie, behält den längsten
    Eintrag. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier ist der
    manuelle Komfort-Knopf fürs UI (z.B. nach vielen Verbatim-Importen)."""
    rows = sorted(list_memories(), key=lambda r: (-len(r.get("content", "")), r.get("created_at", "")))
    used: set[str] = set()
    groups: list[dict] = []
    for i, a in enumerate(rows):
        if a["id"] in used:
            continue
        na = _norm(a.get("content", ""))
        if not na:
            continue
        dups = []
        for b in rows[i + 1:]:
            if b["id"] in used or b.get("category") != a.get("category"):
                continue
            nb = _norm(b.get("content", ""))
            if not nb:
                continue
            if nb in na or na in nb or SequenceMatcher(None, na, nb).ratio() >= threshold:
                dups.append(b); used.add(b["id"])
        if dups:
            used.add(a["id"])
            groups.append({
                "keep": {"id": a["id"], "content": a.get("content"), "category": a.get("category")},
                "remove": [{"id": d["id"], "content": d.get("content")} for d in dups],
            })
    dup_count = sum(len(g["remove"]) for g in groups)
    removed = 0
    if apply:
        for g in groups:
            for d in g["remove"]:
                if delete_memory(d["id"]):
                    removed += 1
    return {"groups": groups, "duplicate_count": dup_count, "removed": removed, "applied": apply}


# ── Auto-Dedupe (D16e): Dubletten von selbst wegräumen, kein Knopf-Zwang ─────────────
# Deterministisch in UNSERER Schicht (wie reminders, Verdikt 11c) statt am Hermes-cron —
# läuft als Hintergrund-Task. Höhere Schwelle als der manuelle Knopf (0.9 statt 0.85), weil
# OHNE Mensch-Review angewandt wird: lieber eine Dublette stehen lassen als etwas Distinktes
# löschen. Mem0 dedupliziert beim Auto-Lernen schon semantisch — das hier fängt den Rest
# (Verbatim-Importe, wiederholte Fakten) ab, damit das Gedächtnis nicht ohne Zutun aufbläht.
AUTO_DEDUPE_ENABLED = os.environ.get("MC_MEM_DEDUPE_ENABLED", "1") != "0"
AUTO_DEDUPE_INTERVAL = int(os.environ.get("MC_MEM_DEDUPE_INTERVAL", str(24 * 3600)))  # täglich
AUTO_DEDUPE_START_DELAY = int(os.environ.get("MC_MEM_DEDUPE_START_DELAY", "300"))     # 5 min nach Start
AUTO_DEDUPE_THRESHOLD = float(os.environ.get("MC_MEM_DEDUPE_THRESHOLD", "0.75"))


async def auto_dedupe_loop() -> None:
    """Hintergrund-Task: räumt periodisch Gedächtnis-Dubletten weg (apply=True)."""
    await asyncio.sleep(AUTO_DEDUPE_START_DELAY)   # Mem0-Sidecar nach Start hochkommen lassen
    while True:
        try:
            res = await asyncio.to_thread(dedupe, True, AUTO_DEDUPE_THRESHOLD)  # sync HTTP → Thread
            if res.get("removed"):
                log.info("mem-dedupe: %d Dublette(n) automatisch entfernt", res["removed"])
        except Exception:
            log.debug("mem-dedupe: Lauf fehlgeschlagen", exc_info=True)
        await asyncio.sleep(AUTO_DEDUPE_INTERVAL)
