#!/usr/bin/env python3
"""
Claude Code Voice Dictation Enhancement
========================================
Enhanced voice dictation for Claude Code terminal with command recognition.

Features:
- Hotkey activation (Ctrl+Alt+V)
- Command recognition for Claude Code slash commands
- Voice-to-RWL task creation
- OpenWork action routing
- Clipboard + direct typing output

Usage:
    python scripts/claude_code_voice.py

Hotkey: Ctrl+Alt+V (hold to record, release to transcribe)

Requirements:
    pip install faster-whisper sounddevice numpy keyboard pyperclip

Author: Genesis System
Version: 1.0.0
"""

import os
import sys
import time
import wave
import tempfile
import threading
import json
import re
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple
from dataclasses import dataclass
from enum import Enum, auto

# Add genesis path
GENESIS_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(GENESIS_ROOT))

# Check dependencies
try:
    import numpy as np
    import sounddevice as sd
    import keyboard
    import pyperclip
    from faster_whisper import WhisperModel
    DEPS_AVAILABLE = True
except ImportError as e:
    print(f"Missing dependency: {e}")
    print("\nInstall with:")
    print("  pip install faster-whisper sounddevice numpy keyboard pyperclip")
    print("  pip install torch --index-url https://download.pytorch.org/whl/cu121")
    DEPS_AVAILABLE = False


class CommandType(Enum):
    """Types of voice commands for Claude Code."""
    SLASH_COMMAND = auto()    # /commit, /help, etc.
    RALPH_TASK = auto()       # "Ralph, create..."
    FILE_NAV = auto()         # "Show me file...", "Open..."
    CODE_ACTION = auto()      # "Fix...", "Refactor...", "Add..."
    OPENWORK_ACTION = auto()  # "Open browser...", "Create file..."
    PLAIN_TEXT = auto()       # Regular dictation


@dataclass
class VoiceCommand:
    """Parsed voice command."""
    command_type: CommandType
    raw_text: str
    processed_text: str
    metadata: Dict[str, Any]


