"""
Conversation manager for voice agent testing.

Provides the Conversation class with listen(), say(), press() methods.

Usage:
    from rehearse import TwilioCall
    from rehearse.audio.tts import ElevenLabsTTS
    from rehearse.audio.stt import ElevenLabsSTT

    tts = ElevenLabsTTS(api_key="...")
    stt = ElevenLabsSTT(api_key="...")

    async with TwilioCall(
        to="+19251234567",
        account_sid="ACxxxxx",
        auth_token="xxxxx",
        from_number="+15551234567",
        ngrok_url="abc123.ngrok-free.app",
        tts=tts,
        stt=stt,
    ) as call:
        response = await call.listen()
        await call.say("Hello!")
"""

import asyncio
import logging
import time
from dataclasses import dataclass, field
from typing import List, Tuple, TYPE_CHECKING

if TYPE_CHECKING:
    from .connectors.base import BaseConnector
    from .audio.tts.base import BaseTTS
    from .audio.stt.base import BaseSTT

logger = logging.getLogger(__name__)


def mix_audio_to_stereo(chunks: List[Tuple[bytes, float, str]]) -> bytes:
    """
    Mix audio chunks into stereo based on timestamps.

    Agent audio goes to left channel, user audio goes to right channel.
    This keeps overlapping audio clear and distinguishable.

    Args:
        chunks: List of (audio_bytes, timestamp, source) tuples.
                Audio is PCM16 24kHz mono. Source is 'agent' or 'user'.

    Returns:
        Stereo PCM16 24kHz audio as bytes
    """
    if not chunks:
        return b""

    # Sort by timestamp
    sorted_chunks = sorted(chunks, key=lambda x: x[1])

    # PCM16 24kHz mono = 2 bytes per sample, 24000 samples per second
    bytes_per_second_mono = 48000

    base_time = sorted_chunks[0][1]

    # Find the end time of the last chunk
    max_end_time = 0.0
    for audio, timestamp, _ in sorted_chunks:
        duration = len(audio) / bytes_per_second_mono
        end_time = (timestamp - base_time) + duration
        if end_time > max_end_time:
            max_end_time = end_time

    # Calculate total samples needed
    total_samples = int(max_end_time * 24000)

    # Create stereo output buffer (4 bytes per sample: 2 for left, 2 for right)
    # Initialize to silence
    output = bytearray(total_samples * 4)

    # Place each chunk in appropriate channel
    for audio, timestamp, source in sorted_chunks:
        sample_offset = int((timestamp - base_time) * 24000)

        # Determine channel offset within stereo frame (0 = left, 2 = right)
        channel_offset = 0 if source == 'agent' else 2

        # Copy samples to appropriate channel
        for i in range(0, len(audio), 2):
            sample_idx = sample_offset + (i // 2)
            if sample_idx >= total_samples:
                break

            # Position in output: sample_idx * 4 bytes per frame + channel offset
            out_pos = sample_idx * 4 + channel_offset

            # Read existing sample in this channel
            existing = int.from_bytes(output[out_pos:out_pos + 2], 'little', signed=True)
            # Read new sample
            new_sample = int.from_bytes(audio[i:i + 2], 'little', signed=True)

            # Mix (add with clipping)
            mixed = existing + new_sample
            mixed = max(-32768, min(32767, mixed))

            output[out_pos:out_pos + 2] = mixed.to_bytes(2, 'little', signed=True)

    return bytes(output)


@dataclass
class Response:
    """Represents an agent response."""

    text: str
    audio: bytes
    tool_calls: List[dict] = field(default_factory=list)
    latency: float = 0.0

    def __contains__(self, substring: str) -> bool:
        """Enable 'x in response' syntax."""
        return substring.lower() in self.text.lower()


class Conversation:
    """
    Manages a conversation with a voice agent.

    Provides a simple API for testing voice agents:
    - listen() - Wait for agent to speak
    - say() - Say something to the agent
    - press() - Send DTMF tones
    """

    def __init__(
        self,
        connector: "BaseConnector",
        tts: "BaseTTS",
        stt: "BaseSTT",
    ):
        self.connector = connector
        self.tts = tts
        self.stt = stt

        self.history: List[dict] = []
        # Store (audio_bytes, timestamp, source) tuples for stereo mixing
        # source is 'agent' (left channel) or 'user' (right channel)
        self.recorded_audio: List[Tuple[bytes, float, str]] = []

    async def listen(
        self,
        timeout: float = 15.0,
        silence_threshold: float = 1.5,
        max_duration: float = 8.0,
    ) -> Response:
        """
        Wait for the agent to speak.

        Args:
            timeout: Maximum wait time for first audio
            silence_threshold: Seconds of silence before considering complete
            max_duration: Maximum seconds of speech to capture

        Returns:
            Response with transcript and audio
        """
        logger.debug("Listening for agent response (timeout=%.1fs, silence=%.1fs, max=%.1fs)",
                     timeout, silence_threshold, max_duration)
        start = time.time()

        result = await self.connector.receive_audio(
            timeout=timeout,
            silence_threshold=silence_threshold,
            max_duration=max_duration,
        )

        latency = time.time() - start
        logger.debug("Received %d bytes of audio in %.2fs", len(result.audio), latency)

        # Transcribe if we have audio
        transcript = ""
        if result.audio:
            transcript = self.stt.transcribe(result.audio)
            # Record with timestamp and source for stereo mixing (agent = left channel)
            self.recorded_audio.append((result.audio, start, 'agent'))
            logger.info("Agent said: %s", transcript)

        response = Response(
            text=transcript,
            audio=result.audio,
            tool_calls=result.tool_calls,
            latency=latency,
        )

        self.history.append({"role": "assistant", "content": transcript})
        return response

    async def say(self, text: str) -> None:
        """
        Say something to the agent.

        Converts text to speech and sends to the agent.

        Args:
            text: The text to speak
        """
        logger.info("Saying: %s", text)
        self.history.append({"role": "user", "content": text})

        # Convert to audio
        audio = self.tts.synthesize(text)
        logger.debug("Synthesized %d bytes of audio", len(audio))

        # Record outgoing audio with timestamp and source for stereo mixing (user = right channel)
        self.recorded_audio.append((audio, time.time(), 'user'))

        # Send to agent
        await self.connector.send_audio(audio)
        logger.debug("Audio sent to agent")

    async def press(self, digits: str, pause_after: float = 0.5) -> None:
        """
        Send DTMF tones (for IVR navigation).

        Args:
            digits: Digits to press (0-9, *, #, w for pause)
            pause_after: Seconds to wait after sending

        Example:
            await call.press("5")  # Press 5
            await call.press("1w2")  # Press 1, wait, press 2
        """
        logger.info("Pressing DTMF: %s", digits)
        self.history.append({"role": "user", "content": f"[DTMF: {digits}]"})
        await self.connector.send_dtmf(digits)

        if pause_after > 0:
            await asyncio.sleep(pause_after)

    @property
    def tool_calls(self) -> list:
        """Tool calls from the agent (if available)."""
        return self.connector.tool_calls

    @property
    def transcripts(self) -> list:
        """All transcripts from this conversation."""
        return self.connector.transcripts

    def get_all_audio(self) -> bytes:
        """Get all recorded audio as stereo (agent=left, user=right)."""
        return mix_audio_to_stereo(self.recorded_audio)
