"""Thin FastAPI wrapper around sherpa-onnx's OfflineRecognizer for Parakeet-TDT.

Load the encoder/decoder/joiner/tokens once at startup; serve:
  POST /transcribe                   — our native shape
  POST /v1/audio/transcriptions      — OpenAI-compatible alias (returns {"text": ...})
  GET  /healthz                      — used by the docker healthcheck

No VAD chunking, no Silero preprocessing — parakeet-tdt handles long-form natively
and the int8 ONNX model on a 24 GB GPU eats everything we're likely to throw at it.
"""

from __future__ import annotations

import io
import logging
import os
from pathlib import Path

import numpy as np
import sherpa_onnx
import soundfile as sf
from fastapi import FastAPI, File, HTTPException, UploadFile

MODEL_DIR = Path(os.environ.get("MODEL_DIR", "/models"))
PROVIDER = os.environ.get("PROVIDER", "cuda")
NUM_THREADS = int(os.environ.get("NUM_THREADS", "1"))

REQUIRED_FILES = (
    "encoder.int8.onnx",
    "decoder.int8.onnx",
    "joiner.int8.onnx",
    "tokens.txt",
)

logger = logging.getLogger("parakeet")
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))


def _ensure_model_present() -> None:
    missing = [f for f in REQUIRED_FILES if not (MODEL_DIR / f).exists()]
    if missing:
        raise RuntimeError(
            f"Missing model files in {MODEL_DIR}: {missing}. "
            "The entrypoint is responsible for downloading them before the server starts."
        )


def _load_recognizer() -> sherpa_onnx.OfflineRecognizer:
    _ensure_model_present()
    logger.info("loading OfflineRecognizer (provider=%s, threads=%d)", PROVIDER, NUM_THREADS)
    return sherpa_onnx.OfflineRecognizer.from_transducer(
        encoder=str(MODEL_DIR / "encoder.int8.onnx"),
        decoder=str(MODEL_DIR / "decoder.int8.onnx"),
        joiner=str(MODEL_DIR / "joiner.int8.onnx"),
        tokens=str(MODEL_DIR / "tokens.txt"),
        model_type="nemo_transducer",
        provider=PROVIDER,
        num_threads=NUM_THREADS,
    )


app = FastAPI(title="Parakeet ASR (sherpa-onnx)")
recognizer = _load_recognizer()


def _decode(raw: bytes) -> str:
    try:
        samples, sample_rate = sf.read(io.BytesIO(raw), dtype="float32")
    except Exception as exc:
        raise HTTPException(400, f"Could not decode audio: {exc}") from exc
    if samples.ndim > 1:
        samples = samples.mean(axis=1).astype(np.float32)

    stream = recognizer.create_stream()
    stream.accept_waveform(sample_rate, samples)
    recognizer.decode_stream(stream)
    return stream.result.text


@app.get("/healthz")
def healthz() -> dict[str, str]:
    return {"status": "ok"}


@app.post("/transcribe")
async def transcribe(file: UploadFile = File(...)) -> dict[str, str]:
    return {"text": _decode(await file.read())}


@app.post("/v1/audio/transcriptions")
async def openai_transcriptions(file: UploadFile = File(...)) -> dict[str, str]:
    # OpenAI's shape: {"text": "..."} by default; extra fields (model, language,
    # response_format) are accepted by real OpenAI but ignored here — the model
    # choice is baked in at container startup.
    return {"text": _decode(await file.read())}
