///|
fn input_role(role : Role) -> @gen.InputMessageRole {
  match role {
    User => @gen.InputMessageRole::User
    Assistant => @gen.InputMessageRole::Assistant
  }
}

///|
fn input_message(message : Message) -> @gen.InputMessage {
  let content = match message.content {
    Text(text) => @gen.InputMessageContent::Text(text)
    Blocks(blocks) => @gen.InputMessageContent::InputContentBlockArray(blocks)
  }
  @gen.InputMessage::new(role=input_role(message.role), content~)
}

///|
fn generated_tool(tool : ToolDef) -> @gen.Tool {
  @gen.Tool::new(
    name=tool.name,
    input_schema=tool.input_schema,
    description?=tool.description,
    strict?=tool.strict,
  )
}

///|
/// The generated request for this facade request, without `extra`.
/// `stream` selects server-sent events; the facade sets it for `stream_message`.
pub fn MessageRequest::to_params(
  self : MessageRequest,
  stream? : Bool = false,
) -> CreateMessageParams {
  @gen.CreateMessageParams::new(
    model=self.model,
    messages=self.messages.map(input_message),
    max_tokens=self.max_tokens,
    system?=self.system.map(text => @gen.CreateMessageParamsSystem::Text(text)),
    temperature?=self.temperature,
    stop_sequences?=self.stop_sequences,
    tools?=self.tools.map(tools => {
      tools.map(tool => {
        @gen.CreateMessageParamsToolsItem::Tool(generated_tool(tool))
      })
    }),
    tool_choice?=self.tool_choice,
    thinking?=self.thinking,
    output_config?=self.output_config,
    metadata?=self.metadata,
    stream?=if stream { Some(true) } else { None },
  )
}

///|
/// The generated token-counting request for this facade request, without `extra`.
pub fn MessageRequest::to_count_tokens_params(
  self : MessageRequest,
) -> @gen.CountMessageTokensParams {
  @gen.CountMessageTokensParams::new(
    model=self.model,
    messages=self.messages.map(input_message),
    system?=self.system.map(text => {
      @gen.CountMessageTokensParamsSystem::Text(text)
    }),
    tools?=self.tools.map(tools => {
      tools.map(tool => {
        @gen.CountMessageTokensParamsToolsItem::Tool(generated_tool(tool))
      })
    }),
    tool_choice?=self.tool_choice,
    thinking?=self.thinking,
    output_config?=self.output_config,
  )
}

///|
/// Keys the facade owns. Anything else in `extra` is merged into the body and
/// validated against the generated schema.
fn is_message_key(key : String) -> Bool {
  match key {
    "model"
    | "max_tokens"
    | "messages"
    | "system"
    | "temperature"
    | "stop_sequences"
    | "tools"
    | "tool_choice"
    | "thinking"
    | "output_config"
    | "metadata"
    | "stream" => true
    _ => false
  }
}

///|
fn[T : ToJson + @json.FromJson] merge_extra(
  params : T,
  extra : Map[String, Json],
  what : String,
) -> (T, Json) raise @runtime.SdkError {
  for key, _ in extra {
    if is_message_key(key) {
      raise @runtime.Config("extra parameter collides with known key: " + key)
    }
  }
  let body = ToJson::to_json(params)
  guard body is Object(fields) else {
    raise @runtime.Config("generated " + what + " must be an object")
  }
  for key, value in extra {
    fields[key] = value
  }
  let validated : T = @json.from_json(body) catch {
    error =>
      raise @runtime.Config(
        what + " does not match the generated schema: " + error.to_string(),
      )
  }
  (validated, body)
}

///|
/// The generated request plus the JSON body actually sent (typed fields, then `extra`).
fn message_body(
  request : MessageRequest,
  stream : Bool,
) -> (CreateMessageParams, Json) raise @runtime.SdkError {
  merge_extra(request.to_params(stream~), request.extra, "message request")
}

///|
fn count_tokens_body(
  request : MessageRequest,
) -> (@gen.CountMessageTokensParams, Json) raise @runtime.SdkError {
  merge_extra(
    request.to_count_tokens_params(),
    request.extra,
    "token count request",
  )
}