"""voice_command_parser.py — Lightweight NLU intent parser for saltybot (Issue #137).

Pure Python, zero ROS2 / ML dependencies.  Uses compiled regex patterns with
named capture groups for entity extraction.  No network calls, runs in < 1 ms.

Intent taxonomy
---------------
nav.*     navigation commands
social.*  social / enrollment commands
system.*  robot system control
config.*  behaviour configuration
confirm.* confirmation flow (yes / no)
fallback  unrecognised → forward to LLM conversation engine

Usage
-----
>>> from saltybot_social.voice_command_parser import parse
>>> r = parse("go to the kitchen please")
>>> r.intent, r.entities, r.confidence
('nav.go_to', {'location': 'kitchen'}, 0.95)

>>> r = parse("shut down")
>>> r.intent, r.requires_confirmation
('system.shutdown', True)
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

# ── Data types ────────────────────────────────────────────────────────────────


@dataclass
class ParsedIntent:
    intent: str                     # e.g. "nav.go_to"
    entities: Dict[str, str]        # e.g. {"location": "kitchen"}
    confidence: float               # 0.0–1.0
    raw_text: str                   # normalised input text
    requires_confirmation: bool = False


# ── Text normalisation ────────────────────────────────────────────────────────

_STRIP_PUNCT = re.compile(r"[^\w\s'.-]")   # keep apostrophe, hyphen, dot
_MULTI_SPACE = re.compile(r"\s+")


def _normalize(text: str) -> str:
    """Lowercase, strip non-word punctuation, collapse whitespace."""
    t = text.lower()
    t = _STRIP_PUNCT.sub(" ", t)
    t = _MULTI_SPACE.sub(" ", t)
    return t.strip()


# ── Internal intent definition ────────────────────────────────────────────────


@dataclass
class _IntentDef:
    name: str
    patterns: List[str]
    confidence: float = 0.90
    requires_confirmation: bool = False
    _compiled: List[re.Pattern] = field(
        default_factory=list, repr=False, init=False
    )

    def compile(self) -> None:
        self._compiled = [
            re.compile(p, re.IGNORECASE) for p in self.patterns
        ]

    def match(
        self, text: str
    ) -> Optional[Tuple[float, Dict[str, str]]]:
        for pat in self._compiled:
            m = pat.search(text)
            if m:
                entities = {
                    k: v.strip()
                    for k, v in m.groupdict().items()
                    if v is not None
                }
                return self.confidence, entities
        return None


# ── Follow-mode keyword table (used for post-processing) ─────────────────────

_MODE_KEYWORDS: Dict[str, List[str]] = {
    "shadow": ["shadow", "directly behind", "behind me"],
    "lead":   ["lead", "go ahead", "walk ahead", "walk in front", "in front"],
    "side":   ["side", "beside", "next to", "alongside"],
    "orbit":  ["orbit", "circle", "spin around"],
    "loose":  ["loose", "give me space", "more space", "back off", "relax"],
    "tight":  ["tight", "stay close", "stay very close", "right next to me"],
}


def _mode_from_text(text: str) -> Optional[str]:
    for mode, keywords in _MODE_KEYWORDS.items():
        for kw in keywords:
            if kw in text:
                return mode
    return None


# ── Volume normalisation ──────────────────────────────────────────────────────

_WORD_VOLUMES = {
    "zero": "0", "off": "0", "mute": "0",
    "low": "20", "quiet": "20", "soft": "20",
    "medium": "50", "mid": "50", "half": "50", "normal": "50",
    "high": "80", "loud": "80",
    "full": "100", "max": "100", "maximum": "100",
}


def _normalize_volume(level: str) -> str:
    """Convert word or numeric volume string to a 0-100 integer string."""
    s = level.strip().rstrip("% ").lower().replace("percent", "").strip()
    if s in _WORD_VOLUMES:
        return _WORD_VOLUMES[s]
    try:
        return str(max(0, min(100, int(s))))
    except ValueError:
        return s


# ── Intent definitions — order matters for equal-confidence tie-breaking ──────

_DEFS: List[_IntentDef] = [

    # ── Nav: go home  (must precede nav.go_to to avoid "go home" → location=home)
    _IntentDef(
        name="nav.go_home",
        confidence=0.95,
        patterns=[
            r"\bgo home\b",
            r"\bhead home\b",
            r"\breturn (?:to )?(?:home|base|your dock|charging station|home base)\b",
            r"\bback to (?:home|base|your dock|charging station)\b",
            r"\bdock yourself\b",
        ],
    ),

    # ── Nav: go to X
    _IntentDef(
        name="nav.go_to",
        confidence=0.95,
        patterns=[
            r"^(?:please\s+)?go to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now|quickly))?$",
            r"^navigate to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now))?$",
            r"^take me to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now))?$",
            r"^head to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now))?$",
            r"^move to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now))?$",
            r"^drive to (?:the\s+)?(?P<location>[\w][\w ]{1,30}?)(?:\s+(?:please|now))?$",
        ],
    ),

    # ── Nav: follow me
    _IntentDef(
        name="nav.follow_me",
        confidence=0.95,
        patterns=[
            r"\bfollow (?:me|along)\b(?!\s+route)",
            r"\bcome with me\b",
            r"\bstick with me\b",
            r"\bkeep up\b(?!\s+with)",
            r"\bwalk with me\b",
            r"\bstay with me\b",
        ],
    ),

    # ── Nav: stop  (anchored to prevent clashes with "stop recording")
    _IntentDef(
        name="nav.stop",
        confidence=0.95,
        patterns=[
            r"^(?:please\s+)?(?:stop|halt|freeze)(?:\s+(?:now|please|moving|right now|right there))?$",
            r"\bdon'?t (?:move|go|proceed|go anywhere)\b",
            r"\bstop (?:moving|right there|in your tracks|where you are)\b",
            r"\bhold (?:it|there|on)$",
        ],
    ),

    # ── Nav: wait here
    _IntentDef(
        name="nav.wait",
        confidence=0.90,
        patterns=[
            r"\bwait(?: (?:here|there|for me|right here|just a|a)(?:\s+\w+)?)?\b(?!\s+until)",
            r"\bhold (?:your )?position\b",
            r"\bhold your ground\b",
            r"\bpause (?:here|there)?\b",
            r"\bstay (?:here|there|put|right here)\b(?!\s+(?:close|beside))",
            r"\bstay (?:right )?where you are\b",
        ],
    ),

    # ── Nav: come here
    _IntentDef(
        name="nav.come_here",
        confidence=0.95,
        patterns=[
            r"\bcome (?:here|over here|over to me|to me|towards me)\b",
            r"\bget (?:over here|here|to me)\b",
            r"\bapproach (?:me|here|this way)\b",
            r"\bmove (?:towards|toward|over to) me\b",
        ],
    ),

    # ── Nav: patrol
    _IntentDef(
        name="nav.patrol",
        confidence=0.90,
        patterns=[
            r"\b(?:start\s+|begin\s+|go on\s+)?patrol(?:ling|s)?\b",
            r"\b(?:start\s+|begin\s+)?guarding(?: the area)?\b",
            r"\bguard the (?:area|perimeter|room|building|place)\b",
        ],
    ),

    # ── Nav: teach route
    _IntentDef(
        name="nav.teach_route",
        confidence=0.95,
        patterns=[
            r"teach (?:a |me a |this |new )?route(?: (?:called|named))? ?(?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please|now))?$",
            r"record (?:a |this |new )?route(?: (?:called|named))? ?(?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
            r"learn (?:this |a |new )?route(?: (?:called|named))? ?(?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
            r"remember this route(?: (?:as|called|named) ?(?P<name>[\w][\w ]{0,29}?))?(?:\s+(?:please))?$",
            r"\bstart (?:recording|teaching)(?: a| the)? route\b",
            r"\bbegin route recording\b",
        ],
    ),

    # ── Nav: stop teaching route
    _IntentDef(
        name="nav.stop_teaching",
        confidence=0.95,
        patterns=[
            r"\bstop (?:recording|teaching)(?: the| this)? route\b",
            r"\bfinish (?:recording|teaching)(?: the| this)? route\b",
            r"\bdone (?:recording|teaching)(?: the| this)? route\b",
            r"\bend route (?:recording|teaching)\b",
        ],
    ),

    # ── Nav: replay route  (must check before nav.follow_me to avoid "follow route X")
    _IntentDef(
        name="nav.replay_route",
        confidence=0.95,
        patterns=[
            r"replay(?: route)? (?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please|now))?$",
            r"play back (?:route )?(?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
            r"follow route (?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
            r"go on route (?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
            r"take (?:the )?(?P<name>[\w][\w ]{0,29}?) route(?:\s+(?:please))?$",
            r"run route (?P<name>[\w][\w ]{0,29}?)(?:\s+(?:please))?$",
        ],
    ),

    # ── Nav: set follow mode  (explicit mode-change phrasing)
    _IntentDef(
        name="nav.set_mode",
        confidence=0.90,
        patterns=[
            r"(?:change|switch|set)(?: follow)? mode to (?P<mode>shadow|lead|side|orbit|loose|tight)\b",
            r"(?:use|enable) (?P<mode>shadow|lead|side|orbit|loose|tight)(?: follow)? mode\b",
            r"\b(?P<mode>shadow)(?: mode| me| follow)?\s*$",
            r"\borbit(?: mode| me| around me)?\s*$",
            r"\bwalk (?:in front|ahead)(?: of me)?\s*$",
            r"\bstay beside(?: me)?\s*$",
            r"\bwalk beside(?: me)?\s*$",
            r"\bgive me (?:more )?space\s*$",
            r"\bback off(?: a (?:bit|little))?\s*$",
            r"\bstay (?:very )?close(?: to me)?\s*$",
            r"\b(?P<mode>loose)(?: mode| follow)?\s*$",
            r"\b(?P<mode>tight)(?: mode| follow)?\s*$",
        ],
    ),

    # ── Social: remember me / register name
    _IntentDef(
        name="social.remember_me",
        confidence=0.95,
        patterns=[
            r"^(?:please\s+)?remember me\b(?: as (?P<name>[\w][\w ]{1,29}?))?(?:\s+please)?$",
            r"\bmy name is (?P<name>[\w][\w ]{1,29}?)(?:\s+please)?$",
            r"\bcall me (?P<name>[\w][\w ]{1,29}?)(?:\s+please)?$",
            r"\bremember (?:my name|who i am)\b",
            r"\blearn my name\b",
            r"\bregister me\b",
        ],
    ),

    # ── Social: forget me  [requires confirmation]
    _IntentDef(
        name="social.forget_me",
        confidence=0.95,
        requires_confirmation=True,
        patterns=[
            r"\bforget me\b",
            r"\bdelete me\b",
            r"\bremove me\b",
            r"\bforget who i am\b",
            r"\berase (?:me|my (?:face|name|data|profile))\b",
            r"\bdelete my (?:face|name|data|profile)\b",
            r"\bunregister me\b",
        ],
    ),

    # ── Social: what's my name?
    _IntentDef(
        name="social.whats_my_name",
        confidence=0.95,
        patterns=[
            r"\bwhat'?s my name\b",
            r"\bdo you (?:know|remember) (?:me|who i am)\b",
            r"\bwho am i\b",
            r"\bdo you (?:know|recognize|recognise) me\b",
            r"\bhave (?:you )?(?:met|seen) me before\b",
            r"\bdo you know my name\b",
        ],
    ),

    # ── Social: tell me a joke
    _IntentDef(
        name="social.tell_joke",
        confidence=0.90,
        patterns=[
            r"\btell(?: me)?(?: a)? joke\b",
            r"\bsay something funny\b",
            r"\bmake me (?:laugh|smile)\b",
            r"\ba joke(?: please)?\b",
            r"\bsomething funny\b",
            r"\bfunny (?:story|one)\b",
            r"\bcheer me up\b",
        ],
    ),

    # ── System: battery status
    _IntentDef(
        name="system.battery_status",
        confidence=0.95,
        patterns=[
            r"\bbattery (?:status|level|life|charge|percentage|reading)\b",
            r"\bhow(?:'?s| much| is)(?: (?:your|the))? battery\b",
            r"\bpower (?:status|level|reading)\b",
            r"\bhow charged are you\b",
            r"\bare you (?:low on|running out of) (?:power|battery|charge)\b",
            r"\bcheck (?:the )?battery\b",
        ],
    ),

    # ── System: map status
    _IntentDef(
        name="system.map_status",
        confidence=0.90,
        patterns=[
            r"\bmap (?:status|ready|available|loaded|complete|done)\b",
            r"\bhow(?:'?s| is)(?: (?:the|your))? map(?:ping)?\b",
            r"\b(?:are you|is the) map(?:ped|ping)?\b",
            r"\bdo you have a map\b",
            r"\bmap (?:updated|up to date)\b",
        ],
    ),

    # ── System: shutdown  [requires confirmation]
    _IntentDef(
        name="system.shutdown",
        confidence=0.95,
        requires_confirmation=True,
        patterns=[
            r"\bshut ?down\b",
            r"\bpower (?:off|down)\b",
            r"\bturn yourself off\b",
            r"\bturn off\b",
            r"\bshut yourself off\b",
        ],
    ),

    # ── System: restart  [requires confirmation]
    _IntentDef(
        name="system.restart",
        confidence=0.95,
        requires_confirmation=True,
        patterns=[
            r"\brestart\b(?!\s+(?:music|audio|video|stream|app|the))",
            r"\breboot\b",
            r"\bstart fresh\b",
            r"\bstart over\b",
            r"\breset yourself\b",
        ],
    ),

    # ── System: volume up
    _IntentDef(
        name="system.volume_up",
        confidence=0.90,
        patterns=[
            r"\bvolume up\b",
            r"\blouder\b",
            r"\bturn (?:it |the volume |yourself )?up\b",
            r"\bspeak up\b",
            r"\bincrease (?:the )?volume\b",
            r"\braise (?:your |the )?voice\b",
        ],
    ),

    # ── System: volume down
    _IntentDef(
        name="system.volume_down",
        confidence=0.90,
        patterns=[
            r"\bvolume down\b",
            r"\bquieter\b",
            r"\bturn (?:it |the volume |yourself )?down\b",
            r"\bsofter\b",
            r"\bdecrease (?:the )?volume\b",
            r"\bkeep (?:it |your voice )?down\b",
            r"\bmute\b",
        ],
    ),

    # ── System: volume set (exact level)
    _IntentDef(
        name="system.volume_set",
        confidence=0.95,
        patterns=[
            r"set (?:the )?volume to (?P<level>\d{1,3}(?:\s*percent)?|low|medium|high|max)\b",
            r"volume (?:to |at )?(?P<level>\d{1,3})(?:\s*percent)?\b",
            r"(?:turn|set) (?:the )?volume (?:to |at )?(?P<level>\d{1,3}(?:\s*percent)?|low|medium|high|max)\b",
        ],
    ),

    # ── Config: change personality
    _IntentDef(
        name="config.personality",
        confidence=0.90,
        patterns=[
            r"(?:change|switch)(?: to)? (?P<personality>[\w][\w ]{1,20}?) personality\b",
            r"(?:use|load) (?P<personality>[\w][\w ]{1,20}?) personality\b",
            r"personality (?:mode )?(?:set (?:to )?)?(?P<personality>[\w][\w ]{1,20}?)(?:\s+please)?$",
            r"be (?:more )?(?P<personality>friendly|formal|casual|playful|serious|stern)\b",
        ],
    ),

    # ── Config: sass level
    _IntentDef(
        name="config.sass_level",
        confidence=0.90,
        patterns=[
            r"\bmore sas(?:sy|s)\b",
            r"\bless sass(?:y|iness)?\b",
            r"(?:change|set) sass (?:level )?to (?P<level>\d{1,2}|low|medium|high)\b",
            r"\bsass (?:level )?(?P<level>\d{1,2}|low|medium|high)\b",
            r"\btone (?:it |yourself )?down\b",
            r"\bless (?:sarcastic|snarky|rude|cheeky)\b",
            r"\bmore (?:sarcastic|snarky|witty|cheeky)\b",
        ],
    ),

    # ── Config: follow mode (explicit config phrasing, lower confidence than nav.set_mode)
    _IntentDef(
        name="config.follow_mode",
        confidence=0.85,
        patterns=[
            r"(?:update|change|set) follow mode (?:to )?(?P<mode>\w+)\b",
            r"follow mode (?:is |should be )?(?P<mode>\w+)\b",
        ],
    ),

    # ── Confirmation: yes
    _IntentDef(
        name="confirm.yes",
        confidence=0.95,
        patterns=[
            r"^(?:yes|yeah|yep|yup|sure|ok|okay|correct|affirmative|"
            r"confirm(?:ed)?|do it|go ahead|proceed|definitely|"
            r"absolutely|right|that'?s right|please do)(?:\s*[!.]*)?$",
        ],
    ),

    # ── Confirmation: no
    _IntentDef(
        name="confirm.no",
        confidence=0.95,
        patterns=[
            r"^(?:no|nope|nah|cancel|abort|never mind|nevermind|"
            r"negative|don'?t|forget it|never|no way)(?:\s*[!.]*)?$",
        ],
    ),
]

# Compile all patterns at module load time (once)
for _defn in _DEFS:
    _defn.compile()


# ── Public API ────────────────────────────────────────────────────────────────


def parse(text: str) -> ParsedIntent:
    """Parse raw utterance text and return the best-matching ParsedIntent.

    Returns a ParsedIntent with intent='fallback' and confidence=0.0 when no
    pattern matches.  The caller should compare confidence against a threshold
    (default 0.7) before acting on the result.

    Args:
        text: Raw transcribed utterance (any case, with punctuation).

    Returns:
        ParsedIntent with intent, entities, confidence, raw_text, and
        requires_confirmation populated.
    """
    normalized = _normalize(text)
    if not normalized:
        return ParsedIntent(
            intent="fallback", entities={}, confidence=0.0, raw_text=text
        )

    best_conf = 0.0
    best_defn: Optional[_IntentDef] = None
    best_entities: Dict[str, str] = {}

    for defn in _DEFS:
        result = defn.match(normalized)
        if result is None:
            continue
        conf, entities = result
        if conf > best_conf:
            best_conf = conf
            best_defn = defn
            best_entities = entities

    if best_defn is None:
        return ParsedIntent(
            intent="fallback", entities={}, confidence=0.0, raw_text=normalized
        )

    # ── Post-processing ───────────────────────────────────────────────────────

    # nav.set_mode: infer mode from keywords when named group didn't capture
    if best_defn.name == "nav.set_mode" and "mode" not in best_entities:
        inferred = _mode_from_text(normalized)
        if inferred:
            best_entities["mode"] = inferred

    # system.volume_set: normalise level to integer string
    if best_defn.name == "system.volume_set" and "level" in best_entities:
        best_entities["level"] = _normalize_volume(best_entities["level"])

    # nav.teach_route / nav.replay_route: strip trailing noise from name
    if best_defn.name in ("nav.teach_route", "nav.replay_route", "nav.stop_teaching"):
        if "name" in best_entities:
            best_entities["name"] = best_entities["name"].strip()
            if not best_entities["name"]:
                del best_entities["name"]

    return ParsedIntent(
        intent=best_defn.name,
        entities=best_entities,
        confidence=best_conf,
        raw_text=normalized,
        requires_confirmation=best_defn.requires_confirmation,
    )