class ClaudeCodeCommandParser:
    """Parse voice input for Claude Code specific commands."""

    # Slash command mappings
    SLASH_COMMANDS = {
        r"\b(commit|make commit|create commit)\b": "/commit",
        r"\b(help|show help)\b": "/help",
        r"\b(clear|clear screen|clear chat)\b": "/clear",
        r"\b(compact|compact mode)\b": "/compact",
        r"\b(config|configuration|settings)\b": "/config",
        r"\b(cost|show cost|costs)\b": "/cost",
        r"\b(doctor|run doctor)\b": "/doctor",
        r"\b(init|initialize)\b": "/init",
        r"\b(login|log in)\b": "/login",
        r"\b(logout|log out)\b": "/logout",
        r"\b(mcp|show mcp)\b": "/mcp",
        r"\b(memory|show memory)\b": "/memory",
        r"\b(model|change model|switch model)\b": "/model",
        r"\b(permissions|show permissions)\b": "/permissions",
        r"\b(pr|pull request|create pr)\b": "/pr",
        r"\b(review|code review)\b": "/review",
        r"\b(status|show status)\b": "/status",
        r"\b(terminal|show terminal)\b": "/terminal",
        r"\b(vim|vim mode)\b": "/vim",
    }

    # Ralph task patterns
    RALPH_PATTERNS = [
        r"^ralph[,\s]+(.+)$",
        r"^hey ralph[,\s]+(.+)$",
        r"^task[:\s]+(.+)$",
        r"^create task[:\s]+(.+)$",
    ]

    # File navigation patterns
    FILE_NAV_PATTERNS = [
        r"^(show|open|read|view)\s+(me\s+)?(the\s+)?file\s+(.+)$",
        r"^(go to|navigate to|find)\s+(.+)$",
        r"^(show|display)\s+(me\s+)?(.+\.[\w]+)$",
    ]

    # Code action patterns
    CODE_ACTION_PATTERNS = [
        r"^(fix|repair|correct)\s+(.+)$",
        r"^(refactor|clean up|improve)\s+(.+)$",
        r"^(add|create|implement)\s+(.+)$",
        r"^(remove|delete|drop)\s+(.+)$",
        r"^(update|modify|change)\s+(.+)$",
        r"^(test|write tests? for)\s+(.+)$",
        r"^(explain|describe|what is)\s+(.+)$",
    ]

    # OpenWork action patterns (computer-use)
    OPENWORK_PATTERNS = [
        r"^(open|launch)\s+(browser|chrome|firefox|edge)(\s+.+)?$",
        r"^(create|make)\s+(a\s+)?(new\s+)?file\s+(.+)$",
        r"^(create|make)\s+(a\s+)?(new\s+)?folder\s+(.+)$",
        r"^(run|execute)\s+(command\s+)?(.+)$",
        r"^(search|find|look for)\s+(.+)\s+in\s+(files?|codebase)$",
    ]

    @classmethod
    def parse(cls, text: str) -> VoiceCommand:
        """Parse voice input to determine command type and process accordingly."""
        text_lower = text.lower().strip()
        text_clean = text.strip()

        # Check for slash commands first
        for pattern, slash_cmd in cls.SLASH_COMMANDS.items():
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommand(
                    command_type=CommandType.SLASH_COMMAND,
                    raw_text=text_clean,
                    processed_text=slash_cmd,
                    metadata={"slash_command": slash_cmd}
                )

        # Check Ralph task patterns
        for pattern in cls.RALPH_PATTERNS:
            match = re.search(pattern, text_lower, re.IGNORECASE)
            if match:
                task_desc = match.group(1) if match.lastindex else text_clean
                return VoiceCommand(
                    command_type=CommandType.RALPH_TASK,
                    raw_text=text_clean,
                    processed_text=f"Create RWL task: {task_desc}",
                    metadata={"task_description": task_desc}
                )

        # Check file navigation patterns
        for pattern in cls.FILE_NAV_PATTERNS:
            match = re.search(pattern, text_lower, re.IGNORECASE)
            if match:
                # Extract file path from match
                groups = match.groups()
                file_path = groups[-1] if groups else text_clean
                return VoiceCommand(
                    command_type=CommandType.FILE_NAV,
                    raw_text=text_clean,
                    processed_text=f"Read {file_path}",
                    metadata={"file_path": file_path}
                )

        # Check code action patterns
        for pattern in cls.CODE_ACTION_PATTERNS:
            match = re.search(pattern, text_lower, re.IGNORECASE)
            if match:
                action = match.group(1)
                target = match.group(2) if match.lastindex >= 2 else ""
                return VoiceCommand(
                    command_type=CommandType.CODE_ACTION,
                    raw_text=text_clean,
                    processed_text=text_clean,  # Pass through for Claude
                    metadata={"action": action, "target": target}
                )

        # Check OpenWork patterns
        for pattern in cls.OPENWORK_PATTERNS:
            match = re.search(pattern, text_lower, re.IGNORECASE)
            if match:
                return VoiceCommand(
                    command_type=CommandType.OPENWORK_ACTION,
                    raw_text=text_clean,
                    processed_text=text_clean,
                    metadata={"openwork_action": True, "groups": match.groups()}
                )

        # Default: plain text dictation
        return VoiceCommand(
            command_type=CommandType.PLAIN_TEXT,
            raw_text=text_clean,
            processed_text=text_clean,
            metadata={}
        )


