"""Lern-Engine: Vokabel-Mastery-Tracking + Spaced Repetition + Transkript-Bewertung.

Umsetzung der Forschungs-Spec `plappi-learning-engine` (2026-06-22):
- 5-Zustands-Mastery-Maschine (new→receptive→practising→productive→mastered→retired)
  mit Leitner-Box-Scheduler und Spontan-Produktions-Fast-Track.
- Evidenz kommt aus einer LLM-Bewertung des Gesprächs-Transkripts (lemmatisiert,
  morphologie-robust für Serbisch), NICHT aus naivem Substring-Zählen der Kind-Turns.
- Fluency-Fast-Path: spricht das Kind spontan flüssig, werden Wörter sofort gemeistert
  und höhere Tiers freigeschaltet (dein „Kiki sagt cao, kako si, šta radiš…"-Fall).

Single-Child-MVP (child_id=1), aber Tabelle ist multi-child-fähig vorbereitet.
"""
from __future__ import annotations

import json
import re
import sqlite3
import subprocess
import time
import unicodedata

from . import config, curriculum

DB = config.DATA_DIR / "plappi.sqlite"
CHILD_ID = 1
STATES = ["new", "receptive", "practising", "productive", "mastered", "retired"]

# ── Transkript-Bewertungs-Prompt (basiert auf der Spec, mit klar parsebaren JSON-Feldern) ──
ANALYSIS_PROMPT = """Du bist eine Bewertungs-Engine für Serbisch als Herkunftssprache (Tutor „Plappi", deutsch-dominante Kinder). Du bekommst EIN Gesprächs-Transkript (Turns 'plappi' und 'child'; Kind-Turns sind rohes ASR eines jungen Kindes: erwarte Rauschen, deutsche Füller, phonetische Schreibweisen) plus Ziel-Vokabeln (serbische LEMMATA + deutsche Glosse + Tier). Aufgabe = morphologische + pragmatische Bewertung, KEINE Übersetzung.

HARTE REGELN:
1. LEMMATISIERE jede Oberflächenform des KINDES auf ihr Lemma (Nomen=Nom.Sg, Verb=Infinitiv, Adj=mask.Nom.Sg). Beispiele: radis/radim->raditi; bila/bio->biti; vrticu->vrtic; macku->macka; idem/isla->ici; dobra/dobro->dobar; dve->dva. Akzeptiere ekavisch+ijekavisch (lepo/lijepo) und phonetisches ASR (macka=mačka, fala=hvala).
2. Bewerte gegen die Ziel-Lemmata; spontane NICHT-Ziel-Lemmata listest du in extra_produced.
3. Pro Lemma der STÄRKSTE Beleg im ganzen Transkript: spontaneous (Kind nutzt es korrekt in EIGENER Äußerung, NICHT als Echo des direkt davor von Plappi gesagten Worts) > prompted (korrekt direkt nach Plappis Vorsagen) > understood (sagt es nicht, reagiert aber passend) > struggled (falsch/falsches Wort/langes Zögern/antwortet deutsch wo Serbisch klar verlangt). Jedes Lemma in GENAU einen Topf.
4. Flexionsfehler bei richtigem Lemma = produced (NICHT struggled). struggled nur wenn das Lemma selbst falsch/abwesend ist.
5. Konservativ: bei <0.6 Konfidenz NICHT als produced markieren. Nie Evidenz erfinden.
6. level_0_100 nach Breite+Spontaneität: 0-10 nur Deutsch; 11-25 einzelne nachgesprochene Wörter; 26-45 A1 mehrere spontane Wörter/2-Wort-Kombis; 46-65 A2 kurze spontane Phrasen, eine Zeitform/Kasus, Fragen; 66-85 B1 verbundene Mehrsatz-Rede; 86-100 B2+. Spontane >=4-Wort-Äußerung mit Tempus/Kasus/Frage => >=55 (A2+).
7. fluency_flag=true, wenn das Kind unprompted >=4 serbische Inhaltswörter ODER eine grammatisch markierte Konstruktion (Tempus/Kasus/Frage) ÜBER dem aktuellen Tier produziert.

Gib AUSSCHLIESSLICH striktes JSON zurück (keine Prosa, kein Markdown), exakt:
{"produced":["lemma",...], "spontaneous":["lemma",...], "understood":["lemma",...], "struggled":["lemma",...], "extra_produced":["lemma",...], "level_0_100":58, "cefr":"A2", "fluency_flag":false, "transcript_quality":"ok", "note":"kurze Begründung"}
- produced = alle korrekt produzierten Ziel-Lemmata; spontaneous = die Teilmenge davon, die spontan (nicht nachgesprochen) kam.
- transcript_quality: ok|noisy|too_short (Kind <2 wertbare Turns => too_short).

ZIEL-VOKABULAR (lemma = glosse [tier]):
{TARGET_VOCAB}

TRANSKRIPT:
{TRANSCRIPT}

Gib jetzt das JSON zurück."""


