///|
/// A request to the `/audio/speech` (text-to-speech) endpoint.
pub(all) struct SpeechRequest {
  model : String
  input : String
  voice : String
  mut response_format : String?
  mut speed : Double?
}

///|
/// Create a text-to-speech request.
pub fn SpeechRequest::new(
  model : String,
  input : String,
  voice : String,
) -> SpeechRequest {
  { model, input, voice, response_format: None, speed: None }
}

///|
/// Set the audio output format, e.g. `"mp3"`, `"wav"`, `"opus"`.
pub fn SpeechRequest::response_format(
  self : SpeechRequest,
  fmt : String,
) -> SpeechRequest {
  self.response_format = Some(fmt)
  self
}

///|
/// Set the playback speed (0.25–4.0).
pub fn SpeechRequest::speed(self : SpeechRequest, s : Double) -> SpeechRequest {
  self.speed = Some(s)
  self
}

///|
pub impl ToJson for SpeechRequest with fn to_json(self : SpeechRequest) -> Json {
  let obj : Map[String, Json] = {
    "model": Json::string(self.model),
    "input": Json::string(self.input),
    "voice": Json::string(self.voice),
  }
  if self.response_format is Some(f) {
    obj["response_format"] = Json::string(f)
  }
  if self.speed is Some(s) {
    obj["speed"] = Json::number(s)
  }
  Json::object(obj)
}

///|
/// Synthesize speech from text. Returns the raw audio bytes.
pub async fn Client::speech(
  self : Client,
  request : SpeechRequest,
) -> Bytes raise LLMError {
  let http = self.http()
  let resp = http
    .post(self.endpoint("/audio/speech"))
    .json(request.to_json())
    .send() catch {
      err => raise Transport(err.to_string())
    }
  let code = resp.response.code
  guard code >= 200 && code < 300 else {
    let text = resp.text() catch { _ => "" }
    raise ApiError(code~, message=text)
  }
  resp.binary()
}

///|
/// A response from the `/audio/transcriptions` endpoint.
pub(all) struct TranscriptionResponse {
  text : String
  language : String?
  duration : Double?
} derive(Debug)

///|
pub impl @json.FromJson for TranscriptionResponse with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> TranscriptionResponse {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError(
      (path, "TranscriptionResponse: expected object"),
    )
  }
  let text = match obj.get("text") {
    Some(String(s)) => s
    _ => ""
  }
  let language = match obj.get("language") {
    Some(String(s)) => Some(s)
    _ => None
  }
  let duration = match obj.get("duration") {
    Some(Number(n, ..)) => Some(n)
    _ => None
  }
  { text, language, duration }
}

///|
/// Parse a transcription response from raw JSON text (as returned by the API).
///
/// The transcription endpoint itself is multipart/form-data for the audio
/// upload, which is out of scope for the JSON client; this helper covers the
/// response side so callers using a multipart layer can decode the result.
pub fn parse_transcription(
  json_text : String,
) -> TranscriptionResponse raise LLMError {
  let json = @json.parse(json_text) catch {
    err => raise Decode(err.to_string())
  }
  @json.from_json(json) catch {
    err => raise Decode(err.to_string())
  }
}

///|
/// The supported audio output formats for speech synthesis.
pub let speech_formats : Array[String] = [
  "mp3", "opus", "aac", "flac", "wav", "pcm",
]

///|
/// Whether `fmt` is a recognized speech output format.
pub fn is_valid_speech_format(fmt : String) -> Bool {
  for f in speech_formats {
    if f == fmt {
      return true
    }
  }
  false
}