"""
OpenAI STT Adapter

Streams 16kHz slin16 PCM audio to OpenAI's Realtime Transcription API
and fires on_transcript callbacks for final transcription results.

Features:
- gpt-4o-transcribe model (default — best multilingual accuracy)
- Server-side VAD with configurable threshold/silence/prefix
- Base64 PCM encoding for the WebSocket protocol
- Clean telephony audio optimization (aggressive VAD defaults)

Protocol:
  Connect to wss://api.openai.com/v1/realtime?intent=transcription
  Send transcription_session.update with model/language/VAD config
  Send input_audio_buffer.append with base64 PCM chunks
  Receive conversation.item.input_audio_transcription.completed events

API Endpoint: wss://api.openai.com/v1/realtime?intent=transcription
Docs: https://platform.openai.com/docs/guides/realtime-transcription
"""

from __future__ import annotations

import asyncio
import base64
import json
import sys
import time
from dataclasses import dataclass
from typing import Any, AsyncIterable, Callable

import websockets
import websockets.client

from dvgateway.audio.codec import float32_to_slin16
from dvgateway.types import AudioChunk, SttAdapter, TranscriptResult


@dataclass
class OpenAISttAdapterOptions:
    api_key: str = ""
    language: str = "ko"
    model: str = "gpt-4o-transcribe"
    vad_enabled: bool = True
    vad_threshold: float = 0.4
    silence_duration_ms: int = 200
    prefix_padding_ms: int = 200


class OpenAISttAdapter(SttAdapter):
    """OpenAI STT adapter implementing the SttAdapter interface."""

    def __init__(
        self,
        api_key: str = "",
        language: str = "ko",
        model: str = "gpt-4o-transcribe",
        vad_enabled: bool = True,
        vad_threshold: float = 0.4,
        silence_duration_ms: int = 200,
        prefix_padding_ms: int = 200,
    ) -> None:
        self._opts = OpenAISttAdapterOptions(
            api_key=api_key,
            language=language,
            model=model,
            vad_enabled=vad_enabled,
            vad_threshold=vad_threshold,
            silence_duration_ms=silence_duration_ms,
            prefix_padding_ms=prefix_padding_ms,
        )
        self._transcript_handler: Callable[[TranscriptResult], None] | None = None
        self._ws: websockets.client.WebSocketClientProtocol | None = None  # type: ignore[type-arg]
        self._stopped = False

    def on_transcript(self, handler: Callable[[TranscriptResult], None]) -> None:
        self._transcript_handler = handler

    async def start_stream(self, linked_id: str, audio_stream: AsyncIterable[AudioChunk]) -> None:
        self._stopped = False
        self._ws = await self._connect()

        # Send session configuration
        await self._send_session_config()

        # Pipe audio in background
        asyncio.ensure_future(self._pipe_audio(audio_stream))

        # Read events until stream ends
        assert self._ws is not None
        try:
            async for message in self._ws:
                if self._stopped:
                    break
                try:
                    msg = json.loads(message if isinstance(message, str) else message.decode("utf-8"))
                    self._handle_event(msg)
                except (json.JSONDecodeError, UnicodeDecodeError):
                    pass
        except websockets.exceptions.ConnectionClosed:
            pass

    async def stop(self) -> None:
        self._stopped = True
        if self._ws:
            try:
                await self._ws.close(1000, "stop")
            except Exception:
                pass
            self._ws = None

    async def _connect(self) -> websockets.client.WebSocketClientProtocol:  # type: ignore[type-arg]
        url = "wss://api.openai.com/v1/realtime?intent=transcription"
        headers = {
            "Authorization": f"Bearer {self._opts.api_key}",
            "OpenAI-Beta": "realtime=v1",
        }
        ws = await websockets.connect(url, additional_headers=headers)  # type: ignore[attr-defined]
        return ws

    async def _send_session_config(self) -> None:
        if not self._ws:
            return

        session: dict[str, Any] = {
            "input_audio_format": "pcm16",
            "input_audio_transcription": {
                "model": self._opts.model,
                "language": self._opts.language,
            },
        }

        if self._opts.vad_enabled:
            session["turn_detection"] = {
                "type": "server_vad",
                "threshold": self._opts.vad_threshold,
                "prefix_padding_ms": self._opts.prefix_padding_ms,
                "silence_duration_ms": self._opts.silence_duration_ms,
            }

        msg = json.dumps({
            "type": "transcription_session.update",
            "session": session,
        })
        await self._ws.send(msg)

    async def _pipe_audio(self, audio_stream: AsyncIterable[AudioChunk]) -> None:
        # OpenAI expects ~100ms chunks (3200 bytes at 16kHz s16le)
        chunk_size = 3200
        buffer = b""

        async for chunk in audio_stream:
            if self._stopped or not self._ws:
                break

            pcm = float32_to_slin16(chunk.samples)
            buffer += pcm

            while len(buffer) >= chunk_size:
                frame = buffer[:chunk_size]
                buffer = buffer[chunk_size:]
                audio_b64 = base64.b64encode(frame).decode("ascii")
                msg = json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": audio_b64,
                })
                try:
                    await self._ws.send(msg)
                except Exception:
                    self._stopped = True
                    return

        # Flush remaining
        if buffer and self._ws and not self._stopped:
            audio_b64 = base64.b64encode(buffer).decode("ascii")
            msg = json.dumps({
                "type": "input_audio_buffer.append",
                "audio": audio_b64,
            })
            try:
                await self._ws.send(msg)
            except Exception:
                pass

    def _handle_event(self, event: dict[str, Any]) -> None:
        event_type = event.get("type", "")

        if event_type == "conversation.item.input_audio_transcription.completed":
            transcript = event.get("transcript", "")
            if transcript and self._transcript_handler:
                result = TranscriptResult(
                    linked_id="",
                    text=transcript,
                    is_final=True,
                    confidence=1.0,
                    language=self._opts.language,
                    timestamp_ms=int(time.time() * 1000),
                )
                self._transcript_handler(result)

        elif event_type == "conversation.item.input_audio_transcription.failed":
            err = event.get("error", {})
            print(
                f"[OpenAISttAdapter] transcription failed: {err.get('code', '?')} - {err.get('message', '?')}",
                file=sys.stderr,
            )

        elif event_type == "error":
            err = event.get("error", {})
            print(
                f"[OpenAISttAdapter] error: {err.get('code', '?')} - {err.get('message', '?')}",
                file=sys.stderr,
            )
