"""Semantic intent router — the fast brain-in-the-middle.

Claude is a thinker, not a light switch. This layer classifies every final
transcript in ~1ms (model2vec static embeddings, pure numpy, CPU) and decides
what KIND of utterance it is:

  reflex intents  (volume_up, mute_sound, next_track, open_app, ...)
      -> executed instantly in-process, no LLM round-trip
  everything else -> flows to Claude unchanged

It matches MEANING, not keywords: "increase the volume", "crank it up",
"I can't hear this" all land on volume_up; "I don't want to hear this" lands
on mute. Each intent is defined by example phrases; an utterance is embedded
and compared to every example (cosine). It only fires when CONFIDENT — best
score >= THRESHOLD and clearly ahead of both the runner-up intent and a set
of "conversation" decoy examples — otherwise it stays out of the way and
Claude handles the nuance. When unsure, think; when sure, act.

Multipliers are parsed separately ("three times", "a lot", "way up" scale
volume presses). Falls back cleanly if model2vec isn't installed.
"""

from __future__ import annotations

import re

import numpy as np

_MODEL_NAME = "minishlab/potion-base-8M"   # ~30MB static embeddings, ~1ms/enc

# ---------------------------------------------------------------------------
# Intent definitions — example phrases per intent. Matching is semantic, so
# these are prototypes, not an exhaustive list.
# ---------------------------------------------------------------------------
INTENTS: dict[str, list[str]] = {
    "volume_up": [
        "volume up", "turn the volume up", "increase the volume", "louder",
        "make it louder", "turn it up", "crank up the volume",
        "raise the volume", "it's too quiet", "pump up the volume",
        "turn the sound up", "bump the volume up",
    ],
    "volume_down": [
        "volume down", "turn the volume down", "decrease the volume",
        "quieter", "lower the volume", "turn it down", "it's too loud",
        "make it quieter", "not so loud", "bring the volume down",
        "turn the sound down",
    ],
    "mute_sound": [
        "mute", "mute the sound", "mute the audio", "silence",
        "i don't want to hear this", "shut the sound off", "kill the sound",
        "mute everything", "no sound", "turn the sound off",
    ],
    "unmute_sound": [
        "unmute", "unmute the sound", "sound back on", "turn the sound on",
        "give me the sound back",
    ],
    "pause_media": [
        "pause", "pause the music", "pause the song", "stop the music",
        "hold the music", "pause playback", "stop playing",
    ],
    "resume_media": [
        "play", "resume", "resume the music", "keep playing", "unpause",
        "continue the music", "play it again",
    ],
    "next_track": [
        "next", "next song", "next track", "skip", "skip this song",
        "skip this one", "play the next one", "put on the next song",
        "i don't like this song",
    ],
    "prev_track": [
        "previous song", "previous track", "go back a song",
        "play the last song", "put the previous one back on",
    ],
    "open_app": [
        "open discord", "open chrome", "launch steam", "start spotify",
        "open notepad", "open the browser", "launch the game",
        "open whatsapp", "start teams", "open file explorer",
    ],
}

# Decoys: things that LOOK like reflexes but need the brain. If an utterance
# is closer to one of these than to any intent, we stand down.
DECOYS = [
    "what's the volume of a sphere",
    "why did the music stop",
    "what song is this",
    "open discord and tell saad i'm coming",
    "open chrome and search for something",
    "can you fix the volume issue in the code",
    "what apps are open right now",
    "how do i open a file in python",
    "turn down the brightness of my future",
    "tell me about the next steps",
    "what should i play next",
    "pause for a second and listen to me",
    "let me teach you a workflow",
    "take a screenshot and tell me what you see",
    # "can't hear you" means the mic/voice is failing — NOT a volume request.
    # These must fall through to the brain, never to volume_up.
    "i can't hear you",
    "i can't hear jarvis",
    "i can't hear you jarvis",
    "i can't hear anything you say",
    "you're breaking up i can't hear you",
    "are you there i can't hear you",
]

THRESHOLD = 0.62    # min cosine to the best intent example
MARGIN = 0.06       # best intent must beat the best decoy by this much
MAX_WORDS = 10      # longer sentences are conversation, not reflexes

_WORD_NUM = {"one": 1, "once": 1, "two": 2, "twice": 2, "three": 3,
             "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8,
             "nine": 9, "ten": 10}


def multiplier(text: str) -> int:
    """How hard to apply a volume intent: '3 times'/'ten times' scale it,
    'a lot'/'way'/'max' big, 'a bit'/'slightly' small. Default 1."""
    t = text.lower()
    m = re.search(r"(\d+|" + "|".join(_WORD_NUM) + r")\s*(?:times|x)\b", t)
    if m:
        raw = m.group(1)
        n = int(raw) if raw.isdigit() else _WORD_NUM.get(raw, 1)
        return max(1, min(n, 10))
    if any(w in t for w in ("a lot", "way ", "max", "all the way", "much")):
        return 3
    if any(w in t for w in ("a bit", "little", "slight", "touch", "tad")):
        return 1
    return 2


_APP_STRIP = re.compile(
    r"^(?:please\s+|jarvis[,\s]+|can\s+you\s+|could\s+you\s+)?"
    r"(?:open|launch|start|run|fire\s+up|bring\s+up)\s+(?:the\s+)?", re.I)


def app_name(text: str) -> str | None:
    """Pull the app name out of an open_app utterance."""
    t = _APP_STRIP.sub("", text.strip().rstrip(".!?"))
    t = re.sub(r"\s+(?:for me|please|now|up)$", "", t, flags=re.I).strip()
    return t or None


class IntentRouter:
    """Embeds intent examples once; classifies utterances in ~1ms."""

    def __init__(self) -> None:
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(_MODEL_NAME)
        self._labels: list[str] = []
        examples: list[str] = []
        for name, phrases in INTENTS.items():
            for p in phrases:
                self._labels.append(name)
                examples.append(p)
        decoy_start = len(examples)
        examples.extend(DECOYS)
        emb = np.asarray(self._model.encode(examples), dtype=np.float32)
        emb /= np.maximum(np.linalg.norm(emb, axis=1, keepdims=True), 1e-9)
        self._intent_emb = emb[:decoy_start]
        self._decoy_emb = emb[decoy_start:]

    def classify(self, text: str) -> tuple[str | None, float]:
        """Return (intent, confidence) or (None, score) when the brain
        should handle it. Confident = clears THRESHOLD, beats decoys."""
        t = (text or "").strip()
        if not t or len(t.split()) > MAX_WORDS:
            return None, 0.0
        v = np.asarray(self._model.encode([t.lower()]),
                       dtype=np.float32)[0]
        v /= max(float(np.linalg.norm(v)), 1e-9)
        intent_scores = self._intent_emb @ v
        best_i = int(np.argmax(intent_scores))
        best = float(intent_scores[best_i])
        decoy_best = float(np.max(self._decoy_emb @ v))
        if best < THRESHOLD or best - decoy_best < MARGIN:
            return None, best
        return self._labels[best_i], best


_router: IntentRouter | None = None
_router_failed = False


def get_router() -> IntentRouter | None:
    """Lazy singleton; None when model2vec isn't available."""
    global _router, _router_failed
    if _router is None and not _router_failed:
        try:
            _router = IntentRouter()
        except Exception as e:  # noqa: BLE001
            _router_failed = True
            print(f"  intent router unavailable ({e}) — regex reflexes only")
    return _router
