# Long-conversation simulator: replays a realistic ~50-turn conversation (chat
# mixed with commands, Spanish, follow-ups, multi-commands, memory statements)
# through the REAL pipeline pieces: wivexx_i18n (lang detect + canonicalize) →
# wivexx_intent_gate → the waterfall's ACTUAL trigger regexes (AST-extracted)
# → wivexx_memory_store rule extraction into a TEMP db.
# For each turn we compare the computed route against the expected one.
import ast
import os
import re
import sys
import tempfile

PROJ = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJ)
import wivexx_i18n as i18n
import wivexx_intent_gate as gate
import wivexx_memory_store as ms

ms._DB_PATH = os.path.join(tempfile.mkdtemp(prefix="wvx_sim_"), "sim.db")
ms._ensured_for_path = ""

# ── extract the real trigger regexes ─────────────────────────────────────────
FLAGS = {"IGNORECASE": re.IGNORECASE, "I": re.IGNORECASE, "VERBOSE": re.VERBOSE,
         "X": re.VERBOSE, "DOTALL": re.DOTALL, "MULTILINE": re.MULTILINE}
def flag_val(node):
    if isinstance(node, ast.Attribute):
        return FLAGS.get(node.attr, 0)
    if isinstance(node, ast.BinOp):
        return flag_val(node.left) | flag_val(node.right)
    return 0
RX = {}
tree = ast.parse(open(os.path.join(PROJ, "wivexx_api.py"), encoding="utf-8").read())
for node in ast.walk(tree):
    if (isinstance(node, ast.Assign) and len(node.targets) == 1
            and isinstance(node.targets[0], ast.Name) and node.targets[0].id.endswith("_RE")
            and isinstance(node.value, ast.Call) and getattr(node.value.func, "attr", "") == "compile"
            and node.value.args and isinstance(node.value.args[0], ast.Constant)):
        fl = flag_val(node.value.args[1]) if len(node.value.args) > 1 else 0
        try:
            RX[node.targets[0].id] = re.compile(node.value.args[0].value, fl)
        except re.error:
            pass

REGISTRY = {"discord", "spotify", "chrome", "steam", "obs", "lightroom",
            "premiere pro", "premiere", "notepad"}

# waterfall approximation: ordered (route_name, trigger regex name)
ORDER = [
    ("selfclose", "_SELF_CLOSE_RE"), ("capability", "_CAPABILITY_QUERY_RE"),
    ("timer_status", "_ACTIVE_TIMER_QUERY_RE"), ("time", "_CURRENT_TIME_RE"),
    ("date", "_CURRENT_DATE_RE"), ("version", "_VERSION_QUERY_RE"),
    ("tldr", "_LATEST_UPDATE_RE"), ("battery", "_BATTERY_RE"),
    ("workmode_on", "_WORK_MODE_ON_RE"), ("workmode_off", "_WORK_MODE_OFF_RE"),
    ("remember", "_REMEMBER_RE"), ("forget", "_FORGET_RE"),
    ("recall", "_WHAT_REMEMBER_RE"), ("goal_add", "_ADD_GOAL_RE"),
    ("goal_done", "_COMPLETE_GOAL_RE"), ("goals", "_GOALS_QUERY_RE"),
    ("social", "_SOCIAL_REPORT_RE"), ("caption", "_CAPTION_RE"),
    ("disk", "_DISK_ANALYZE_RE"), ("pending", "_PENDING_QUERY_RE"),
    ("shutdown_cancel", "_SHUTDOWN_CANCEL_RE"), ("shutdown", "_SHUTDOWN_RE"),
    ("joke", "_JOKE_RE"), ("youtube", "_YOUTUBE_RE"), ("note", "_NOTE_TRIGGER_RE"),
    ("media", "_MEDIA_KEY_RE"),                      # ← new section (may not exist yet)
    ("audio_out", "_AUDIO_OUTPUT_RE"), ("volume", "_VOLUME_RE"),
    ("weather", "_WEATHER_TRIGGER_RE"), ("calc", "_CALC_RE"),
    ("clipboard", "_CLIP_RE"), ("winctl", "_WINCTL_GATE_RE"),
    ("cal_create", "_CAL_CREATE_RE"), ("cal_query", "_CAL_QUERY_RE"),
    ("gaming", "_GAMING_MODE_RE"), ("speedtest", "_SPEEDTEST_RE"),
    ("optimize", "_OPTIMIZE_RE"), ("hud", "_PERF_HUD_RE"),
    ("enroll", "_ENROLL_RE"), ("label", "_SCREEN_LABEL_RE"),
]

