"""voice_command_node.py — Voice-command NLU bridge for saltybot (Issue #137).

Subscribes to /social/speech/transcript, runs the lightweight regex-based NLU
parser, and publishes /social/voice_command (VoiceCommand).

Confirmation flow
-----------------
Dangerous commands (system.shutdown, system.restart, social.forget_me) set
requires_confirmation=True and a unique token.  The node waits up to
``confirmation_timeout_s`` seconds for a confirm.yes or confirm.no response.

  transcript: "shut down"
    → publishes VoiceCommand(intent="system.shutdown", requires_confirmation=True)
    → (TTS asks "Are you sure you want to shut down?")
  transcript: "yes"
    → publishes VoiceCommand(intent="system.shutdown", requires_confirmation=False)
       with the same confirmation_token  ← downstream treats this as "execute"

Fallback
--------
If confidence < ``min_confidence`` (default 0.70) or intent == "fallback",
the node publishes VoiceCommand(intent="fallback") so the orchestrator can
route the utterance to the LLM conversation engine.

ROS2 topics
-----------
Subscribe: /social/speech/transcript    (saltybot_social_msgs/SpeechTranscript)
Publish:   /social/voice_command        (saltybot_social_msgs/VoiceCommand)

Parameters
----------
min_confidence          float   0.70    Intent confidence threshold for execution
confirmation_timeout_s  float   10.0   Seconds before pending confirmation expires
announce_intent         bool    true    Log intent at INFO level (useful for debug)
"""

from __future__ import annotations

import time
import uuid
from typing import Dict, Optional

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, QoSReliabilityPolicy

from saltybot_social_msgs.msg import SpeechTranscript, VoiceCommand

from .voice_command_parser import ParsedIntent, parse