def _norm(s: str) -> str:
    s = unicodedata.normalize("NFKD", (s or "").lower())
    return "".join(c for c in s if not unicodedata.combining(c))


# normalisierte Oberflächen → Lemma (für Plappi-Expositions-Zählung)
_SURFACE: dict[str, str] = {}
for _it in curriculum.ITEMS:
    _lem = curriculum.lemma_of(_it)
    for _form in (_it.get("sr", ""), _lem):
        nf = _norm(_form)
        if nf:
            _SURFACE.setdefault(nf, _lem)
_TIER = {curriculum.lemma_of(i): i.get("tier", 1) for i in curriculum.ITEMS}
_WTYPE = {curriculum.lemma_of(i): i.get("type", "") for i in curriculum.ITEMS}


def _today_days() -> int:
    return int(time.time() // 86400)


def _conn():
    c = sqlite3.connect(DB)
    c.row_factory = sqlite3.Row
    return c


def init():
    c = _conn()
    c.executescript("""
    CREATE TABLE IF NOT EXISTS lemma_mastery(
        child_id INT, lemma TEXT, tier INT, word_type TEXT,
        state TEXT DEFAULT 'new', box INT DEFAULT 0, ease REAL DEFAULT 1.0,
        exposure_credits REAL DEFAULT 0,
        prod_spontaneous INT DEFAULT 0, prod_prompted INT DEFAULT 0,
        understood INT DEFAULT 0, struggled INT DEFAULT 0,
        distinct_sessions_produced INT DEFAULT 0,
        last_seen_days INT, next_due_days INT, teach_weight INT DEFAULT 1,
        PRIMARY KEY(child_id, lemma));
    CREATE TABLE IF NOT EXISTS scored_convs(conv_id TEXT PRIMARY KEY, scored_at INT);
    CREATE TABLE IF NOT EXISTS child_state(
        child_id INT PRIMARY KEY, level REAL DEFAULT 0, cefr TEXT DEFAULT 'pre-A1',
        max_tier_unlocked INT DEFAULT 1, updated_at INT, level_floor REAL DEFAULT 0);
    """)
    try:                                            # Migration für ältere DBs
        c.execute("ALTER TABLE child_state ADD COLUMN level_floor REAL DEFAULT 0")
    except Exception:
        pass
    c.commit(); c.close()


# ── claude-CLI Bewertung ─────────────────────────────────────────────────────
def _call_claude(prompt: str) -> str:
    base = [config.CLAUDE_CLI, "-p", prompt, "--model", config.LEARNING_MODEL]
    attempts = [
        base + ["--output-format", "text", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}'],
        base + ["--output-format", "text"],
    ]
    for cmd in attempts:
        try:
            r = subprocess.run(cmd, capture_output=True, text=True,
                               timeout=config.LEARNING_TIMEOUT, cwd=str(config.BRAIN_CWD))
            out = (r.stdout or "").strip()
            if r.returncode == 0 and out and not out.startswith("Error:"):
                return out
        except Exception:
            continue
    return ""


def _extract_json(text: str) -> dict | None:
    if not text:
        return None
    s, e = text.find("{"), text.rfind("}")
    if s < 0 or e <= s:
        return None
    try:
        return json.loads(text[s:e + 1])
    except Exception:
        return None


def score_conversation(turns: list[dict]) -> dict | None:
    """Bewertet ein Transkript via claude-CLI → lemmatisierte Evidenz + Level."""
    child_turns = [t for t in turns if t.get("role") == "child" and (t.get("text") or "").strip()]
    if len(child_turns) < 1:
        return None
    lines = [("Plappi" if t.get("role") == "plappi" else "Kind") + ": " + (t.get("text") or "")
             for t in turns]
    # Nur Kern-Tiers als Ziel-Liste (Tempo): produzierte Wörter darüber kommen via
    # extra_produced rein. Volle 210 Wörter machen den Prompt zu langsam (>180s).
    prompt = (ANALYSIS_PROMPT
              .replace("{TARGET_VOCAB}", curriculum.target_vocab_str(limit=80))
              .replace("{TRANSCRIPT}", "\n".join(lines)))
    return _extract_json(_call_claude(prompt))


def _plappi_exposures(turns: list[dict]) -> dict[str, int]:
    """Zählt, welche Lehrplan-Lemmas Plappi gesagt hat (saubere Modell-Texte)."""
    counts: dict[str, int] = {}
    for t in turns:
        if t.get("role") != "plappi":
            continue
        n = _norm(t.get("text") or "")
        for nf, lem in _SURFACE.items():
            if re.search(rf"\b{re.escape(nf)}\b", n):
                counts[lem] = counts.get(lem, 0) + 1
    return counts


# ── Mastery-Zustandsmaschine ─────────────────────────────────────────────────
def _row(c, lemma: str) -> dict:
    r = c.execute("SELECT * FROM lemma_mastery WHERE child_id=? AND lemma=?", (CHILD_ID, lemma)).fetchone()
    if r:
        return dict(r)
    return {"child_id": CHILD_ID, "lemma": lemma, "tier": _TIER.get(lemma, 1),
            "word_type": _WTYPE.get(lemma, ""), "state": "new", "box": 0, "ease": 1.0,
            "exposure_credits": 0, "prod_spontaneous": 0, "prod_prompted": 0,
            "understood": 0, "struggled": 0, "distinct_sessions_produced": 0,
            "last_seen_days": None, "next_due_days": None, "teach_weight": 1}


def _save(c, r: dict):
    c.execute("""INSERT OR REPLACE INTO lemma_mastery
        (child_id,lemma,tier,word_type,state,box,ease,exposure_credits,prod_spontaneous,
         prod_prompted,understood,struggled,distinct_sessions_produced,last_seen_days,
         next_due_days,teach_weight)
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
        (r["child_id"], r["lemma"], r["tier"], r["word_type"], r["state"], r["box"], r["ease"],
         r["exposure_credits"], r["prod_spontaneous"], r["prod_prompted"], r["understood"],
         r["struggled"], r["distinct_sessions_produced"], r["last_seen_days"],
         r["next_due_days"], r["teach_weight"]))


def _schedule(r: dict, today: int):
    """Leitner: next_due aus aktueller Box + ease."""
    box = max(0, min(r["box"], len(config.BOX_DAYS) - 1))
    r["next_due_days"] = today + max(1, round(config.BOX_DAYS[box] * (r.get("ease") or 1.0)))


def _transition(r: dict, today: int):
    """Zustandsübergänge gemäß Spec (mehrstufig in einem Schritt erlaubt)."""
    if r["state"] in ("mastered", "retired"):
        return
    produced = r["prod_prompted"] + r["prod_spontaneous"]
    if r["state"] == "new" and (r["exposure_credits"] >= config.EXPOSURES_FOR_RECEPTIVE
                                or r["understood"] >= 1 or produced >= 1):
        r["state"] = "receptive"
    if r["state"] == "receptive" and produced >= 1:
        r["state"] = "practising"
    if r["state"] == "practising" and (
        (r["exposure_credits"] >= config.EXPOSURES_FOR_PRODUCTIVE_BASELINE
         and r["distinct_sessions_produced"] >= config.DISTINCT_SESSIONS_FOR_MASTERED)
        or r["prod_spontaneous"] >= 1):
        r["state"] = "productive"
    if r["state"] == "productive" and r["prod_spontaneous"] >= 2 and r["distinct_sessions_produced"] >= 2:
        _master(r, today)


def _master(r: dict, today: int):
    r["state"] = "mastered"
    r["teach_weight"] = 0
    r["box"] = max(r["box"], 5)
    r["next_due_days"] = today + config.CONFIRMATION_REVIEW_DAYS


def _cefr_to_tier(cefr: str) -> int:
    c = (cefr or "").upper()
    if c.startswith("A1"):
        return 2
    if c.startswith("A2"):
        return 4
    if c.startswith("B") or c.startswith("C"):
        return 6
    return 1


def apply_conversation(conv_id: str, conv_started: int, turns: list[dict]) -> dict:
    """Wertet EIN Gespräch aus (idempotent) → aktualisiert Mastery + Level."""
    init()
    c = _conn()
    if c.execute("SELECT 1 FROM scored_convs WHERE conv_id=?", (conv_id,)).fetchone():
        c.close()
        return {"ok": True, "skipped": "already_scored"}

    today = _today_days()
    day = int(conv_started // 86400) if conv_started else today
    scored = score_conversation(turns) or {}
    exposures = _plappi_exposures(turns)

    def L(key):
        return [str(x).strip().lower() for x in (scored.get(key) or []) if str(x).strip()]
    produced, spontaneous = set(L("produced")), set(L("spontaneous"))
    understood, struggled = set(L("understood")), set(L("struggled"))
    extra = set(L("extra_produced"))
    fluency = bool(scored.get("fluency_flag"))

    touched: set[str] = set(exposures) | produced | spontaneous | understood | struggled
    touched &= set(_TIER.keys())                       # nur bekannte Lehrplan-Lemmas

    for lem in touched:
        r = _row(c, lem)
        r["last_seen_days"] = day
        add_exp = min(exposures.get(lem, 0), config.MAX_EXPOSURES_PER_SESSION)
        r["exposure_credits"] += add_exp               # Plappi-Exposition: Gewicht 1
        correct = False
        if lem in spontaneous:
            r["prod_spontaneous"] += 1
            r["exposure_credits"] += config.WEIGHT_SPONTANEOUS
            correct = True
        elif lem in produced:
            r["prod_prompted"] += 1
            r["exposure_credits"] += config.WEIGHT_PROMPTED
            correct = True
        if lem in understood:
            r["understood"] += 1
            r["exposure_credits"] += 1
            correct = correct or True
        if lem in (produced | spontaneous):
            r["distinct_sessions_produced"] += 1       # 1 Gespräch = 1 Sitzung
        if lem in struggled and lem not in (produced | spontaneous):
            r["struggled"] += 1
            r["box"] = max(0, r["box"] - 2)
            r["ease"] = max(0.7, (r.get("ease") or 1.0) - 0.2)
        elif correct:
            r["box"] = min(len(config.BOX_DAYS) - 1, r["box"] + 1)
            if lem in spontaneous:
                r["ease"] = min(1.5, (r.get("ease") or 1.0) + 0.15)
        _transition(r, today)
        if r["state"] not in ("mastered", "retired"):
            _schedule(r, today)
        _save(c, r)

    # Fluency-Fast-Path: spontane Flüssigkeit → alles Produzierte sofort meistern
    if fluency:
        for lem in (produced | spontaneous | extra) & set(_TIER.keys()):
            r = _row(c, lem)
            r["last_seen_days"] = day
            r["prod_spontaneous"] = max(r["prod_spontaneous"], 2)
            r["distinct_sessions_produced"] = max(r["distinct_sessions_produced"], 2)
            _master(r, today)
            _save(c, r)

    c.execute("INSERT OR REPLACE INTO scored_convs VALUES(?,?)", (conv_id, int(time.time())))
    c.commit()
    _update_level(c, scored, fluency)
    c.commit(); c.close()
    return {"ok": True, "level": scored.get("level_0_100"), "cefr": scored.get("cefr"),
            "produced": len(produced), "fluency": fluency,
            "quality": scored.get("transcript_quality")}


def _update_level(c, scored: dict, fluency: bool):
    st = c.execute("SELECT * FROM child_state WHERE child_id=?", (CHILD_ID,)).fetchone()
    cur_level = float(st["level"]) if st else 0.0
    cur_tier = int(st["max_tier_unlocked"]) if st else 1
    target = scored.get("level_0_100")
    quality = scored.get("transcript_quality")
    cefr = scored.get("cefr") or (st["cefr"] if st else "pre-A1")
    retired = c.execute("SELECT COUNT(*) n FROM lemma_mastery WHERE child_id=? AND state IN('mastered','retired')",
                        (CHILD_ID,)).fetchone()["n"]
    try:
        floor = float(st["level_floor"]) if st else 0.0
    except (KeyError, IndexError, TypeError):
        floor = 0.0
    new_level = cur_level
    if isinstance(target, (int, float)) and quality != "too_short":
        diff = target - cur_level
        if diff >= 0:
            new_level = cur_level + 0.7 * diff               # Anstieg: schnell
        elif diff >= -15:
            new_level = cur_level + 0.2 * diff               # kleiner Abfall: gedämpft
        # großer Einbruch (>15 Punkte) in EINER Session wird ignoriert
    if fluency:                                              # Böden sind PERSISTENT
        floor = max(floor, 45.0)
    if retired >= 5:
        floor = max(floor, 25.0)
    new_level = max(new_level, floor)
    new_tier = max(cur_tier, _cefr_to_tier(cefr)) if fluency else cur_tier
    c.execute("""INSERT OR REPLACE INTO child_state
                 (child_id, level, cefr, max_tier_unlocked, updated_at, level_floor)
                 VALUES(?,?,?,?,?,?)""",
              (CHILD_ID, round(new_level, 1), cefr, new_tier, int(time.time()), round(floor, 1)))


# ── Lese-APIs (für Prompt-Feed + Dashboard) ──────────────────────────────────
def get_mastery_rows() -> list[dict]:
    init()
    c = _conn()
    rows = [dict(r) for r in c.execute("SELECT * FROM lemma_mastery WHERE child_id=?", (CHILD_ID,)).fetchall()]
    c.close()
    return rows


def get_child_state() -> dict:
    init()
    c = _conn()
    r = c.execute("SELECT * FROM child_state WHERE child_id=?", (CHILD_ID,)).fetchone()
    c.close()
    return dict(r) if r else {"level": 0, "cefr": "pre-A1", "max_tier_unlocked": 1}


def focus_for_prompt(age) -> dict:
    """Liefert Fokus-/gemeisterte Wörter + Band-Methode + Level für den Agent-Prompt."""
    rows = get_mastery_rows()
    state = get_child_state()
    sel = curriculum.select_focus(age, rows, _today_days(),
                                  max_tier_unlocked=state.get("max_tier_unlocked"))
    mastered = [r["lemma"] for r in rows if r.get("state") in ("mastered", "retired")]
    focus_str = ", ".join(f"{f['sr']} ({f['de']})" for f in sel["focus"]) or "—"
    mastered_str = ", ".join(sorted(mastered)) or "—"
    lvl = round(state.get("level", 0) or 0)
    level_label = f"{lvl}/100 ({state.get('cefr','pre-A1')})"
    return {"band": sel["band"], "focus": sel["focus"], "frames": sel.get("frames", []),
            "focus_str": focus_str, "mastered_str": mastered_str,
            "level_label": level_label, "method": sel["band"].get("method", "")}


def mastery_summary() -> dict:
    rows = get_mastery_rows()
    state = get_child_state()
    by_state = {s: 0 for s in STATES}
    for r in rows:
        by_state[r.get("state", "new")] = by_state.get(r.get("state", "new"), 0) + 1
    words = sorted(rows, key=lambda r: (STATES.index(r.get("state", "new")) if r.get("state") in STATES else 0,
                                        -(r.get("prod_spontaneous", 0))), reverse=True)
    return {
        "level": round(state.get("level", 0) or 0),
        "cefr": state.get("cefr", "pre-A1"),
        "max_tier": state.get("max_tier_unlocked", 1),
        "counts": {"mastered": by_state["mastered"] + by_state["retired"],
                   "learning": by_state["receptive"] + by_state["practising"] + by_state["productive"],
                   "tracked": len(rows)},
        "by_state": by_state,
        "words": [{"sr": curriculum.BY_LEMMA.get(r["lemma"], {}).get("sr", r["lemma"]),
                   "de": curriculum.BY_LEMMA.get(r["lemma"], {}).get("de", ""),
                   "lemma": r["lemma"], "state": r.get("state"),
                   "spontaneous": r.get("prod_spontaneous", 0),
                   "box": r.get("box", 0)} for r in words],
    }