class ClaudeCodeVoice:
    """Enhanced voice dictation for Claude Code terminal."""

    def __init__(self, config: Optional[Dict[str, Any]] = None):
        self.config = config or {
            "model": "medium.en",
            "device": "auto",
            "compute_type": "auto",
            "hotkey": "ctrl+alt+v",
            "sample_rate": 16000,
            "channels": 1,
            "vad_threshold_ms": 500,
            "typing_delay": 0.005,
        }

        # Auto-detect device
        if self.config["device"] == "auto":
            try:
                import torch
                self.config["device"] = "cuda" if torch.cuda.is_available() else "cpu"
            except ImportError:
                self.config["device"] = "cpu"

        # Auto-detect compute type
        if self.config["compute_type"] == "auto":
            self.config["compute_type"] = "float16" if self.config["device"] == "cuda" else "int8"

        self.recording = False
        self.audio_data = []
        self.model = None
        self.stream = None

        # Command history
        self.command_history: List[VoiceCommand] = []
        self.max_history = 100

        # OpenWork bridge (optional)
        self._openwork_callback = None

    def load_model(self):
        """Load Whisper model with GPU acceleration."""
        print(f"\nLoading model '{self.config['model']}' on {self.config['device']}...")
        print("(First run will download the model)")

        self.model = WhisperModel(
            self.config["model"],
            device=self.config["device"],
            compute_type=self.config["compute_type"]
        )
        print("Model loaded successfully!\n")

    def audio_callback(self, indata, frames, time_info, status):
        """Callback for audio recording."""
        if status:
            print(f"Audio status: {status}")
        if self.recording:
            self.audio_data.append(indata.copy())

    def start_recording(self):
        """Start recording audio."""
        if self.recording:
            return

        self.recording = True
        self.audio_data = []
        print("\r[RECORDING] Speak now...", end="", flush=True)

        self.stream = sd.InputStream(
            samplerate=self.config["sample_rate"],
            channels=self.config["channels"],
            dtype=np.float32,
            callback=self.audio_callback
        )
        self.stream.start()

    def stop_recording(self):
        """Stop recording and process."""
        if not self.recording:
            return

        self.recording = False
        if self.stream:
            self.stream.stop()
            self.stream.close()

        print("\r[PROCESSING] Transcribing...", end="", flush=True)

        if not self.audio_data:
            print("\r[WARNING] No audio recorded           ")
            return

        # Concatenate and transcribe
        audio = np.concatenate(self.audio_data, axis=0).flatten()
        text = self._transcribe(audio)

        if text:
            self._process_transcription(text)
        else:
            print("\r[WARNING] No speech detected           ")

    def _transcribe(self, audio: np.ndarray) -> Optional[str]:
        """Transcribe audio to text."""
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            temp_path = f.name
            with wave.open(f.name, 'wb') as wf:
                wf.setnchannels(self.config["channels"])
                wf.setsampwidth(2)
                wf.setframerate(self.config["sample_rate"])
                wf.writeframes((audio * 32767).astype(np.int16).tobytes())

        try:
            segments, info = self.model.transcribe(
                temp_path,
                language="en",
                vad_filter=True,
                vad_parameters=dict(
                    min_silence_duration_ms=self.config["vad_threshold_ms"]
                )
            )
            return " ".join([segment.text for segment in segments]).strip()
        except Exception as e:
            print(f"\r[ERROR] Transcription failed: {e}           ")
            return None
        finally:
            os.unlink(temp_path)

    def _process_transcription(self, text: str):
        """Process transcribed text and execute appropriate action."""
        # Parse command
        command = ClaudeCodeCommandParser.parse(text)

        # Add to history
        self.command_history.append(command)
        if len(self.command_history) > self.max_history:
            self.command_history.pop(0)

        # Process based on command type
        if command.command_type == CommandType.SLASH_COMMAND:
            self._output_text(command.processed_text)
            print(f"\r[SLASH] {command.processed_text}           ")

        elif command.command_type == CommandType.RALPH_TASK:
            # Write to Ralph mission file
            self._create_ralph_task(command)
            print(f"\r[RALPH] Task created: {command.metadata.get('task_description', '')[:40]}...           ")

        elif command.command_type == CommandType.FILE_NAV:
            # Output read command
            self._output_text(command.processed_text)
            print(f"\r[NAV] {command.processed_text}           ")

        elif command.command_type == CommandType.OPENWORK_ACTION:
            # Route to OpenWork if available
            if self._openwork_callback:
                self._openwork_callback(command)
                print(f"\r[OPENWORK] {command.raw_text[:40]}...           ")
            else:
                # Fallback to text output
                self._output_text(command.processed_text)
                print(f"\r[ACTION] {command.processed_text[:40]}...           ")

        else:
            # Plain text - output directly
            self._output_text(command.processed_text)
            print(f"\r[TEXT] {command.processed_text[:50]}{'...' if len(command.processed_text) > 50 else ''}           ")

    def _output_text(self, text: str):
        """Output text to clipboard and type at cursor."""
        # Copy to clipboard
        pyperclip.copy(text)

        # Type the text
        keyboard.write(text, delay=self.config["typing_delay"])

    def _create_ralph_task(self, command: VoiceCommand):
        """Create a Ralph mission from voice command."""
        task_desc = command.metadata.get("task_description", command.raw_text)

        # Write to VOICE_INPUT.txt for voice_to_ralph.py to pick up
        voice_input_path = GENESIS_ROOT / "VOICE_INPUT.txt"
        with open(voice_input_path, "w") as f:
            f.write(f"Project: Voice Command\n")
            f.write(f"Objective: {task_desc}\n")
            f.write(f"Success: Task completed successfully\n")
            f.write(f"Max iterations: 5\n")

        # Also output to terminal
        self._output_text(f"Ralph task: {task_desc}")

    def on_openwork_action(self, callback):
        """Register callback for OpenWork actions."""
        self._openwork_callback = callback

    def run(self):
        """Main loop with hotkey detection."""
        if not DEPS_AVAILABLE:
            print("Cannot run - missing dependencies")
            return

        self.load_model()

        print("=" * 60)
        print("  CLAUDE CODE VOICE DICTATION (Enhanced)")
        print("=" * 60)
        print(f"  Hotkey: {self.config['hotkey'].upper()} (hold to record)")
        print(f"  Model:  {self.config['model']}")
        print(f"  Device: {self.config['device']}")
        print("=" * 60)
        print("\nCommands recognized:")
        print("  - Slash commands: 'commit', 'help', 'clear', etc.")
        print("  - Ralph tasks: 'Ralph, create a dashboard...'")
        print("  - File nav: 'Show me file core/app.py'")
        print("  - Code actions: 'Fix the bug in...', 'Add tests for...'")
        print("  - OpenWork: 'Open browser', 'Create file...'")
        print("  - Or just dictate text normally")
        print("=" * 60)
        print("\nPress Ctrl+C to exit.\n")
        print("Ready. Waiting for hotkey...")

        try:
            while True:
                if keyboard.is_pressed(self.config["hotkey"]):
                    if not self.recording:
                        self.start_recording()
                else:
                    if self.recording:
                        self.stop_recording()
                time.sleep(0.05)

        except KeyboardInterrupt:
            print("\n\nExiting...")
            if self.recording:
                self.stop_recording()

    def get_history(self, limit: int = 10) -> List[Dict[str, Any]]:
        """Get recent command history."""
        return [
            {
                "command_type": cmd.command_type.name,
                "raw_text": cmd.raw_text,
                "processed_text": cmd.processed_text,
                "metadata": cmd.metadata
            }
            for cmd in self.command_history[-limit:]
        ]


