"""
Kinan-AIVA Real-Time Voice Channel
===================================
Bidirectional voice communication between Kinan (founder) and AIVA Queen.

Features:
- Whisper STT for Kinan's voice input
- Gemini/ElevenLabs TTS for AIVA's spoken responses
- WebSocket server for OpenWork desktop integration
- Integration with KinanLiaisonSystem for message context
- Command recognition for voice approvals
- Queue-based async message handling

Usage:
    from core.voice.kinan_aiva_voice_channel import KinanAivaVoiceChannel

    channel = KinanAivaVoiceChannel()
    await channel.start()

    # Or from CLI:
    python -m core.voice.kinan_aiva_voice_channel --serve

Author: Genesis System
Version: 1.0.0
"""

import os
import sys
import json
import asyncio
import logging
import tempfile
import wave
import base64
import re
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable, Awaitable
from dataclasses import dataclass, field
from enum import Enum, auto
import threading
import queue

# Add genesis path
GENESIS_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(GENESIS_ROOT))

# Conditional imports with fallbacks
try:
    import numpy as np
    NUMPY_AVAILABLE = True
except ImportError:
    NUMPY_AVAILABLE = False

try:
    import websockets
    from websockets.server import serve as ws_serve
    WEBSOCKETS_AVAILABLE = True
except ImportError:
    WEBSOCKETS_AVAILABLE = False

try:
    import sounddevice as sd
    SOUNDDEVICE_AVAILABLE = True
except ImportError:
    SOUNDDEVICE_AVAILABLE = False

from core.gemini_live_api_client import GeminiLiveAPIClient # New import
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class VoiceCommandType(Enum):
    """Types of voice commands recognized."""
    APPROVE = auto()      # "Approve", "Yes", "Go ahead"
    REJECT = auto()       # "Reject", "No", "Cancel"
    QUESTION = auto()     # Questions for AIVA
    TASK = auto()         # Task creation commands
    STATUS = auto()       # Status requests
    CONVERSATION = auto() # General conversation
    UNKNOWN = auto()


@dataclass
class VoiceMessage:
    """A voice message in the channel."""
    message_id: str
    direction: str  # "kinan_to_aiva" or "aiva_to_kinan"
    text: str
    audio_data: Optional[bytes] = None
    command_type: VoiceCommandType = VoiceCommandType.CONVERSATION
    timestamp: datetime = field(default_factory=datetime.utcnow)
    context: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "message_id": self.message_id,
            "direction": self.direction,
            "text": self.text,
            "command_type": self.command_type.name,
            "timestamp": self.timestamp.isoformat(),
            "context": self.context,
            "has_audio": self.audio_data is not None
        }


