"""Automatische Erinnerungs-Extraktion.

Nach einigen Gespraechsturns destilliert ein LLM dauerhafte Fakten/Vorlieben
ueber den Nutzer aus dem Verlauf und legt sie als Nutzer-Erinnerungen ab.

Bewusst **best-effort und nicht-blockierend**: Die Extraktion laeuft als
Hintergrund-Task und darf die Antwortlatenz nie erhoehen. Schlaegt sie fehl
(LLM-Fehler, kaputtes JSON), ist die Folge nur "kein neuer Fakt" - niemals ein
Fehler im Antwort-Turn.
"""

import asyncio
import json
import logging
import re

from app.config import Settings, settings

logger = logging.getLogger(__name__)

# Turn-Zaehler pro Session (in-memory, bewusst kein DB-Schema-Eingriff).
_turn_counts: dict[str, int] = {}
# Referenzen auf laufende Tasks halten, damit sie nicht vorzeitig vom GC kassiert werden.
_pending: set[asyncio.Task] = set()

_EXTRACTION_SYSTEM_PROMPT = (
    "Du extrahierst dauerhafte, langfristig relevante Fakten und Vorlieben ueber den "
    "Nutzer aus einem Gespraech (z. B. Name, Wohnort, Familie, Gesundheit, Hobbys, "
    "Vorlieben, Abneigungen, feste Routinen). Gib AUSSCHLIESSLICH ein JSON-Array "
    "kurzer deutscher Strings zurueck, ohne Erklaerung und ohne Markdown. Nimm nur "
    "NEUE Fakten auf, die nicht bereits bekannt sind. Ignoriere fluechtige oder rein "
    "situative Aussagen. Gibt es nichts Neues, antworte mit []."
)


def _build_extractor_llm(cfg: Settings):
    """Baut eine eigene LLM-Instanz fuer die Extraktion (nicht der Sprach-Provider).

    Fuer den lokalen Provider wird der Extraktions-System-Prompt direkt gesetzt
    (der Chat-Provider ist auf kurze, vorlesbare Saetze getrimmt und taugt nicht
    fuer JSON). Fuer andere Provider wird der generische Provider verwendet; die
    Anweisung steckt dann zusaetzlich in der Nachricht selbst.
    """
    provider = cfg.memory_extraction_provider or cfg.default_llm_provider
    if provider == "local-openai-compatible":
        from app.providers.llm.local_openai_compatible import LocalOpenAICompatibleLLM

        return LocalOpenAICompatibleLLM(
            cfg.local_llm_base_url,
            cfg.local_llm_api_key,
            cfg.local_llm_model,
            system_prompt=_EXTRACTION_SYSTEM_PROMPT,
            disable_reasoning=True,
            max_tokens=512,
            temperature=0.1,
        )

    from app.dependencies import get_llm_provider

    return get_llm_provider(provider, cfg)


def _format_conversation(messages: list[dict]) -> str:
    lines = []
    for msg in messages:
        role = "Nutzer" if msg.get("role") == "user" else "Assistent"
        content = (msg.get("content") or "").strip()
        if content:
            lines.append(f"{role}: {content}")
    return "\n".join(lines)


def parse_facts(raw: str) -> list[str]:
    """Liest ein JSON-Array von Fakt-Strings aus der (evtl. verrauschten) LLM-Antwort."""
    if not raw:
        return []
    match = re.search(r"\[.*\]", raw, re.DOTALL)
    if not match:
        return []
    try:
        data = json.loads(match.group(0))
    except ValueError:
        return []
    if not isinstance(data, list):
        return []
    facts = []
    for item in data:
        if isinstance(item, str):
            fact = item.strip()
            if fact:
                facts.append(fact)
    return facts


def _norm(text: str) -> str:
    return " ".join(text.lower().split())


async def extract_and_store(store, user_id: str, session_id: str, cfg: Settings = settings) -> int:
    """Extrahiert neue Fakten und speichert sie. Liefert die Anzahl neu gespeicherter."""
    messages = store.get_recent_messages(session_id, cfg.history_max_messages)
    conversation = _format_conversation(messages)
    if not conversation:
        return 0

    existing = store.get_memories(user_id)
    if len(existing) >= cfg.memory_extraction_max:
        return 0

    known = [m.content for m in existing]
    known_text = "\n".join(f"- {k}" for k in known) if known else "(noch nichts bekannt)"
    user_prompt = (
        f"Bereits bekannt:\n{known_text}\n\n"
        f"Gespraech:\n{conversation}\n\n"
        "Neue Fakten als JSON-Array:"
    )

    llm = _build_extractor_llm(cfg)
    raw = await llm.complete(user_prompt)
    facts = parse_facts(raw)
    if not facts:
        return 0

    seen = {_norm(k) for k in known}
    added = 0
    for fact in facts:
        if len(existing) + added >= cfg.memory_extraction_max:
            break
        key = _norm(fact)
        if key in seen:
            continue
        seen.add(key)
        store.add_memory(user_id, fact)
        added += 1
    if added:
        logger.info("memory-extraction: %d neue Erinnerung(en) fuer %s", added, user_id)
    return added


async def _run_safe(store, user_id: str, session_id: str, cfg: Settings) -> None:
    try:
        await extract_and_store(store, user_id, session_id, cfg)
    except Exception:  # best-effort: niemals den Turn beeintraechtigen
        logger.exception("memory-extraction fehlgeschlagen (ignoriert)")


def maybe_schedule_extraction(store, user_id: str, session_id: str | None,
                              cfg: Settings = settings) -> asyncio.Task | None:
    """Plant die Extraktion als Hintergrund-Task, sofern aktiviert und N Turns erreicht.

    Gibt den geplanten Task zurueck (oder None) - blockiert nie.
    """
    if not cfg.memory_extraction_enabled or not session_id:
        return None
    every = max(1, cfg.memory_extraction_every_n_turns)
    count = _turn_counts.get(session_id, 0) + 1
    _turn_counts[session_id] = count
    if count % every != 0:
        return None

    task = asyncio.create_task(_run_safe(store, user_id, session_id, cfg))
    _pending.add(task)
    task.add_done_callback(_pending.discard)
    return task
