///|
/// A tool (function) definition advertised to the model.
pub(all) struct Tool {
  name : String
  description : String
  /// JSON Schema for the function parameters, as a `Json` value.
  parameters : Json
}

///|
pub impl ToJson for Tool with fn to_json(self : Tool) -> Json {
  Json::object({
    "type": Json::string("function"),
    "function": Json::object({
      "name": Json::string(self.name),
      "description": Json::string(self.description),
      "parameters": self.parameters,
    }),
  })
}

///|
/// A chat completion request. Build with `ChatRequest::new` and the
/// chainable setters, or construct the struct directly.
pub(all) struct ChatRequest {
  model : String
  messages : Array[Message]
  mut temperature : Double?
  mut max_tokens : Int?
  mut top_p : Double?
  mut stop : Array[String]?
  mut tools : Array[Tool]?
  /// Controls which (if any) tool is called: `"auto"`, `"none"`, `"required"`,
  /// or a specific function name.
  mut tool_choice : ToolChoice?
  mut frequency_penalty : Double?
  mut presence_penalty : Double?
  /// Deterministic sampling seed (best-effort, model dependent).
  mut seed : Int?
  /// Number of completions to generate.
  mut n : Int?
  /// Response format: plain text or a JSON object ("JSON mode").
  mut response_format : ResponseFormat?
  /// Per-token logit biases keyed by token id (as a string).
  mut logit_bias : Map[String, Double]?
  /// End-user identifier for abuse monitoring.
  mut user : String?
  mut stream : Bool
}

///|
/// How the model should choose among the provided tools.
pub(all) enum ToolChoice {
  /// Let the model decide (default when tools are present).
  Auto
  /// Never call a tool.
  NoneChoice
  /// Force the model to call some tool.
  Required
  /// Force a specific function by name.
  Function(String)
} derive(Eq, Debug)

///|
pub impl ToJson for ToolChoice with fn to_json(self : ToolChoice) -> Json {
  match self {
    Auto => Json::string("auto")
    NoneChoice => Json::string("none")
    Required => Json::string("required")
    Function(name) =>
      Json::object({
        "type": Json::string("function"),
        "function": Json::object({ "name": Json::string(name) }),
      })
  }
}

///|
/// The requested response format.
pub(all) enum ResponseFormat {
  /// Plain text (the default).
  TextFormat
  /// A JSON object ("JSON mode").
  JsonObject
  /// A JSON object conforming to the given JSON Schema.
  JsonSchema(name~ : String, schema~ : Json)
} derive(Eq, Debug)

///|
pub impl ToJson for ResponseFormat with fn to_json(self : ResponseFormat) -> Json {
  match self {
    TextFormat => Json::object({ "type": Json::string("text") })
    JsonObject => Json::object({ "type": Json::string("json_object") })
    JsonSchema(name~, schema~) =>
      Json::object({
        "type": Json::string("json_schema"),
        "json_schema": Json::object({
          "name": Json::string(name),
          "schema": schema,
        }),
      })
  }
}

///|
/// Create a request for `model` with the given `messages`.
pub fn ChatRequest::new(
  model : String,
  messages : Array[Message],
) -> ChatRequest {
  {
    model,
    messages,
    temperature: None,
    max_tokens: None,
    top_p: None,
    stop: None,
    tools: None,
    tool_choice: None,
    frequency_penalty: None,
    presence_penalty: None,
    seed: None,
    n: None,
    response_format: None,
    logit_bias: None,
    user: None,
    stream: false,
  }
}

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

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

///|
pub fn ChatRequest::top_p(self : ChatRequest, p : Double) -> ChatRequest {
  self.top_p = Some(p)
  self
}

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

///|
pub fn ChatRequest::tools(self : ChatRequest, t : Array[Tool]) -> ChatRequest {
  self.tools = Some(t)
  self
}

///|
pub fn ChatRequest::tool_choice(
  self : ChatRequest,
  c : ToolChoice,
) -> ChatRequest {
  self.tool_choice = Some(c)
  self
}

///|
pub fn ChatRequest::frequency_penalty(
  self : ChatRequest,
  p : Double,
) -> ChatRequest {
  self.frequency_penalty = Some(p)
  self
}

///|
pub fn ChatRequest::presence_penalty(
  self : ChatRequest,
  p : Double,
) -> ChatRequest {
  self.presence_penalty = Some(p)
  self
}

///|
pub fn ChatRequest::seed(self : ChatRequest, s : Int) -> ChatRequest {
  self.seed = Some(s)
  self
}

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

///|
pub fn ChatRequest::response_format(
  self : ChatRequest,
  f : ResponseFormat,
) -> ChatRequest {
  self.response_format = Some(f)
  self
}

///|
/// Shortcut: request a JSON-object response ("JSON mode").
pub fn ChatRequest::json_mode(self : ChatRequest) -> ChatRequest {
  self.response_format = Some(JsonObject)
  self
}