class VoiceCommandParser:
    """Parse voice input to detect commands and intents."""

    APPROVAL_PATTERNS = [
        r"\b(approve|approved|yes|go ahead|proceed|do it|confirmed?|affirmative)\b",
        r"\b(looks? good|sounds? good|that'?s? fine|ok(ay)?)\b"
    ]

    REJECTION_PATTERNS = [
        r"\b(reject|rejected|no|cancel|stop|don'?t|negative|abort)\b",
        r"\b(not now|hold off|wait|pause)\b"
    ]

    QUESTION_PATTERNS = [
        r"\?$",
        r"\b(what|who|where|when|why|how|which|can you|could you|would you)\b"
    ]

    TASK_PATTERNS = [
        r"\b(create|build|make|implement|add|fix|update|deploy|run|execute)\b",
        r"\b(ralph|task|story|prd)\b"
    ]

    STATUS_PATTERNS = [
        r"\b(status|progress|update|report|health|how('?s| is))\b"
    ]

    @classmethod
    def parse(cls, text: str) -> VoiceCommandType:
        """Parse text to determine command type."""
        text_lower = text.lower().strip()

        # Check approval patterns first (for pending approvals)
        for pattern in cls.APPROVAL_PATTERNS:
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommandType.APPROVE

        # Check rejection patterns
        for pattern in cls.REJECTION_PATTERNS:
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommandType.REJECT

        # Check if it's a question
        for pattern in cls.QUESTION_PATTERNS:
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommandType.QUESTION

        # Check task patterns
        for pattern in cls.TASK_PATTERNS:
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommandType.TASK

        # Check status patterns
        for pattern in cls.STATUS_PATTERNS:
            if re.search(pattern, text_lower, re.IGNORECASE):
                return VoiceCommandType.STATUS

        return VoiceCommandType.CONVERSATION

    @classmethod
    def extract_task_details(cls, text: str) -> Dict[str, Any]:
        """Extract task details from voice command."""
        return {
            "raw_text": text,
            "detected_action": cls._extract_action(text),
            "detected_target": cls._extract_target(text)
        }

    @classmethod
    def _extract_action(cls, text: str) -> Optional[str]:
        """Extract action verb from text."""
        actions = ["create", "build", "make", "implement", "add", "fix",
                   "update", "deploy", "run", "execute", "delete", "remove"]
        text_lower = text.lower()
        for action in actions:
            if action in text_lower:
                return action
        return None

    @classmethod
    def _extract_target(cls, text: str) -> Optional[str]:
        """Extract target/object from text."""
        # Simple extraction - get words after action verb
        text_lower = text.lower()
        for action in ["create", "build", "make", "implement", "add", "fix"]:
            if action in text_lower:
                idx = text_lower.index(action) + len(action)
                target = text[idx:].strip()
                # Clean up common fillers
                target = re.sub(r"^(a|an|the|some)\s+", "", target, flags=re.IGNORECASE)
                return target[:100] if target else None
        return None







