///|
/// A request to the legacy `/completions` (text completion) endpoint.
///
/// This is the pre-chat completion API, still supported by some models and
/// many OpenAI-compatible gateways for raw prompt completion.
pub(all) struct CompletionRequest {
  model : String
  prompt : String
  mut max_tokens : Int?
  mut temperature : Double?
  mut top_p : Double?
  mut stop : Array[String]?
  mut suffix : String?
}

///|
/// Create a completion request.
pub fn CompletionRequest::new(
  model : String,
  prompt : String,
) -> CompletionRequest {
  {
    model,
    prompt,
    max_tokens: None,
    temperature: None,
    top_p: None,
    stop: None,
    suffix: None,
  }
}

///|
pub fn CompletionRequest::max_tokens(
  self : CompletionRequest,
  n : Int,
) -> CompletionRequest {
  self.max_tokens = Some(n)
  self
}

///|
pub fn CompletionRequest::temperature(
  self : CompletionRequest,
  t : Double,
) -> CompletionRequest {
  self.temperature = Some(t)
  self
}

///|
pub fn CompletionRequest::stop(
  self : CompletionRequest,
  s : Array[String],
) -> CompletionRequest {
  self.stop = Some(s)
  self
}

///|
pub impl ToJson for CompletionRequest with fn to_json(self : CompletionRequest) -> Json {
  let obj : Map[String, Json] = {
    "model": Json::string(self.model),
    "prompt": Json::string(self.prompt),
  }
  if self.max_tokens is Some(n) {
    obj["max_tokens"] = Json::number(n.to_double())
  }
  if self.temperature is Some(t) {
    obj["temperature"] = Json::number(t)
  }
  if self.top_p is Some(p) {
    obj["top_p"] = Json::number(p)
  }
  if self.stop is Some(s) {
    obj["stop"] = s.to_json()
  }
  if self.suffix is Some(s) {
    obj["suffix"] = Json::string(s)
  }
  Json::object(obj)
}

///|
/// A single completion choice.
pub(all) struct CompletionChoice {
  index : Int
  text : String
  finish_reason : String?
} derive(Debug)

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

///|
/// A response from the legacy `/completions` endpoint.
pub(all) struct CompletionResponse {
  id : String
  model : String
  choices : Array[CompletionChoice]
  usage : Usage?
} derive(Debug)

///|
pub impl @json.FromJson for CompletionResponse with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> CompletionResponse {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError((path, "CompletionResponse: expected object"))
  }
  let id = match obj.get("id") {
    Some(String(s)) => s
    _ => ""
  }
  let model = match obj.get("model") {
    Some(String(s)) => s
    _ => ""
  }
  let choices = match obj.get("choices") {
    Some(Array(_) as c) => @json.from_json(c)
    _ => []
  }
  let usage = match obj.get("usage") {
    Some(Object(_) as u) => Some(@json.from_json(u))
    _ => None
  }
  { id, model, choices, usage }
}

///|
/// The text of the first completion choice, if any.
pub fn CompletionResponse::text(self : CompletionResponse) -> String {
  match self.choices.get(0) {
    Some(c) => c.text
    None => ""
  }
}

///|
/// Perform a legacy text completion.
pub async fn Client::completion(
  self : Client,
  request : CompletionRequest,
) -> CompletionResponse raise LLMError {
  let json = self.post_json("/completions", request.to_json())
  @json.from_json(json) catch {
    err => raise Decode(err.to_string())
  }
}