"""wivexx_intent_gate.py — central command-vs-conversation triage (v1.33).

WHY: _handle_turn_impl in wivexx_api.py is a waterfall of ~45 command sections,
each with its own regex and its own (inconsistent) guards. Keywords buried in a
normal sentence ("my battery died while I was watching a youtube video") could
fire a command section. Conversely the policy for "is this even a command?" was
scattered across the file.

This module is the ONE place that decision is made, BEFORE the waterfall runs:

    classify(text) -> {"kind": "command" | "chat" | "ambiguous", ...}

  - "chat"      → confidently conversational: wivexx_api skips the ENTIRE
                  command waterfall and goes straight to Claude. This is the
                  only verdict that changes behaviour, so it needs a HIGH bar.
  - "command"   → confidently a command; waterfall runs (same as before).
  - "ambiguous" → can't tell; waterfall runs (same as before). Non-English
                  input is always ambiguous — Spanish flows to Claude exactly
                  like it did before this module existed.

Design notes (out-of-domain detection research): forcing a binary verdict makes
rule/model systems overconfident on odd inputs, so an explicit middle class is
kept; and the thresholds are ASYMMETRIC — a command misrouted to chat means the
action silently doesn't happen (worst outcome), while chat let through to the
waterfall merely relies on the per-section guards (the pre-v1.33 status quo).
Hence "chat" requires strong evidence AND no command evidence.

Pure logic, zero app imports, no side effects — safe to unit-test directly
(`python wivexx_intent_gate.py` runs the labelled regression suite).
"""
from __future__ import annotations

import re

# ── vocabulary ────────────────────────────────────────────────────────────────
# Verbs that head real Wivexx commands ("open X", "set a timer", "mute", …)
COMMAND_VERBS = {
    "open", "close", "launch", "start", "stop", "run", "load", "quit", "exit",
    "kill", "terminate", "restart", "reboot", "shutdown", "shut", "set", "take",
    "capture", "grab", "snap", "screenshot", "show", "hide", "play", "pause",
    "resume", "skip", "mute", "unmute", "turn", "switch", "activate",
    "deactivate", "enable", "disable", "engage", "search", "google", "find",
    "look", "remember", "forget", "recall", "remind", "note", "write", "save",
    "export", "log", "record", "add", "create", "make", "track", "untrack",
    "deliver", "transcribe", "caption", "optimize", "optimise", "free", "clean",
    "lock", "click", "label", "number", "move", "minimize", "minimise",
    "maximize", "maximise", "focus", "translate", "summarize", "summarise",
    "rewrite", "calculate", "convert", "cancel", "schedule", "repeat", "increase",
    "decrease", "raise", "lower", "volume", "brighten", "dim", "check", "scan",
    "list", "read", "book", "wake", "test", "measure", "fix", "update",
}

# Topics a QUESTION can legitimately be about and still be a Wivexx command
# ("what time is it", "how much battery is left", "do I have any timers…").
SYSTEM_TOPICS = re.compile(
    r"\b(?:"
    r"time|date|today|weather|temperature|forecast|battery|timer[s]?|alarm[s]?|"
    r"version|update[s]?|changelog|new\s+in|token[s]?|usage|cost|spent|"
    r"task[s]?|goal[s]?|disk|storage|space\s+left|drive|"
    r"volume|sound|audio|mic|microphone|speaker[s]?|"
    r"internet|connection|speed|ping|download\s+speed|"
    r"followers?|subscribers?|instagram|tiktok|youtube\s+channel|"
    r"remember|memor(?:y|ies)|know\s+about\s+me|on\s+record|"
    r"timers?\s+(?:left|running)|shortcut[s]?|hotkey[s]?|"
    r"cpu|gpu|ram|performance|fps|"
    r"clipboard|calendar|schedule|meeting[s]?|appointment[s]?|event[s]?"
    r")\b", re.IGNORECASE)

# Things Wivexx itself can be asked about — also legitimate question-commands
SELF_QUERY = re.compile(
    r"\b(?:what\s+can\s+you\s+do|your\s+(?:abilities|capabilit|skills|features|"
    r"version|memory)|can\s+you\s+hear|are\s+you\s+(?:there|listening|awake))\b",
    re.IGNORECASE)