class KinanAivaVoiceChannel:
    """
    Main voice channel for Kinan-AIVA communication.

    Provides:
    - Bidirectional voice communication
    - WebSocket server for OpenWork integration
    - Command recognition for approvals
    - Integration with KinanLiaisonSystem
    """

    def __init__(
        self,
        websocket_host: str = "0.0.0.0",
        websocket_port: int = 8766,
        liaison_db_path: str = None,
        gemini_api_key: Optional[str] = None
    ):
        self.gemini_live_client = GeminiLiveAPIClient(api_key=gemini_api_key or os.environ.get("GEMINI_API_KEY", "YOUR_GEMINI_API_KEY"))

        # WebSocket config
        self.ws_host = websocket_host
        self.ws_port = websocket_port
        self.ws_clients: List[Any] = []

        # Message queues
        self.inbound_queue: asyncio.Queue = asyncio.Queue()
        self.outbound_queue: asyncio.Queue = asyncio.Queue()
        self.inbound_audio_streams: Dict[str, asyncio.Queue] = {} # New: for continuous audio
        self.outbound_audio_streams: Dict[str, asyncio.Queue] = {} # New: for AIVA's responses to specific sessions

        # Conversation history
        self.conversation_history: List[VoiceMessage] = []
        self.max_history = 50

        # State
        self._running = False
        self._server = None

        # Callbacks
        self._message_handlers: List[Callable[[VoiceMessage], Awaitable[None]]] = []
        self._approval_callback: Optional[Callable[[str, bool], Awaitable[None]]] = None

        # Kinan Liaison integration
        self._liaison = None
        self._liaison_db_path = liaison_db_path or str(GENESIS_ROOT / "data" / "kinan_liaison.db")

        logger.info(f"KinanAivaVoiceChannel initialized (ws://{websocket_host}:{websocket_port})")

    async def receive_audio_chunk(self, session_id: str, audio_bytes: bytes):
        """
        Receives an audio chunk for a specific session and puts it into the
        inbound audio stream queue for that session.
        """
        if session_id not in self.inbound_audio_streams:
            self.inbound_audio_streams[session_id] = asyncio.Queue()
            asyncio.create_task(self._process_audio_stream(session_id)) # Start processing for new stream
            logger.info(f"Started audio stream processing for session: {session_id}")

        await self.inbound_audio_streams[session_id].put(audio_bytes)

    def _init_liaison(self):
        """Initialize KinanLiaisonSystem integration."""
        try:
            from AIVA.queen_outputs.orchestration.orch_03_kinan_liaison import KinanLiaisonSystem
            self._liaison = KinanLiaisonSystem(db_path=self._liaison_db_path)
            logger.info("KinanLiaisonSystem integrated")
        except Exception as e:
            logger.warning(f"Could not initialize KinanLiaisonSystem: {e}")

    async def start(self):
        """Start the voice channel."""
        if self._running:
            return

        self._running = True
        self._init_liaison()

        # Start WebSocket server
        if WEBSOCKETS_AVAILABLE:
            self._server = await ws_serve(
                self._handle_websocket,
                self.ws_host,
                self.ws_port
            )
            logger.info(f"WebSocket server started on ws://{self.ws_host}:{self.ws_port}")

        # Start message processors
        asyncio.create_task(self._process_inbound())
        asyncio.create_task(self._process_outbound())

        if self._liaison:
            await self._liaison.start()

        logger.info("KinanAivaVoiceChannel started")

    async def stop(self):
        """Stop the voice channel."""
        self._running = False

        if self._server:
            self._server.close()
            await self._server.wait_closed()

        if self._liaison:
            await self._liaison.stop()

        logger.info("KinanAivaVoiceChannel stopped")

    async def _handle_websocket(self, websocket, path):
        """Handle WebSocket connections from OpenWork."""
        self.ws_clients.append(websocket)
        client_id = id(websocket)
        logger.info(f"OpenWork client connected: {client_id}")

        try:
            async for message in websocket:
                await self._handle_ws_message(websocket, message)
        except websockets.exceptions.ConnectionClosed:
            pass
        finally:
            self.ws_clients.remove(websocket)
            logger.info(f"OpenWork client disconnected: {client_id}")

    async def _handle_ws_message(self, websocket, message: str):
        """Handle incoming WebSocket message."""
        try:
            data = json.loads(message)
            msg_type = data.get("type")

            if msg_type == "audio":
                # Audio data from OpenWork
                audio_b64 = data.get("audio")
                if audio_b64:
                    audio_bytes = base64.b64decode(audio_b64)
                    text = self.stt.transcribe(audio_bytes)

                    if text:
                        voice_msg = await self.receive_from_kinan(text, audio_bytes)

                        # Send transcription back
                        await websocket.send(json.dumps({
                            "type": "transcription",
                            "text": text,
                            "command_type": voice_msg.command_type.name,
                            "message_id": voice_msg.message_id
                        }))

            elif msg_type == "text":
                # Direct text input
                text = data.get("text", "").strip()
                if text:
                    voice_msg = await self.receive_from_kinan(text)

                    await websocket.send(json.dumps({
                        "type": "received",
                        "message_id": voice_msg.message_id,
                        "command_type": voice_msg.command_type.name
                    }))

            elif msg_type == "ping":
                await websocket.send(json.dumps({"type": "pong"}))

        except json.JSONDecodeError:
            logger.warning(f"Invalid JSON from WebSocket: {message[:100]}")
        except Exception as e:
            logger.error(f"WebSocket message handling error: {e}")

    async def receive_from_kinan(
        self,
        text: str,
        audio_data: Optional[bytes] = None,
        session_id: Optional[str] = None
    ) -> VoiceMessage:
        """Process voice/text input from Kinan."""
        import uuid

        # Parse command type
        command_type = VoiceCommandParser.parse(text)

        # Create message
        message = VoiceMessage(
            message_id=str(uuid.uuid4()),
            direction="kinan_to_aiva",
            text=text,
            audio_data=audio_data,
            command_type=command_type,
            context={"session_id": session_id} if session_id else {}
        )

        # Add to history
        self.conversation_history.append(message)
        if len(self.conversation_history) > self.max_history:
            self.conversation_history.pop(0)

        # Queue for processing
        await self.inbound_queue.put(message)

        logger.info(f"Received from Kinan (Session: {session_id or 'N/A'}): [{command_type.name}] {text[:50]}...")
        return message

    async def speak_to_kinan(
        self,
        text: str,
        context: Optional[Dict[str, Any]] = None
    ) -> VoiceMessage:
        """Send voice response to Kinan."""
        import uuid

        # Generate audio (now handled by Gemini Live API stream)
        audio_data = None # Gemini Live API will provide audio via the stream

        # Create message
        message = VoiceMessage(
            message_id=str(uuid.uuid4()),
            direction="aiva_to_kinan",
            text=text,
            audio_data=audio_data, # Will be None as audio is streamed directly
            command_type=VoiceCommandType.CONVERSATION,
            context=context or {}
        )

        # Add to history
        self.conversation_history.append(message)
        if len(self.conversation_history) > self.max_history:
            self.conversation_history.pop(0)

        # Route audio based on session_id in context
        session_id = message.context.get("session_id")
        if session_id:
            if session_id not in self.outbound_audio_streams:
                self.outbound_audio_streams[session_id] = asyncio.Queue()
            await self.outbound_audio_streams[session_id].put((audio_data, text)) # Put tuple (audio, text)
            logger.info(f"Speaking to Kinan (Session: {session_id}): {text[:50]}...")
        else:
            # Default to general outbound queue for OpenWork clients
            await self.outbound_queue.put(message)
            logger.info(f"Speaking to Kinan: {text[:50]}...")

        return message

    async def _process_inbound(self):
        """Process inbound messages from Kinan."""
        while self._running:
            try:
                message = await asyncio.wait_for(
                    self.inbound_queue.get(),
                    timeout=1.0
                )

                # Handle based on command type
                if message.command_type == VoiceCommandType.APPROVE:
                    await self._handle_approval(message, approved=True)

                elif message.command_type == VoiceCommandType.REJECT:
                    await self._handle_approval(message, approved=False)

                elif message.command_type == VoiceCommandType.STATUS:
                    await self._handle_status_request(message)

                elif message.command_type == VoiceCommandType.TASK:
                    await self._handle_task_command(message)

                else:
                    # General conversation or question
                    await self._handle_conversation(message)

                # Call registered handlers
                for handler in self._message_handlers:
                    try:
                        await handler(message)
                    except Exception as e:
                        logger.error(f"Message handler error: {e}")

            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Inbound processing error: {e}")

    async def _process_outbound(self):
        """Process outbound messages to Kinan."""
        while self._running:
            try:
                message = await asyncio.wait_for(
                    self.outbound_queue.get(),
                    timeout=1.0
                )

                # Send to all WebSocket clients
                if message.audio_data:
                    payload = {
                        "type": "voice_response",
                        "message_id": message.message_id,
                        "text": message.text,
                        "audio": base64.b64encode(message.audio_data).decode(),
                        "timestamp": message.timestamp.isoformat()
                    }
                else:
                    payload = {
                        "type": "text_response",
                        "message_id": message.message_id,
                        "text": message.text,
                        "timestamp": message.timestamp.isoformat()
                    }

                payload_json = json.dumps(payload)

                for client in self.ws_clients:
                    try:
                        await client.send(payload_json)
                    except Exception as e:
                        logger.error(f"WebSocket send error: {e}")

            except asyncio.TimeoutError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Outbound processing error: {e}")

    async def _handle_approval(self, message: VoiceMessage, approved: bool):
        """Handle approval/rejection voice command."""
        action = "approved" if approved else "rejected"

        # Check for pending approvals in liaison
        if self._liaison:
            pending = self._liaison.approvals.get_pending_approvals()
            if pending:
                # Approve/reject most recent pending item
                latest = pending[-1]
                from AIVA.queen_outputs.orchestration.orch_03_kinan_liaison import ApprovalStatus

                status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED
                self._liaison.approvals.record_decision(
                    latest.request_id,
                    status,
                    f"Voice {action} by Kinan"
                )

                response = f"I've {action} the request: {latest.action_description[:50]}"
                await self.speak_to_kinan(response)
                return

        # No pending items
        await self.speak_to_kinan(f"I heard you say {action}, but there are no pending items to process.")

        # Call approval callback if registered
        if self._approval_callback:
            await self._approval_callback(message.text, approved)

    async def _handle_status_request(self, message: VoiceMessage):
        """Handle status request voice command."""
        if self._liaison:
            stats = self._liaison.get_statistics()
            pending = stats.get("pending_approvals", 0)
            resources = stats.get("pending_resources", 0)

            response = f"Current status: {pending} pending approvals, {resources} resource requests."

            # Add more context if available
            if pending > 0:
                response += " Would you like me to read the pending items?"
        else:
            response = "I'm online and listening. All systems operational."

        await self.speak_to_kinan(response)

    async def _handle_task_command(self, message: VoiceMessage):
        """Handle task creation voice command."""
        details = VoiceCommandParser.extract_task_details(message.text)
        action = details.get("detected_action", "create")
        target = details.get("detected_target", "task")

        # Acknowledge and confirm
        response = f"I understood you want to {action} {target}. Let me add that to the task queue."
        await self.speak_to_kinan(response)

        # TODO: Integrate with RWL task queue
        # This would connect to core/voice_to_task.py or core/voice_to_ralph.py

    async def _handle_conversation(self, message: VoiceMessage):
        """Handle general conversation."""
        # For now, acknowledge receipt
        # In full implementation, this would call AIVA's LLM for response

        if message.command_type == VoiceCommandType.QUESTION:
            response = "That's a good question. Let me think about it."
        else:
            response = "I heard you. Is there anything specific you'd like me to do?"

        await self.speak_to_kinan(response)

    def on_message(self, handler: Callable[[VoiceMessage], Awaitable[None]]):
        """Register a message handler callback."""
        self._message_handlers.append(handler)

    def on_approval(self, callback: Callable[[str, bool], Awaitable[None]]):
        """Register approval callback for OpenWork integration."""
        self._approval_callback = callback

    def get_conversation_history(self, limit: int = 10) -> List[Dict[str, Any]]:
        """Get recent conversation history."""
        return [msg.to_dict() for msg in self.conversation_history[-limit:]]

    async def _process_audio_stream(self, session_id: str):
        """
        Processes a continuous audio stream for a given session using Gemini Live API,
        performing STT and triggering AIVA's response generation and TTS.
        """
        current_transcription = ""
        last_stt_segment_time = asyncio.get_event_loop().time()

        async def audio_chunk_generator():
            while self._running:
                try:
                    chunk = await asyncio.wait_for(self.inbound_audio_streams[session_id].get(), timeout=0.1)
                    yield chunk
                except asyncio.TimeoutError:
                    # No audio for a short period, keep yielding to maintain connection
                    yield b""
                except asyncio.CancelledError:
                    logger.info(f"Audio chunk generator for session {session_id} cancelled.")
                    break
                except Exception as e:
                    logger.error(f"Error in audio chunk generator for session {session_id}: {e}")
                    break
            logger.info(f"Audio chunk generator for session {session_id} finished.")

        try:
            async for response in self.gemini_live_client.generate_response_stream(
                audio_chunk_generator(),
                context=f"Conversation with Kinan on session {session_id}" # Example context
            ):
                if response.get("type") == "stt_segment":
                    segment_text = response.get("text_segment", "")
                    if segment_text:
                        current_transcription += segment_text + " "
                        last_stt_segment_time = asyncio.get_event_loop().time()
                        logger.debug(f"Session {session_id} STT: {segment_text}")
                    
                    # Heuristic to detect end of user's turn (e.g., pause in speaking)
                    # For a real system, this would be handled by Gemini's endpointing.
                    if (asyncio.get_event_loop().time() - last_stt_segment_time > 1.0) and current_transcription.strip():
                        full_transcription = current_transcription.strip()
                        current_transcription = "" # Reset for next turn
                        logger.info(f"Session {session_id} Full STT: {full_transcription}")
                        
                        # Process the transcribed text as if from Kinan
                        # AIVA's response will be generated by Gemini and returned in the stream
                        _ = await self.receive_from_kinan(full_transcription, session_id=session_id)

                elif response.get("type") == "tts_response":
                    audio_b64 = response.get("audio_base64")
                    text = response.get("text")
                    if audio_b64 and text:
                        audio_data = base64.b64decode(audio_b64)
                        if session_id not in self.outbound_audio_streams:
                            self.outbound_audio_streams[session_id] = asyncio.Queue()
                        await self.outbound_audio_streams[session_id].put((audio_data, text))
                        logger.debug(f"Session {session_id} TTS received from Gemini: {text[:50]}...")

                elif response.get("type") == "multimodal_output":
                    logger.info(f"Session {session_id} Multimodal output received: {response}")
                    # TODO: Handle multimodal outputs (e.g., browser commands)

            logger.info(f"Gemini Live API stream for session {session_id} finished.")

        except asyncio.CancelledError:
            logger.info(f"Audio stream processing for session {session_id} cancelled.")
        except Exception as e:
            logger.error(f"Error processing audio stream with Gemini Live API for session {session_id}: {e}")
        finally:
            # Clean up queue when loop exits
            if session_id in self.inbound_audio_streams:
                del self.inbound_audio_streams[session_id]
            if session_id in self.outbound_audio_streams:
                # Ensure outbound queue is cleared or marked for cleanup
                # Actual cleanup depends on outbound consumer lifecycle
                pass 
            logger.info(f"Audio stream processing for session {session_id} stopped and cleaned up.")