_PRONOUNS = {"it", "that", "that app", "the app", "them"}
last_app = [None]

def route_one(q_en):
    ql = q_en.lower().strip(" .,!?")
    for name, rxname in ORDER:
        rx = RX.get(rxname)
        if rx and rx.search(ql):
            return name
    # screenshot / timer / open / close approximation of _extract_intent
    if re.search(r"\b(take|capture|grab|snap)\b.*\bscreen\s?shot\b|\bscreenshot\b", ql) \
            and gate.classify(q_en)["kind"] != "chat":
        return "screenshot"
    if RX.get("_TIMER_TRIGGER_RE") and RX["_TIMER_TRIGGER_RE"].search(ql):
        return "timer"
    m = RX["_CLOSE_VERB_RE"].search(ql) if RX.get("_CLOSE_VERB_RE") else None
    if m:
        tgt = m.group(1).strip(" .,!?")
        if tgt in _PRONOUNS:
            return "close:" + last_app[0] if last_app[0] else "MISS(close-pronoun)"
        names = [t.strip() for t in re.split(r"\s+and\s+", tgt)]
        hits = [n for n in names if any(v in n for v in REGISTRY)]
        if hits:
            last_app[0] = hits[-1]
            return "close:" + "+".join(hits)
    m = RX["_APP_INTENT_RE"].search(ql) if RX.get("_APP_INTENT_RE") else None
    if m and gate.classify(q_en)["kind"] != "chat":
        tgt = re.sub(r"[,\s]*(?:please|for me|now)\W*$", "", m.group(1).strip(" .,!?"))
        if tgt in _PRONOUNS:
            return "open:" + last_app[0] if last_app[0] else "MISS(open-pronoun)"
        names = [t.strip() for t in re.split(r"\s+and\s+", tgt)]
        hits = [n for n in names if any(v in n for v in REGISTRY)]
        if hits:
            last_app[0] = hits[-1]
            return "open:" + "+".join(hits)
    return "chat"

# replicate _split_compound_command using the real verb set from source
COMPOUND_VERBS = None
for node in ast.walk(tree):
    if (isinstance(node, ast.Assign) and len(node.targets) == 1
            and isinstance(node.targets[0], ast.Name)
            and node.targets[0].id == "_COMPOUND_VERBS"):
        COMPOUND_VERBS = ast.literal_eval(node.value)
assert COMPOUND_VERBS, "compound verb set not found in wivexx_api"

def split_compound(q):
    m = re.match(r"^(.{3,70}?)\s+(?:and\s+(?:then\s+|also\s+)?|then\s+)(.{3,90})$", q.strip(), re.I)
    if not m:
        return None
    a, b = m.group(1).strip(), m.group(2).strip()
    if a.split()[0].lower() in COMPOUND_VERBS and b.split()[0].lower() in COMPOUND_VERBS:
        return a, b
    return None

def process(utter):
    lang = i18n.detect_lang(utter)
    q = i18n.quick_english(utter) or utter if lang != "en" else utter
    verdict = gate.classify(q)["kind"]
    if verdict == "chat":
        route = "chat"
    else:
        parts = split_compound(q)
        if parts:
            route = "MULTI(%s + %s)" % (route_one(parts[0]), route_one(parts[1]))
        else:
            route = route_one(q)
    # memory (rule path, as when Ollama is off)
    stored = []
    if not utter.rstrip().endswith("?"):
        ops = ms.extract_facts_rules(q if lang == "en" else utter)
        ops = ms.filter_ops_to_user_text(ops, utter + " " + q)
        if ops:
            c = ms.apply_ops(ops, source="auto")
            if c["add"] or c["update"]:
                stored = [o["key"] for o in ops]
    return lang, q, route, stored