# Polite / indirect request frames that put the verb later in the sentence
POLITE_REQ = re.compile(
    r"^(?:please\s+|kindly\s+)?(?:can|could|would|will)\s+you\s+(?:please\s+)?(\w+)"
    r"|^please\s+(\w+)"
    r"|^(?:go\s+ahead\s+and|i\s+(?:want|need)\s+you\s+to|let'?s)\s+(\w+)",
    re.IGNORECASE)

# Narrative openers — the speaker is talking ABOUT something, not commanding.
# Includes temporal story-openers ("yesterday…", "when I was…"). The lookahead
# spares delegation frames ("I want/need you to…", "I'd like you to…").
NARRATIVE_OPEN = re.compile(
    r"^(?:(?:i|we|he|she|they|it|my|our|his|her|their|someone|somebody)\b"
    r"(?!'?d\s+like\s+you|\s+(?:want|need)\s+you)"
    r"|yesterday|last\s+(?:night|week|month|year)|the\s+other\s+day"
    r"|when\s+i\s+was|back\s+in|one\s+time|once\s+i)",
    re.IGNORECASE)

# Concept / opinion / social question forms — almost always conversation
CONCEPT_Q = re.compile(
    r"^(?:what\s+is|what\s+are|what\s+was|what\s+were|who\s+is|who\s+was|"
    r"when\s+(?:did|was|were)|where\s+(?:is|was|does|did)(?!\s+my)|"
    r"why\s+(?:is|are|do|does|did|would|can'?t)|"
    r"how\s+(?:do|does|did|would|should|can|could)\s+(?!i\s+(?:have|much))|"
    r"how\s+(?:important|useful|different|similar|good|bad|old|big|far|long\s+ago)|"
    # advice-seeking questions ("should i mute my teammates", "when should i take a
    # screenshot") — these carry a command verb but are deliberation, not a command.
    r"(?:when|where|why|how)?\s*should\s+i\b|is\s+it\s+(?:worth|better|smart|okay|ok|"
    r"a\s+(?:good|bad)\s+idea)\b|(?:do|would)\s+you\s+recommend|"
    r"tell\s+me\s+about|explain|describe|"
    r"do\s+you\s+(?:think|like|believe|prefer|feel)|what\s+do\s+you\s+think|"
    r"have\s+you\s+(?:ever|heard|seen)|"
    r"what(?:'s|\s+is)\s+the\s+(?:difference|meaning|point|deal|story)|"
    r"what(?:'s|\s+is)\s+your\s+(?:favorite|favourite|opinion|take))",
    re.IGNORECASE)

# Feeling / opinion / storytelling markers anywhere in the sentence
CHATTY_MARKERS = re.compile(
    r"\b(?:i\s+(?:think|feel|felt|believe|love|hate|wonder|guess|heard|read|saw|"
    r"watched|played|tried|remember\s+when)|yesterday|last\s+night|last\s+week|"
    r"the\s+other\s+day|when\s+i\s+was|did\s+you\s+know|guess\s+what|"
    r"fun\s+fact|by\s+the\s+way)\b", re.IGNORECASE)

# First-person past/modal narrative anywhere ("… so i had to mute myself") — a
# story about doing something, never an imperative. Strong chat signal (a real
# command is never phrased "i had to …"), so it can appear deep in the sentence.
NARRATIVE_PAST = re.compile(
    r"\bi\s+(?:had\s+to|needed\s+to|ended\s+up|used\s+to|"
    r"was\s+(?:gonna|going\s+to|about\s+to)|kept|keep|forgot\s+to)\b", re.IGNORECASE)

# Hypothetical / conditional OPENER ("if i mute everyone will they know", "if you
# mute your mic nobody can hear you") — a supposition, not an imperative. Anchored
# to the START so a trailing conditional on a real command ("set a timer if i'm
# not back") is NOT swept up. A command is never phrased "if i/you mute…".
CONDITIONAL = re.compile(
    r"^(?:if|whenever|unless|suppose(?:\s+that)?|imagine\s+if|what\s+if|say)\s+"
    r"(?:i|you|we|they|he|she|it|someone|somebody|my\b|the\s+enemy|everyone)", re.IGNORECASE)