# CLI interface
async def main():
    """CLI for testing the voice channel."""
    import argparse

    parser = argparse.ArgumentParser(description="Kinan-AIVA Voice Channel")
    parser.add_argument("--serve", action="store_true", help="Start WebSocket server")
    parser.add_argument("--port", type=int, default=8766, help="WebSocket port")
    parser.add_argument("--model", type=str, default="medium.en", help="Whisper model")
    parser.add_argument("--tts", type=str, default="gemini", choices=["gemini", "elevenlabs", "local"])
    parser.add_argument("--test", action="store_true", help="Run test mode")
    args = parser.parse_args()

    if args.serve:
        channel = KinanAivaVoiceChannel(
            stt_model=args.model,
            tts_backend=args.tts,
            websocket_port=args.port
        )

        await channel.start()

        print("=" * 50)
        print("  KINAN-AIVA VOICE CHANNEL")
        print("=" * 50)
        print(f"  WebSocket: ws://localhost:{args.port}")
        print(f"  STT Model: {args.model}")
        print(f"  TTS Backend: {args.tts}")
        print("=" * 50)
        print("\nListening for OpenWork connections...")
        print("Press Ctrl+C to stop.\n")

        try:
            await asyncio.Future()  # Run forever
        except KeyboardInterrupt:
            print("\nShutting down...")
            await channel.stop()

    elif args.test:
        print("Running voice channel test...")

        channel = KinanAivaVoiceChannel(
            stt_model="tiny.en",  # Fast for testing
            tts_backend="local"
        )

        # Test command parsing
        test_inputs = [
            "Approve the deployment",
            "No, cancel that",
            "What's the status?",
            "Create a new dashboard",
            "How are you doing today?"
        ]

        for text in test_inputs:
            cmd_type = VoiceCommandParser.parse(text)
            print(f"  '{text}' -> {cmd_type.name}")

        print("\nTest complete!")

    else:
        parser.print_help()


if __name__ == "__main__":
    asyncio.run(main())
