///|
/// Each role is its own generated message type. A system message accepts only
/// text, so a non-text block in one is sent as it is, and the API rejects it.
fn input_message(message : Message) -> @gen.InputMessageUnion {
  let content = match message.content {
    Text(text) => @gen.InputMessageContent::Text(text)
    Blocks(blocks) => @gen.InputMessageContent::InputContentBlockArray(blocks)
  }
  match message.role {
    User =>
      @gen.InputMessageUnion::User(
        @gen.UserInputMessage::new(role="user", content~),
      )
    Assistant =>
      @gen.InputMessageUnion::Assistant(
        @gen.AssistantInputMessage::new(role="assistant", content~),
      )
    System => {
      let content = match message.content {
        Text(text) => @gen.SystemInputMessageContent::Text(text)
        Blocks(blocks) =>
          @gen.SystemInputMessageContent::ValueArray(
            blocks.map(block => {
              match block {
                Text(text) =>
                  @gen.SystemInputMessageContentValueArrayItem::Text(text)
                other => {
                  let json = other.to_json()
                  @gen.SystemInputMessageContentValueArrayItem::Unknown(
                    block_type(json),
                    json,
                  )
                }
              }
            }),
          )
      }
      @gen.InputMessageUnion::System(
        @gen.SystemInputMessage::new(role="system", 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)),
    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"
    | "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 ValidationError {
  for key, _ in extra {
    if is_message_key(key) {
      raise ValidationError(
        message="extra parameter collides with known key: " + key,
      )
    }
  }
  let body = ToJson::to_json(params)
  guard body is Object(fields) else {
    raise ValidationError(message="generated " + what + " must be an object")
  }
  for key, value in extra {
    fields[key] = value
  }
  let validated : T = @json.from_json(body) catch {
    error =>
      raise ValidationError(
        message=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 ValidationError {
  merge_extra(request.to_params(stream~), request.extra, "message request")
}

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