///|
pub fn ChatRequest::logit_bias(
  self : ChatRequest,
  bias : Map[String, Double],
) -> ChatRequest {
  self.logit_bias = Some(bias)
  self
}

///|
pub fn ChatRequest::user(self : ChatRequest, u : String) -> ChatRequest {
  self.user = Some(u)
  self
}

///|
/// Validate the request parameters, returning an error message if any value
/// is out of its documented range. Returns `None` when the request is valid.
///
/// This is a best-effort local check against the OpenAI-documented ranges;
/// it does not guarantee the server will accept the request.
pub fn ChatRequest::validate(self : ChatRequest) -> String? {
  if self.model == "" {
    return Some("model must not be empty")
  }
  if self.messages.length() == 0 {
    return Some("messages must not be empty")
  }
  if self.temperature is Some(t) && (t < 0.0 || t > 2.0) {
    return Some("temperature must be in [0, 2]")
  }
  if self.top_p is Some(p) && (p < 0.0 || p > 1.0) {
    return Some("top_p must be in [0, 1]")
  }
  if self.frequency_penalty is Some(p) && (p < -2.0 || p > 2.0) {
    return Some("frequency_penalty must be in [-2, 2]")
  }
  if self.presence_penalty is Some(p) && (p < -2.0 || p > 2.0) {
    return Some("presence_penalty must be in [-2, 2]")
  }
  if self.max_tokens is Some(m) && m <= 0 {
    return Some("max_tokens must be positive")
  }
  if self.n is Some(count) && count <= 0 {
    return Some("n must be positive")
  }
  None
}

///|
pub impl ToJson for ChatRequest with fn to_json(self : ChatRequest) -> Json {
  let obj : Map[String, Json] = {
    "model": Json::string(self.model),
    "messages": self.messages.to_json(),
    "stream": Json::boolean(self.stream),
  }
  if self.temperature is Some(t) {
    obj["temperature"] = Json::number(t)
  }
  if self.max_tokens is Some(n) {
    obj["max_tokens"] = Json::number(n.to_double())
  }
  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.tools is Some(t) {
    obj["tools"] = t.to_json()
  }
  if self.tool_choice is Some(c) {
    obj["tool_choice"] = c.to_json()
  }
  if self.frequency_penalty is Some(p) {
    obj["frequency_penalty"] = Json::number(p)
  }
  if self.presence_penalty is Some(p) {
    obj["presence_penalty"] = Json::number(p)
  }
  if self.seed is Some(s) {
    obj["seed"] = Json::number(s.to_double())
  }
  if self.n is Some(count) {
    obj["n"] = Json::number(count.to_double())
  }
  if self.response_format is Some(f) {
    obj["response_format"] = f.to_json()
  }
  if self.logit_bias is Some(bias) {
    let bias_obj : Map[String, Json] = {}
    for k, v in bias {
      bias_obj[k] = Json::number(v)
    }
    obj["logit_bias"] = Json::object(bias_obj)
  }
  if self.user is Some(u) {
    obj["user"] = Json::string(u)
  }
  Json::object(obj)
}

///|
/// Convenience constructors for messages.
pub fn Message::system(text : String) -> Message {
  {
    role: System,
    content: Str(text),
    tool_calls: None,
    tool_call_id: None,
    name: None,
  }
}

///|
pub fn Message::user(text : String) -> Message {
  {
    role: User,
    content: Str(text),
    tool_calls: None,
    tool_call_id: None,
    name: None,
  }
}

///|
pub fn Message::assistant(text : String) -> Message {
  {
    role: Assistant,
    content: Str(text),
    tool_calls: None,
    tool_call_id: None,
    name: None,
  }
}

///|
/// Build a multimodal user message from content parts (text and/or images).
pub fn Message::user_parts(parts : Array[ContentPart]) -> Message {
  {
    role: User,
    content: Parts(parts),
    tool_calls: None,
    tool_call_id: None,
    name: None,
  }
}

///|
/// Build a `tool` role message carrying the result of a tool call.
pub fn Message::tool_result(tool_call_id : String, content : String) -> Message {
  {
    role: Tool,
    content: Str(content),
    tool_calls: None,
    tool_call_id: Some(tool_call_id),
    name: None,
  }
}

///|
/// A text content part.
pub fn ContentPart::text(s : String) -> ContentPart {
  Text(s)
}

///|
/// An image content part from a URL (`https://...` or a `data:` URI).
pub fn ContentPart::image_url(url : String) -> ContentPart {
  ImageUrl(url)
}

///|
/// An image content part from raw base64 data, wrapped as a `data:` URI.
///
/// `mime` is e.g. `"image/png"` or `"image/jpeg"`.
pub fn ContentPart::image_base64(mime : String, data : String) -> ContentPart {
  ImageUrl("data:" + mime + ";base64," + data)
}