# Negated-imperative opener ("don't mute me", "never close my game", "no need to
# take a screenshot", "you don't have to set a timer") — the user is REFUSING the
# action, so firing the embedded verb does the EXACT OPPOSITE of their intent (the
# worst kind of false-fire). Start-anchored: no real Wivexx command begins this
# way. "don't forget to X" is EXCLUDED — it's an affirmative request ("don't forget
# to remind me at 5" is a genuine reminder), so a lost command is impossible here.
NEGATED_CMD = re.compile(
    r"^(?:please\s+)?(?:"
    r"(?:don'?t|do\s+not|never)\s+(?!forget\b)\w"
    r"|no\s+need\s+to\b"
    r"|there'?s\s+no\s+(?:need|point)\b"
    r"|you\s+(?:don'?t|do\s+not)\s+(?:have|need)\s+to\b"
    r"|i\s+(?:don'?t|do\s+not)\s+(?:want|need)\s+(?:you\s+)?to\b"
    r")", re.IGNORECASE)

# Reported speech ("my coach told me to mute", "the streamer said to open the map",
# "he wants me to close it") — the speaker is recounting someone ELSE's instruction,
# not issuing one. Most first-person "my/he/she…" openers already read as narrative;
# this also catches third-party subjects ("the streamer told everyone to …"). No
# real command contains "told me to / said to / wants me to", so it can't steal one.
REPORTED = re.compile(
    r"\b(?:told|asked|reminded)\s+(?:me|us|him|her|them|everyone)\s+(?:to|not\s+to)\b"
    r"|\b(?:said|says)\s+(?:to\b|i\s+should|we\s+should)"
    r"|\bwants?\s+me\s+to\b", re.IGNORECASE)

# Arithmetic / unit-conversion questions — they LOOK like concept questions
# ("what is 15 percent of 380") but they're commands for the local calculator.
MATH_Q = re.compile(
    r"\d.*\b(?:percent|%|plus|minus|times|divided|squared?|cubed?|root|"
    r"km|kilometers?|miles?|kg|kilos?|pounds?|lbs|celsius|fahrenheit|"
    r"meters?|feet|inches|liters?|gallons?|euros?|dollars?)\b"
    r"|\b(?:percent|%)\s+of\s+\d", re.IGNORECASE)

# Whisper fillers + wake-word prefixes to strip before analysis
_FILLERS = re.compile(
    r"^(?:(?:uh+|um+|erm+|hmm+|ok(?:ay)?|so|well|right|listen|hey|oh|dude|man|"
    r"guess\s+what|you\s+know\s+what)[,\s]+)+",
    re.IGNORECASE)
_WAKE_PREFIX = re.compile(r"^(?:hey\s+|ok\s+|okay\s+)?(?:wivexx|jarvis)[,.!\s]+", re.IGNORECASE)

# Cheap "probably not English" check: Spanish/Portuguese/etc. markers → the gate
# stays out of the way (ambiguous), exactly the pre-gate behaviour for Spanish.
_NON_EN = re.compile(
    r"[¿¡áéíóúñüàèìòùâêîôûäëïöß]|"
    r"\b(?:qué|como|cómo|cuál|dónde|cuándo|por\s+qué|abre|cierra|pon|dime|hola|"
    r"gracias|por\s+favor|puedes|quiero|necesito|hazme|dame|busca|reproduce)\b",
    re.IGNORECASE)


