///|
/// A minimal wrapper for OpenAI's Responses API (`/responses`), a newer
/// unified endpoint that accepts a single `input` (string or message list)
/// and returns an `output` array of items.
///
/// This covers the common text-in/text-out case; the full Responses API has a
/// much larger surface (tools, state, streaming) that a real client would
/// extend over time.
pub(all) struct ResponseRequest {
  model : String
  input : String
  mut instructions : String?
  mut max_output_tokens : Int?
  mut temperature : Double?
}

///|
/// Create a Responses request with a plain-text input.
pub fn ResponseRequest::new(model : String, input : String) -> ResponseRequest {
  {
    model,
    input,
    instructions: None,
    max_output_tokens: None,
    temperature: None,
  }
}

///|
/// Set top-level instructions (akin to a system prompt).
pub fn ResponseRequest::instructions(
  self : ResponseRequest,
  text : String,
) -> ResponseRequest {
  self.instructions = Some(text)
  self
}

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

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

///|
pub impl ToJson for ResponseRequest with fn to_json(self : ResponseRequest) -> Json {
  let obj : Map[String, Json] = {
    "model": Json::string(self.model),
    "input": Json::string(self.input),
  }
  if self.instructions is Some(i) {
    obj["instructions"] = Json::string(i)
  }
  if self.max_output_tokens is Some(n) {
    obj["max_output_tokens"] = Json::number(n.to_double())
  }
  if self.temperature is Some(t) {
    obj["temperature"] = Json::number(t)
  }
  Json::object(obj)
}

///|
/// A response from the `/responses` endpoint.
pub(all) struct ResponseResult {
  id : String
  model : String
  status : String
  output_text : String
  usage : Usage?
} derive(Debug)

///|
pub impl @json.FromJson for ResponseResult with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> ResponseResult {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError((path, "ResponseResult: expected object"))
  }
  let id = match obj.get("id") {
    Some(String(s)) => s
    _ => ""
  }
  let model = match obj.get("model") {
    Some(String(s)) => s
    _ => ""
  }
  let status = match obj.get("status") {
    Some(String(s)) => s
    _ => ""
  }
  // Prefer the convenience `output_text` field when present; otherwise walk
  // the `output` array and concatenate text content.
  let output_text = match obj.get("output_text") {
    Some(String(s)) => s
    _ => extract_output_text(obj.get("output"))
  }
  let usage = match obj.get("usage") {
    Some(Object(_) as u) => Some(@json.from_json(u))
    _ => None
  }
  { id, model, status, output_text, usage }
}

///|
/// Walk a Responses `output` array, concatenating the text of message items.
fn extract_output_text(output : Json?) -> String {
  let buf = StringBuilder::new()
  guard output is Some(Array(items)) else { return "" }
  for item in items {
    guard item is Object(it) else { continue }
    guard it.get("content") is Some(Array(contents)) else { continue }
    for c in contents {
      guard c is Object(cc) else { continue }
      match cc.get("text") {
        Some(String(t)) => buf.write_string(t)
        _ => ()
      }
    }
  }
  buf.to_string()
}

///|
/// Perform a request against the Responses API (`POST /responses`).
pub async fn Client::respond(
  self : Client,
  request : ResponseRequest,
) -> ResponseResult raise LLMError {
  let json = self.post_json("/responses", request.to_json())
  @json.from_json(json) catch {
    err => raise Decode(err.to_string())
  }
}