"""Encapsulated streaming session for audio transcription."""

from __future__ import annotations

import queue
import threading
import time
from collections.abc import Iterator
from dataclasses import dataclass, field
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.client import 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 ProtoTranscriptUpdate
    from noteflow.grpc._client_mixins.protocols import NoteFlowServiceStubProtocol
    from noteflow.grpc.client import ConnectionCallback, TranscriptCallback

logger = get_logger(__name__)


@dataclass
class StreamingSessionConfig:
    """Configuration for a streaming session."""

    chunk_timeout: float = field(default=STREAMING_CONFIG.CHUNK_TIMEOUT_SECONDS)
    queue_max_size: int = field(default=STREAMING_CONFIG.QUEUE_MAX_SIZE)
    sample_rate: int = field(default=STREAMING_CONFIG.SAMPLE_RATE)
    channels: int = field(default=STREAMING_CONFIG.CHANNELS)


class StreamingSession:
    """Encapsulates state and logic for a single audio streaming session.

    This class manages the lifecycle of streaming audio to the server,
    including the background thread, audio queue, and response processing.
    """

    def __init__(
        self,
        meeting_id: str,
        stub: NoteFlowServiceStubProtocol,
        on_transcript: TranscriptCallback | None = None,
        on_connection_change: ConnectionCallback | None = None,
        config: StreamingSessionConfig | None = None,
    ) -> None:
        """Initialize a streaming session.

        Args:
            meeting_id: Meeting ID to stream to.
            stub: gRPC service stub.
            on_transcript: Callback for transcript updates.
            on_connection_change: Callback for connection state changes.
            config: Optional session configuration.
        """
        self._meeting_id = meeting_id
        self._stub = stub
        self._on_transcript = on_transcript
        self._on_connection_change = on_connection_change
        self._config = config or StreamingSessionConfig()

        self._audio_queue: queue.Queue[tuple[str, NDArray[np.float32], float]] = (
            queue.Queue(maxsize=self._config.queue_max_size)
        )
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._started = False

    @property
    def meeting_id(self) -> str:
        """Return the meeting ID for this session."""
        return self._meeting_id

    @property
    def is_streaming(self) -> bool:
        """Check if the session is currently streaming."""
        return self._started and self._thread is not None and self._thread.is_alive()

    def start(self) -> bool:
        """Start the streaming session.

        Returns:
            True if started successfully, False if already running.
        """
        if self._started:
            logger.warning("Session already started for meeting %s", self._meeting_id)
            return False

        self._stop_event.clear()
        self._clear_queue()

        self._thread = threading.Thread(
            target=self._stream_worker,
            daemon=True,
            name=f"StreamingSession-{self._meeting_id[:8]}",
        )
        self._thread.start()
        self._started = True

        logger.info("Started streaming session for meeting %s", self._meeting_id)
        return True

    def stop(self, timeout: float = 2.0) -> None:
        """Stop the streaming session.

        Args:
            timeout: Maximum time to wait for thread to exit.
        """
        self._stop_event.set()

        if self._thread:
            self._thread.join(timeout=timeout)
            if self._thread.is_alive():
                logger.warning(
                    "Stream thread for %s did not exit within timeout",
                    self._meeting_id,
                )
            else:
                self._thread = None

        self._started = False
        logger.info("Stopped streaming session for meeting %s", self._meeting_id)

    def send_audio(
        self,
        audio: NDArray[np.float32],
        timestamp: float | None = None,
    ) -> bool:
        """Queue audio chunk for streaming.

        Non-blocking - queues audio for the streaming thread.

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

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

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

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

    def _clear_queue(self) -> None:
        """Clear any pending audio from the queue."""
        while not self._audio_queue.empty():
            try:
                self._audio_queue.get_nowait()
            except queue.Empty:
                break

    def _stream_worker(self) -> None:
        """Background thread for audio streaming."""

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

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

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

                self._process_response(response)

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

    def _process_response(self, response: ProtoTranscriptUpdate) -> None:
        """Process a transcript update response.

        Args:
            response: Transcript update from server.
        """
        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)

    def _notify_transcript(self, 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 streaming session
            except Exception as e:
                logger.error("Transcript callback error: %s", e)

    def _notify_connection(self, 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 streaming session
            except Exception as e:
                logger.error("Connection callback error: %s", e)
