# FastAPI shim around diffusers' StableAudioPipeline.
# Single endpoint POST /v1/audio/sfx returns a WAV blob.
# Model is loaded once on startup and held in process memory.
import asyncio
import io
import os
import time
from contextlib import asynccontextmanager
from typing import Optional

import soundfile as sf
import torch
from diffusers import StableAudioPipeline
from fastapi import FastAPI, HTTPException, Response
from pydantic import BaseModel, Field

MODEL_ID = os.environ.get("SAO_MODEL", "stabilityai/stable-audio-open-1.0")
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32

state: dict = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    print(f"[sao] loading {MODEL_ID} on {DEVICE} ({DTYPE})", flush=True)
    t0 = time.time()
    pipe = StableAudioPipeline.from_pretrained(MODEL_ID, torch_dtype=DTYPE)
    pipe = pipe.to(DEVICE)
    state["pipe"] = pipe
    # Single-GPU diffusers pipelines aren't reentrant — concurrent calls
    # share the same scheduler step_index counter and corrupt each
    # other (observed: IndexError in cosine_dpmsolver_multistep when
    # two requests overlap mid-run). Serialize at the request boundary.
    state["lock"] = asyncio.Lock()
    print(f"[sao] loaded in {time.time() - t0:.1f}s", flush=True)
    yield
    state.clear()


app = FastAPI(lifespan=lifespan)


class SfxRequest(BaseModel):
    prompt: str = Field(..., min_length=1)
    negative_prompt: Optional[str] = "Low quality."
    duration: float = Field(10.0, gt=0.5, le=47.0)
    steps: int = Field(100, ge=10, le=300)
    seed: Optional[int] = None
    cfg_scale: float = Field(7.0, gt=0.0, le=20.0)


@app.get("/health")
def health():
    return {
        "status": "ok",
        "model": MODEL_ID,
        "device": DEVICE,
        "loaded": "pipe" in state,
    }


@app.post("/v1/audio/sfx")
async def sfx(req: SfxRequest):
    pipe = state.get("pipe")
    lock = state.get("lock")
    if pipe is None or lock is None:
        raise HTTPException(503, "model not loaded yet")

    generator = None
    if req.seed is not None:
        generator = torch.Generator(DEVICE).manual_seed(req.seed)

    # Hold the lock for the whole inference + encode. Concurrent
    # requests queue cleanly instead of racing the scheduler's
    # step_index counter into an IndexError.
    async with lock:
        audio = await asyncio.to_thread(
            lambda: pipe(
                req.prompt,
                negative_prompt=req.negative_prompt,
                num_inference_steps=req.steps,
                audio_end_in_s=req.duration,
                num_waveforms_per_prompt=1,
                guidance_scale=req.cfg_scale,
                generator=generator,
            ).audios
        )
        waveform = audio[0].T.float().cpu().numpy()

    buf = io.BytesIO()
    sf.write(buf, waveform, pipe.vae.sampling_rate, format="WAV")
    buf.seek(0)
    return Response(content=buf.read(), media_type="audio/wav")