def classify(text: str) -> dict:
    """Triage an utterance. Returns {"kind", "score", "reasons"} where kind is
    "command" (run the waterfall), "chat" (skip straight to conversation) or
    "ambiguous" (run the waterfall — the pre-v1.33 status quo)."""
    raw = (text or "").strip()
    if not raw:
        return {"kind": "ambiguous", "score": 0.0, "reasons": ["empty"]}

    had_wake = bool(_WAKE_PREFIX.match(raw))
    t = _WAKE_PREFIX.sub("", raw)
    t = _FILLERS.sub("", t).strip()
    if not t:
        return {"kind": "ambiguous", "score": 0.0, "reasons": ["only-fillers"]}
    q = t.lower().strip(" .,!")          # keep '?' — it's a signal
    words = q.strip("?").split()
    n = len(words)

    if _NON_EN.search(q):
        return {"kind": "ambiguous", "score": 0.0, "reasons": ["non-english"]}

    cmd, chat, why = 0.0, 0.0, []
    narrative = bool(NARRATIVE_OPEN.match(q))
    narrative_past = bool(NARRATIVE_PAST.search(q))
    conditional = bool(CONDITIONAL.match(q))
    negated = bool(NEGATED_CMD.match(q))
    reported = bool(REPORTED.search(q))

    # ── command evidence ─────────────────────────────────────────────────────
    if words and words[0] in COMMAND_VERBS:
        cmd += 3.0; why.append("imperative-first-verb")
    m = POLITE_REQ.match(q)
    if m and not negated:   # "please don't lock the pc" is a refusal, not a request
        verb = next((g for g in m.groups() if g), "").lower()
        if verb in COMMAND_VERBS:
            cmd += 3.0; why.append("polite-request")
        else:
            cmd += 0.75; why.append("polite-frame-unknown-verb")
    if had_wake:
        cmd += 1.0; why.append("wake-prefix")
    if SYSTEM_TOPICS.search(q) and not narrative and not narrative_past \
            and not conditional and not negated and not reported:
        # a QUESTION about a system topic is a query-command ("what time is it");
        # a topic word inside a story ("yesterday my battery died…"), a refusal
        # ("never mute my mic") or reported speech is not.
        cmd += 2.0; why.append("system-topic")
    if SELF_QUERY.search(q):
        cmd += 2.5; why.append("self-query")
    if MATH_Q.search(q) and not narrative:
        cmd += 2.5; why.append("math-query")
    if n <= 4 and any(w in COMMAND_VERBS for w in words) and not negated:
        cmd += 1.5; why.append("terse-with-verb")

    # ── chat evidence ────────────────────────────────────────────────────────
    if narrative:
        chat += 2.5; why.append("narrative-opener")
    if CONCEPT_Q.match(q):
        chat += 2.5; why.append("concept-question")
    if CHATTY_MARKERS.search(q):
        chat += 2.0; why.append("chatty-marker")
    if narrative_past:
        chat += 2.5; why.append("narrative-past")
    if conditional:
        chat += 2.5; why.append("conditional")
    if negated:
        chat += 2.5; why.append("negated-command")
    if reported:
        chat += 2.5; why.append("reported-speech")
    if n > 12 and not any(w in COMMAND_VERBS for w in words[:5]):
        chat += 2.0; why.append("long-no-early-verb")
    if q.endswith("?") and not SYSTEM_TOPICS.search(q) and not SELF_QUERY.search(q) \
            and not m and not (words and words[0] in COMMAND_VERBS):
        chat += 1.5; why.append("question-mark-offtopic")

    net = cmd - chat
    # Asymmetric thresholds: "chat" (the only behaviour-changing verdict) needs
    # strong chat evidence AND essentially zero command evidence.
    if net >= 2.0:
        kind = "command"
    elif chat >= 2.5 and cmd <= 1.0 and net <= -2.0:
        kind = "chat"
    else:
        kind = "ambiguous"
    return {"kind": kind, "score": round(net, 2), "reasons": why}


