#!/usr/bin/env python3
"""Small OpenAI-compatible Whisper service for the dedicated Jetson."""

from __future__ import annotations

import cgi
import json
import os
import re
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

import torch
import whisper


HOST = os.getenv("HERMES_STT_HOST", "0.0.0.0")
PORT = int(os.getenv("HERMES_STT_PORT", "9000"))
MODEL_NAME = os.getenv("HERMES_STT_MODEL", "small")
CACHE_DIR = Path(os.getenv("HERMES_STT_CACHE", "/cache/whisper"))
MAX_AUDIO_BYTES = 30 * 1024 * 1024
MODEL_LOCK = threading.Lock()


def _repetitive_token(token: str) -> bool:
    """Identify long periodic Whisper hallucinations caused by steady noise."""
    letters = "".join(re.findall(r"[a-z]+", token.lower()))
    if len(letters) < 10:
        return False
    for period in range(1, 5):
        pattern = letters[:period]
        matches = sum(
            character == pattern[index % period]
            for index, character in enumerate(letters)
        )
        if matches / len(letters) >= 0.86:
            return True
    return max(letters.count(character) for character in set(letters)) / len(letters) >= 0.78


def _clean_transcript(result: dict) -> str:
    """Drop noise-only segments and repetitive tokens while retaining speech."""
    segments = result.get("segments")
    if not isinstance(segments, list):
        segments = [{"text": result.get("text") or ""}]
    kept: list[str] = []
    for segment in segments:
        if not isinstance(segment, dict):
            continue
        text = str(segment.get("text") or "").strip()
        if not text:
            continue
        no_speech = float(segment.get("no_speech_prob") or 0.0)
        average_logprob = float(segment.get("avg_logprob") or 0.0)
        if no_speech >= 0.55 and average_logprob <= -0.55:
            continue
        words = [word for word in text.split() if not _repetitive_token(word)]
        if words:
            kept.append(" ".join(words))
    return " ".join(kept).strip()


def _json(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
    body = json.dumps(payload).encode("utf-8")
    handler.send_response(status)
    handler.send_header("Content-Type", "application/json")
    handler.send_header("Content-Length", str(len(body)))
    handler.send_header("Cache-Control", "no-store")
    handler.end_headers()
    handler.wfile.write(body)


class SpeechHandler(BaseHTTPRequestHandler):
    """Serve health and transcription without exposing a general runtime."""

    server_version = "AtlasWhisper/1"

    def log_message(self, message: str, *args: object) -> None:
        print(f"[stt] {self.address_string()} {message % args}", flush=True)

    def do_GET(self) -> None:
        if self.path != "/health":
            _json(self, 404, {"error": "not found"})
            return
        _json(
            self,
            200,
            {
                "ok": True,
                "model": MODEL_NAME,
                "device": "cuda" if torch.cuda.is_available() else "cpu",
            },
        )

    def do_POST(self) -> None:
        if self.path != "/v1/audio/transcriptions":
            _json(self, 404, {"error": "not found"})
            return
        content_length = int(self.headers.get("Content-Length", "0") or "0")
        if content_length <= 0 or content_length > MAX_AUDIO_BYTES:
            _json(self, 413, {"error": "audio payload is missing or too large"})
            return

        content_type = self.headers.get("Content-Type", "")
        if not content_type.lower().startswith("multipart/form-data"):
            _json(self, 400, {"error": "multipart/form-data is required"})
            return

        form = cgi.FieldStorage(
            fp=self.rfile,
            headers=self.headers,
            environ={
                "REQUEST_METHOD": "POST",
                "CONTENT_TYPE": content_type,
                "CONTENT_LENGTH": str(content_length),
            },
        )
        audio = form["file"] if "file" in form else None
        if audio is None or not getattr(audio, "file", None):
            _json(self, 400, {"error": "file is required"})
            return

        suffix = Path(getattr(audio, "filename", "audio.wav") or "audio.wav").suffix
        suffix = suffix if suffix in {".wav", ".webm", ".ogg", ".mp3", ".m4a"} else ".wav"
        language = str(form.getfirst("language", "auto") or "auto").strip().lower()
        temp_path = ""
        try:
            with tempfile.NamedTemporaryFile(prefix="atlas-stt-", suffix=suffix, delete=False) as temp:
                temp_path = temp.name
                while True:
                    chunk = audio.file.read(1024 * 1024)
                    if not chunk:
                        break
                    temp.write(chunk)

            with MODEL_LOCK:
                result = self.server.model.transcribe(  # type: ignore[attr-defined]
                    temp_path,
                    language=None if language in {"", "auto"} else language,
                    task="transcribe",
                    fp16=torch.cuda.is_available(),
                    condition_on_previous_text=False,
                    temperature=0,
                    compression_ratio_threshold=2.0,
                    logprob_threshold=-0.8,
                    no_speech_threshold=0.5,
                    verbose=False,
                )
            transcript = _clean_transcript(result)
            _json(self, 200, {"text": transcript, "model": MODEL_NAME})
        except Exception as exc:
            print(f"[stt] transcription failed: {exc}", flush=True)
            _json(self, 500, {"error": "transcription failed"})
        finally:
            if temp_path:
                try:
                    os.unlink(temp_path)
                except OSError:
                    pass


def main() -> None:
    """Warm Whisper once, then serve concurrent clients through one GPU lock."""
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    if not torch.cuda.is_available():
        raise RuntimeError("CUDA is required for the Atlas Whisper service")
    print(f"[stt] loading Whisper {MODEL_NAME} into CUDA", flush=True)
    model = whisper.load_model(MODEL_NAME, device="cuda", download_root=str(CACHE_DIR))
    server = ThreadingHTTPServer((HOST, PORT), SpeechHandler)
    server.model = model  # type: ignore[attr-defined]
    print(f"[stt] ready on {HOST}:{PORT}", flush=True)
    server.serve_forever(poll_interval=0.25)


if __name__ == "__main__":
    main()