# ── the conversation: (utterance, expected route prefix, expect_memory_keys) ──
CONVO = [
    ("good morning wivexx, how are you today?", "chat", []),
    ("i've been thinking about getting more serious with video editing lately", "chat", []),
    ("do you think it's too late to start a youtube channel in 2026?", "chat", []),
    ("open premiere pro", "open:premiere", []),
    ("set a timer for 25 minutes so i stay focused", "timer", []),
    ("my sister is called Ana by the way, she might call while i work", "chat", ["sister"]),
    ("so yesterday my brother and i watched the whole lord of the rings trilogy", "chat", []),
    ("remember that my brother's name is Leo", "remember", []),
    ("tell me a joke", "joke", []),
    ("haha, tell me another joke", "joke", []),
    ("what time is it", "time", []),
    ("take a screenshot and set a timer for 5 minutes", "MULTI", []),
    ("open discord and spotify", "open:discord+spotify", []),
    ("actually close it", "close:", []),
    ("my friend said i should open a coffee shop, can you imagine that?", "chat", []),
    ("what's the weather like today?", "weather", []),
    ("pause the music", "media", []),
    ("next song", "media", []),
    ("play", "media", []),
    ("stop the music", "media", []),
    ("volume up", "volume", []),
    ("make it louder", "volume", []),
    ("cierra spotify", "close:spotify", []),
    ("sube el volumen", "volume", []),
    ("¿quién es mrbeast?", "chat", []),
    ("who won the champions league this year?", "chat", []),      # search-flagged chat
    ("i work as a real estate photographer, so mornings are busy", "chat", ["work"]),
    ("what do you know about me", "recall", []),
    ("note that i owe marco 20 bucks", "note", []),
    ("what's on my calendar tomorrow", "cal_query", []),
    ("schedule lunch with anna tomorrow at 1pm", "cal_create", []),
    ("my dog Max destroyed another pillow yesterday", "chat", []),
    ("remember that my dog is called Max", "remember", []),
    ("close premiere pro and lightroom", "close:premiere", []),
    ("how much battery do i have left", "battery", []),
    ("run an internet speed test", "speedtest", []),
    ("free up some ram", "optimize", []),
    ("that reminds me, when i was a kid we had dial up internet", "chat", []),
    ("what can you do?", "capability", []),
    ("shut down the pc in 20 minutes", "shutdown", []),
    ("cancel the shutdown", "shutdown_cancel", []),
    ("abre steam", "open:steam", []),
    ("i love that game where you build farms, stardew valley i think", "chat", []),
    ("take a note: check the stardew valley sale on friday", "note", []),
    ("what version are you on", "version", []),
    # v1.34 additions: spanish media + comparative volume + more compounds
    ("pausa la música", "media", []),
    ("siguiente canción", "media", []),
    ("más fuerte", "volume", []),
    ("a bit quieter please", "volume", []),
    ("mute the sound and take a screenshot", "MULTI", []),
    ("open obs and start a timer for 90 minutes", "MULTI", []),
    # v1.50: a mute whose object is the GAME / a person / the mic is NOT a system
    # mute — it must stay in chat (Wivexx can only mute its OWN output).
    ("mute the enemy pings for a sec", "chat", []),
    ("mute the mic and take a screenshot", "MULTI(chat + screenshot)", []),
    # traps that must NOT be media/split
    ("play despacito on youtube", "youtube", []),
    ("i used to play in a band and then i quit when we moved", "chat", []),
    # v1.45: Spanish facts must be remembered offline (no LLM/key)
    ("por cierto mi perro se llama max", "chat", ["perro"]),
    ("y trabajo como fotógrafo por las mañanas", "chat", ["work"]),
    ("mi hermano se llama leo por si preguntas", "chat", ["hermano"]),
    ("goodbye wivexx", "selfclose", []),
]

fails, misses = [], []
print(f"{len(RX)} trigger regexes loaded\n")
for utter, want, want_mem in CONVO:
    lang, q, route, stored = process(utter)
    ok = route.startswith("MULTI(") if want == "MULTI" else route.startswith(want)
    if want == "MULTI" and ok:
        ok = "chat" not in route and "MISS" not in route   # both halves must be commands
    mark = "PASS" if ok else "FAIL"
    if not ok:
        fails.append((utter, want, route))
    extra = f" [mem: {stored}]" if stored else ""
    langtag = f" ({lang}→{q!r})" if lang != "en" else ""
    print(f"{mark} {route:<22} expected={want:<18} | {utter[:58]}{langtag}{extra}")
    for k in want_mem:
        if k not in stored:
            misses.append((utter, k))

print("\n--- memory state after conversation:")
for f in ms.list_facts():
    print("  ", f["key"], "=", f["value"])
print("\n=== ROUTE FAILURES:", len(fails))
for u, w, r in fails:
    print("   ", repr(u), "wanted", w, "got", r)
print("=== MEMORY MISSES:", misses if misses else "none")