# ── labelled regression suite ─────────────────────────────────────────────────
# HARD RULE: nothing labelled "command" may EVER come out "chat" (a lost command
# is the worst failure). Chat lines SHOULD come out "chat" but "ambiguous" is
# acceptable (the waterfall's own guards then decide, as before v1.33).
_COMMANDS = [
    "open discord",
    "hey wivexx, open discord please",
    "can you open discord for me?",
    "could you please launch spotify",
    "close all my apps",
    "take a screenshot",
    "set a timer for ten minutes",
    "remind me in five minutes to check the oven",
    "mute",
    "volume up",
    "turn the volume down a bit",
    "what time is it",
    "what's the weather like today?",
    "how much battery do I have left",
    "do I have any timers running?",
    "what version are you on",
    "what's new in the latest update",
    "remember that my sister's birthday is in june",
    "forget what I told you about the gym",
    "what do you remember about my projects",
    "add a goal to post three reels this week",
    "search youtube for lofi beats",
    "play some jazz on youtube",
    "take a note: buy batteries tomorrow",
    "save this conversation",
    "what can you do?",
    "shut down the pc in twenty minutes",
    "put the pc to sleep",
    "sleep the computer",
    "secure my pc",
    "cut the sound",
    "free up some ram",
    "run an internet speed test",
    "how fast is my internet right now",
    "translate the clipboard to spanish",
    "summarize what i copied",
    "move this window to the other monitor",
    "what's on my calendar tomorrow",
    "book a meeting with marco on friday at noon",
    "activate work mode",
    "switch audio to my headphones",
    "wivexx, tell me a joke",
    "i want you to close chrome",
    "go ahead and start obs",
    "please check my pending tasks",
    "how many tokens have i used today",
    # one representative phrasing per waterfall section (sweep set, v1.33)
    "stand down", "goodbye wivexx", "enable call mode", "repeat that",
    "say that again", "add app blender", "what day is it today",
    "which version are you running", "what's new", "give me the changelog",
    "battery status", "is my laptop plugged in", "wake up my pc",
    "turn on work mode", "remember that i park on level 3",
    "what do you know about me", "complete my first goal", "list my goals",
    "track my instagram", "give me my social report", "caption this video",
    "deliver the photos to my client", "what's eating my disk space",
    "any pending tasks", "cancel the shutdown", "shut down the computer",
    "lock the pc", "lock my computer", "lock the screen",
    "tell me a joke", "flip a coin", "roll a dice", "roll a d20",
    "play despacito on youtube",
    "note that i owe marco 20 bucks", "switch audio output to speakers",
    "activate gaming environment", "set volume to 50 percent", "louder",
    "quieter", "what is 15 percent of 380", "convert 5 miles to kilometers",
    "summarize my clipboard", "minimize everything except chrome",
    "what's on my calendar this week", "schedule lunch with anna tomorrow at 1",
    "performance mode on", "import to lightroom", "show the game overlay",
    "test my internet speed", "transcribe this recording", "free up memory",
    "show the performance hud", "show status", "enroll my voice",
    "number the screen", "click 7", "how many tokens did we burn today",
    "mute the mic", "take a screenshot of both monitors",
    "set an alarm for 7 am", "stop the music", "pause", "next song",
    "lock the pc",
]
_CHAT = [
    "i opened a coffee shop with my brother last year",
    "we played that new game all night and it was amazing",
    # gaming advice questions carrying a command verb — must NOT fire the command
    "when should i mute my teammates in ranked",
    "should i take a screenshot of my rank or not",
    "is it worth muting my teammates in this lobby",
    "when should i lock in my pick before someone dodges",
    # first-person past narrative carrying a command verb — story, not a command
    "the meeting ran long so i had to mute myself",
    "i keep forgetting to turn down my mic in calls",
    "i ended up muting everyone in the call by accident",
    # hypothetical / conditional openers carrying a command verb — supposition, not a command
    "if i mute everyone will the enemy still hear my pings",
    "if you mute your mic nobody can hear you",
    "what if i take a screenshot of this right now",
    # negated commands — the user REFUSES the action; firing it does the opposite
    "don't mute me",
    "please don't lock the pc",
    "no need to take a screenshot",
    "never mute my mic during a game",
    "you don't have to set a timer",
    "don't turn off the lights yet",
    # reported speech — recounting someone else's instruction, not issuing one
    "my coach told me to mute my mic during raids",
    "the streamer told everyone to open the map",
    "he wants me to close the game and go to bed",
    "what is a black hole exactly",
    "tell me about the roman empire",
    "why is the sky blue",
    "do you think pineapple belongs on pizza?",
    "i think the new star wars movie was better than people say",
    "yesterday my battery died while i was watching a youtube video",
    "my grandmother used to make the best cookies when i was a kid",
    "have you ever heard the story about the guy who sold the eiffel tower",
    "what's the difference between a crocodile and an alligator",
    "i saw a documentary about volcanoes and now i can't stop thinking about them",
    "when i was in college i wanted to start a band",
    "explain how photosynthesis works",
    "who was the first person to climb everest",
    "i feel like i haven't slept properly in weeks",
    "the other day someone told me that coffee stunts your growth, is that true?",
    "how do people in antarctica get internet",   # concept, not a speed test
    "what's your favorite movie",
    "guess what, i finally beat that boss i was stuck on",
]

if __name__ == "__main__":
    lost, noisy = [], []
    for s in _COMMANDS:
        r = classify(s)
        if r["kind"] == "chat":
            lost.append((s, r))
    hits = 0
    for s in _CHAT:
        r = classify(s)
        if r["kind"] == "chat":
            hits += 1
        elif r["kind"] == "command":
            noisy.append((s, r))
    print(f"commands kept out of 'chat': {len(_COMMANDS) - len(lost)}/{len(_COMMANDS)}  (MUST be all)")
    for s, r in lost:
        print(f"  LOST COMMAND: {s!r}  -> {r}")
    print(f"chat correctly skipped: {hits}/{len(_CHAT)}  (higher is better; ambiguous is OK)")
    for s, r in noisy:
        print(f"  chat scored as command (ok-ish, waterfall guards decide): {s!r} -> {r}")
    print("RESULT:", "PASS" if not lost else "FAIL — a command would be silently dropped")
