///|
fn is_response_key(key : String) -> Bool {
  match key {
    "model"
    | "input"
    | "instructions"
    | "max_output_tokens"
    | "temperature"
    | "previous_response_id"
    | "tools"
    | "tool_choice"
    | "stream" => true
    _ => false
  }
}

///|
fn is_chat_key(key : String) -> Bool {
  match key {
    "model"
    | "messages"
    | "max_tokens"
    | "temperature"
    | "reasoning_effort"
    | "tools"
    | "tool_choice"
    | "stream"
    | "stream_options" => true
    _ => false
  }
}

///|
fn chat_message_json(message : ChatMessage) -> Json raise @runtime.SdkError {
  let fields : Map[String, Json] = { "role": message.role }
  if !(message.role == "assistant" &&
    message.tool_calls is Some(_) &&
    message.content.is_empty()) {
    fields["content"] = message.content.to_json()
  }
  if message.name is Some(name) {
    fields["name"] = name.to_json()
  }
  match message.role {
    "system" | "user" =>
      if message.tool_calls is Some(_) || message.tool_call_id is Some(_) {
        raise @runtime.Config(
          message.role + " chat message cannot contain tool call fields",
        )
      }
    "assistant" => {
      if message.tool_call_id is Some(_) {
        raise @runtime.Config(
          "assistant chat message cannot contain tool_call_id",
        )
      }
      if message.tool_calls is Some(calls) {
        fields["tool_calls"] = calls
          .map(call => {
            @gen.ChatCompletionMessageToolCall::new(
              id=call.id,
              function=@gen.ChatCompletionMessageToolCallFunction::new(
                name=call.name,
                arguments=call.arguments,
              ),
            ).to_json()
          })
          .to_json()
      }
    }
    "tool" => {
      if message.tool_calls is Some(_) {
        raise @runtime.Config("tool chat message cannot contain tool_calls")
      }
      match message.tool_call_id {
        Some(tool_call_id) => fields["tool_call_id"] = tool_call_id.to_json()
        None => raise @runtime.Config("tool chat message requires tool_call_id")
      }
    }
    role => raise @runtime.Config("unsupported chat message role: " + role)
  }
  Json::object(fields)
}

///|
fn chat_tool_json(tool : ToolDef) -> Json {
  @gen.ChatCompletionTool::new(
    function=@gen.FunctionObject::new(
      name=tool.name,
      parameters=tool.parameters,
      description?=tool.description,
      strict?=tool.strict,
    ),
  ).to_json()
}

///|
fn response_tool(tool : ToolDef) -> @gen.Tool {
  @gen.Function(
    @gen.FunctionTool::new(
      type_="function",
      name=tool.name,
      parameters=tool.parameters,
      description?=tool.description,
      strict?=tool.strict,
    ),
  )
}

///|
fn chat_parts(
  request : ChatRequest,
  stream : Bool,
) -> (@gen.CreateChatCompletionRequest, Json) raise @runtime.SdkError {
  for key, _ in request.extra {
    if is_chat_key(key) {
      raise @runtime.Config("extra parameter collides with known key: " + key)
    }
  }
  let fields : Map[String, Json] = {
    "model": request.model,
    "messages": Json::array(request.messages.map(chat_message_json)),
  }
  if request.max_tokens is Some(value) {
    fields["max_tokens"] = value.to_json()
  }
  if request.temperature is Some(value) {
    fields["temperature"] = value.to_json()
  }
  if request.reasoning_effort is Some(value) {
    fields["reasoning_effort"] = value.to_json()
  }
  if request.tools is Some(tools) {
    fields["tools"] = tools.map(chat_tool_json).to_json()
  }
  if request.tool_choice is Some(tool_choice) {
    fields["tool_choice"] = tool_choice
  }
  if stream {
    fields["stream"] = true.to_json()
    fields["stream_options"] = { "include_usage": true }
  }
  let raw = Json::object(fields)
  let generated : @gen.CreateChatCompletionRequest = @json.from_json(raw) catch {
    error =>
      raise @runtime.Config(
        "chat request does not match the generated schema: " + error.to_string(),
      )
  }
  let body = generated.to_json()
  guard body is Object(generated_fields) else {
    raise @runtime.Config("generated chat request must be an object")
  }
  // The upstream generated tool-message type omits the optional `name` field.
  // Restore it after generated validation and encoding until the schema gains it.
  guard generated_fields.get("messages") is Some(Array(messages)) else {
    raise @runtime.Config("generated chat messages must be an array")
  }
  for index, message in request.messages {
    if message.role == "tool" && message.name is Some(name) {
      guard messages.get(index) is Some(Object(fields)) else {
        raise @runtime.Config("generated chat message must be an object")
      }
      fields["name"] = name.to_json()
    }
  }
  for key, value in request.extra {
    generated_fields[key] = value
  }
  (generated, body)
}

///|
fn response_parts(
  request : ResponseRequest,
  stream : Bool,
) -> (@gen.CreateResponse, Json) raise @runtime.SdkError {
  for key, _ in request.extra {
    if is_response_key(key) {
      raise @runtime.Config("extra parameter collides with known key: " + key)
    }
  }
  let input = if request.tool_outputs.is_empty() {
    @gen.CreateResponseInput::Text(request.input)
  } else {
    let items : Array[@gen.InputItem] = [
      @gen.EasyInputMessage(
        @gen.EasyInputMessage::new(
          role=@gen.EasyInputMessageRole::User,
          content=@gen.EasyInputMessageContent::Text(request.input),
        ),
      ),
    ]
    for pair in request.tool_outputs {
      let (call_id, output) = pair
      items.push(
        @gen.FunctionCallOutput(
          @gen.FunctionCallOutputItemParam::new(
            type_="function_call_output",
            call_id~,
            output~,
          ),
        ),
      )
    }
    @gen.CreateResponseInput::InputItemArray(items)
  }
  let generated = @gen.CreateResponse::new(
    model=request.model,
    input~,
    temperature?=request.temperature,
    previous_response_id?=request.previous_response_id,
    max_output_tokens?=request.max_output_tokens,
    instructions?=request.instructions,
    tools?=request.tools.map(tools => tools.map(response_tool)),
    tool_choice?=request.tool_choice,
    stream?=if stream { Some(true) } else { None },
  )
  let body = generated.to_json()
  guard body is Object(fields) else {
    raise @runtime.Config("generated response request must be an object")
  }
  for key, value in request.extra {
    fields[key] = value
  }
  (generated, body)
}