"""Audio streaming mixin for NoteFlowClient."""

from __future__ import annotations

import queue
import threading
import time
from collections.abc import Iterator
from typing import TYPE_CHECKING

import grpc

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.grpc._config import STREAMING_CONFIG
from noteflow.grpc._types import ConnectionCallback, TranscriptCallback, TranscriptSegment
from noteflow.grpc.proto import noteflow_pb2
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    import numpy as np
    from numpy.typing import NDArray

    from noteflow.grpc._client_mixins.protocols import ClientHost

logger = get_logger(__name__)


class StreamingClientMixin:
    """Mixin providing audio streaming operations for NoteFlowClient."""

    # These are expected to be set by the host class
    on_transcript: TranscriptCallback | None
    on_connection_change: ConnectionCallback | None
    stream_thread: threading.Thread | None
    audio_queue: queue.Queue[tuple[str, NDArray[np.float32], float]]
    stop_streaming_event: threading.Event
    current_meeting_id: str | None

    def start_streaming(self: ClientHost, meeting_id: str) -> bool:
        """Start streaming audio for a meeting.

        Args:
            meeting_id: Meeting ID to stream to.

        Returns:
            True if streaming started.
        """
        if not self.stub:
            logger.error("Not connected")
            return False

        if self.stream_thread and self.stream_thread.is_alive():
            logger.warning("Already streaming")
            return False

        self.current_meeting_id = meeting_id
        self.stop_streaming_event.clear()

        # Clear any pending audio
        while not self.audio_queue.empty():
            try:
                self.audio_queue.get_nowait()
            except queue.Empty:
                break

        # Start streaming thread
        self.stream_thread = threading.Thread(
            target=self.stream_worker,
            daemon=True,
        )
        self.stream_thread.start()

        logger.info("Started streaming for meeting %s", meeting_id)
        return True

    def stop_streaming(self: ClientHost) -> None:
        """Stop streaming audio."""
        self.stop_streaming_event.set()

        if self.stream_thread:
            self.stream_thread.join(timeout=2.0)
            if self.stream_thread.is_alive():
                logger.warning("Stream thread did not exit within timeout")
            else:
                self.stream_thread = None

        self.current_meeting_id = None
        logger.info("Stopped streaming")

    def send_audio(
        self: ClientHost,
        audio: NDArray[np.float32],
        timestamp: float | None = None,
    ) -> bool:
        """Send audio chunk to server.

        Non-blocking - queues audio for streaming thread.

        Args:
            audio: Audio samples (float32, mono, 16kHz).
            timestamp: Optional capture timestamp.

        Returns:
            True if queued successfully, False if queue is full or not streaming.
        """
        if not self.current_meeting_id:
            return False

        if timestamp is None:
            timestamp = time.time()

        try:
            self.audio_queue.put_nowait((self.current_meeting_id, audio, timestamp))
            return True
        except queue.Full:
            logger.warning("Audio queue full for meeting %s", self.current_meeting_id)
            return False

    def stream_worker(self: ClientHost) -> None:
        """Background thread for audio streaming."""
        if not self.stub:
            return

        def audio_generator() -> Iterator[noteflow_pb2.AudioChunk]:
            """Generate audio chunks from queue."""
            while not self.stop_streaming_event.is_set():
                try:
                    meeting_id, audio, timestamp = self.audio_queue.get(
                        timeout=STREAMING_CONFIG.CHUNK_TIMEOUT_SECONDS,
                    )
                    yield noteflow_pb2.AudioChunk(
                        meeting_id=meeting_id,
                        audio_data=audio.tobytes(),
                        timestamp=timestamp,
                        sample_rate=DEFAULT_SAMPLE_RATE,
                        channels=1,
                    )
                except queue.Empty:
                    continue

        try:
            responses = self.stub.StreamTranscription(audio_generator())

            for response in responses:
                if self.stop_streaming_event.is_set():
                    break

                if response.update_type == noteflow_pb2.UPDATE_TYPE_FINAL:
                    segment = TranscriptSegment(
                        segment_id=response.segment.segment_id,
                        text=response.segment.text,
                        start_time=response.segment.start_time,
                        end_time=response.segment.end_time,
                        language=response.segment.language,
                        is_final=True,
                        speaker_id=response.segment.speaker_id,
                        speaker_confidence=response.segment.speaker_confidence,
                    )
                    self.notify_transcript(segment)

                elif response.update_type == noteflow_pb2.UPDATE_TYPE_PARTIAL:
                    segment = TranscriptSegment(
                        segment_id=0,
                        text=response.partial_text,
                        start_time=0,
                        end_time=0,
                        language="",
                        is_final=False,
                    )
                    self.notify_transcript(segment)

        except grpc.RpcError as e:
            logger.error("Stream error: %s", e)
            self.notify_connection(False, f"Stream error: {e}")

    def notify_transcript(self: ClientHost, segment: TranscriptSegment) -> None:
        """Notify transcript callback.

        Args:
            segment: Transcript segment.
        """
        if self.on_transcript:
            try:
                self.on_transcript(segment)
            # INTENTIONAL BROAD HANDLER: User-provided callback
            # - External code can raise any exception
            # - Must not crash client streaming loop
            except Exception as e:
                logger.error("Transcript callback error: %s", e)

    def notify_connection(self: ClientHost, connected: bool, message: str) -> None:
        """Notify connection state change.

        Args:
            connected: Connection state.
            message: Status message.
        """
        if self.on_connection_change:
            try:
                self.on_connection_change(connected, message)
            # INTENTIONAL BROAD HANDLER: User-provided callback
            # - External code can raise any exception
            # - Must not crash client streaming loop
            except Exception as e:
                logger.error("Connection callback error: %s", e)