def main():
    """CLI entry point."""
    import argparse

    parser = argparse.ArgumentParser(description="Claude Code Voice Dictation")
    parser.add_argument("--model", type=str, default="medium.en",
                        help="Whisper model (tiny, base, small, medium, large-v3-turbo)")
    parser.add_argument("--hotkey", type=str, default="ctrl+alt+v",
                        help="Activation hotkey")
    parser.add_argument("--test", action="store_true",
                        help="Run command parser test")
    args = parser.parse_args()

    if args.test:
        print("Testing command parser...\n")

        test_inputs = [
            "commit",
            "show help",
            "Ralph, create a new dashboard component",
            "show me file core/app.py",
            "fix the bug in the login function",
            "open browser and go to github",
            "This is just regular dictation text",
            "add tests for the voice module",
            "what is the status",
        ]

        for text in test_inputs:
            cmd = ClaudeCodeCommandParser.parse(text)
            print(f"  '{text}'")
            print(f"    -> Type: {cmd.command_type.name}")
            print(f"    -> Output: {cmd.processed_text}")
            print()

        print("Test complete!")
        return

    # Check for CUDA
    try:
        import torch
        if torch.cuda.is_available():
            gpu_name = torch.cuda.get_device_name(0)
            gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1024**3
            print(f"GPU: {gpu_name} ({gpu_mem:.1f}GB)")
        else:
            print("INFO: CUDA not available, using CPU")
    except ImportError:
        print("INFO: PyTorch not installed, using CPU")

    # Run voice dictation
    voice = ClaudeCodeVoice(config={
        "model": args.model,
        "device": "auto",
        "compute_type": "auto",
        "hotkey": args.hotkey,
        "sample_rate": 16000,
        "channels": 1,
        "vad_threshold_ms": 500,
        "typing_delay": 0.005,
    })

    voice.run()


if __name__ == "__main__":
    main()