class VoiceCommandNode(Node):
    """NLU bridge: SpeechTranscript → VoiceCommand."""

    def __init__(self) -> None:
        super().__init__("voice_command_node")

        # ── Parameters ──────────────────────────────────────────────────────
        self.declare_parameter("min_confidence", 0.70)
        self.declare_parameter("confirmation_timeout_s", 10.0)
        self.declare_parameter("announce_intent", True)

        self._min_conf: float = self.get_parameter("min_confidence").value
        self._confirm_timeout: float = self.get_parameter("confirmation_timeout_s").value
        self._announce: bool = self.get_parameter("announce_intent").value

        # ── Reliable QoS — voice commands must not be dropped ───────────────
        qos = QoSProfile(depth=10)
        qos.reliability = QoSReliabilityPolicy.RELIABLE

        self._cmd_pub = self.create_publisher(VoiceCommand, "/social/voice_command", qos)

        self._transcript_sub = self.create_subscription(
            SpeechTranscript,
            "/social/speech/transcript",
            self._on_transcript,
            qos,
        )

        # ── Confirmation state ───────────────────────────────────────────────
        self._pending: Optional[Dict] = None   # dict with pending command data

        # Periodic timeout check
        self._timeout_timer = self.create_timer(1.0, self._check_confirmation_timeout)

        self.get_logger().info(
            f"voice_command_node ready "
            f"(min_confidence={self._min_conf}, "
            f"confirmation_timeout={self._confirm_timeout}s)"
        )

    # ── Transcript callback ───────────────────────────────────────────────────

    def _on_transcript(self, msg: SpeechTranscript) -> None:
        # Skip streaming partial results
        if msg.is_partial:
            return

        text = msg.text.strip()
        if not text:
            return

        parsed: ParsedIntent = parse(text)

        if self._announce:
            self.get_logger().info(
                f"[NLU] '{text}' → {parsed.intent} "
                f"(conf={parsed.confidence:.2f}, "
                f"entities={parsed.entities})"
            )

        # ── Confirmation-pending branch ──────────────────────────────────────
        if self._pending is not None:
            self._handle_confirmation_response(parsed, msg.speaker_id)
            return

        # ── Normal dispatch ──────────────────────────────────────────────────
        self._dispatch(parsed, msg.speaker_id)

    # ── Dispatch ──────────────────────────────────────────────────────────────

    def _dispatch(self, parsed: ParsedIntent, speaker_id: str) -> None:
        """Route a freshly parsed intent to the appropriate action."""

        # Below-threshold → fallback to LLM
        if parsed.confidence < self._min_conf or parsed.intent == "fallback":
            self._publish(
                intent="fallback",
                entities={},
                confidence=parsed.confidence,
                raw_text=parsed.raw_text,
                speaker_id=speaker_id,
            )
            return

        # Dangerous command → ask for confirmation
        if parsed.requires_confirmation:
            token = str(uuid.uuid4())
            self._pending = {
                "token": token,
                "intent": parsed.intent,
                "entities": parsed.entities,
                "raw_text": parsed.raw_text,
                "speaker_id": speaker_id,
                "expires_at": time.monotonic() + self._confirm_timeout,
            }
            self._publish(
                intent=parsed.intent,
                entities=parsed.entities,
                confidence=parsed.confidence,
                raw_text=parsed.raw_text,
                speaker_id=speaker_id,
                requires_confirmation=True,
                confirmation_token=token,
            )
            self.get_logger().warn(
                f"Dangerous command '{parsed.intent}' — "
                f"awaiting confirmation (token={token[:8]}…)"
            )
            return

        # Safe command — execute immediately
        self._publish(
            intent=parsed.intent,
            entities=parsed.entities,
            confidence=parsed.confidence,
            raw_text=parsed.raw_text,
            speaker_id=speaker_id,
        )

    # ── Confirmation handling ─────────────────────────────────────────────────

    def _handle_confirmation_response(
        self, parsed: ParsedIntent, speaker_id: str
    ) -> None:
        """Process a transcript while a dangerous command is awaiting confirmation."""
        assert self._pending is not None

        if parsed.intent == "confirm.yes":
            # Execute the originally pending command
            p = self._pending
            self._pending = None
            self.get_logger().info(
                f"Confirmed '{p['intent']}' (token={p['token'][:8]}…)"
            )
            self._publish(
                intent=p["intent"],
                entities=p["entities"],
                confidence=1.0,
                raw_text=p["raw_text"],
                speaker_id=p["speaker_id"],
                requires_confirmation=False,
                confirmation_token=p["token"],
            )

        elif parsed.intent in ("confirm.no", "nav.stop"):
            # Cancel
            p = self._pending
            self._pending = None
            self.get_logger().info(
                f"Cancelled '{p['intent']}' (token={p['token'][:8]}…)"
            )
            self._publish(
                intent="confirm.no",
                entities={"cancelled_intent": p["intent"]},
                confidence=1.0,
                raw_text=parsed.raw_text,
                speaker_id=speaker_id,
                confirmation_token=p["token"],
            )

        else:
            # Unrecognised response while awaiting confirmation — ignore and wait
            self.get_logger().debug(
                f"Ignoring '{parsed.intent}' while awaiting confirmation for "
                f"'{self._pending['intent']}'"
            )

    # ── Confirmation timeout ──────────────────────────────────────────────────

    def _check_confirmation_timeout(self) -> None:
        if self._pending is None:
            return
        if time.monotonic() >= self._pending["expires_at"]:
            p = self._pending
            self._pending = None
            self.get_logger().warn(
                f"Confirmation timeout for '{p['intent']}' — cancelled"
            )
            self._publish(
                intent="confirm.no",
                entities={"cancelled_intent": p["intent"], "reason": "timeout"},
                confidence=1.0,
                raw_text="",
                speaker_id=p["speaker_id"],
                confirmation_token=p["token"],
            )

    # ── Publisher helper ──────────────────────────────────────────────────────

    def _publish(
        self,
        intent: str,
        entities: Dict[str, str],
        confidence: float,
        raw_text: str,
        speaker_id: str,
        requires_confirmation: bool = False,
        confirmation_token: str = "",
    ) -> None:
        msg = VoiceCommand()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.intent = intent
        msg.raw_text = raw_text
        msg.speaker_id = speaker_id
        msg.confidence = float(confidence)
        msg.entities = [f"{k}={v}" for k, v in entities.items()]
        msg.requires_confirmation = requires_confirmation
        msg.confirmation_token = confirmation_token
        self._cmd_pub.publish(msg)


# ── Entry point ───────────────────────────────────────────────────────────────


def main(args=None) -> None:
    rclpy.init(args=args)
    node = VoiceCommandNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()
