///|
/// Adapter for Google's Gemini `generateContent` API.
///
/// Gemini's native request/response shape differs from OpenAI's:
///
/// - Messages are `contents`, each `{role, parts:[{text}|{inline_data}]}`.
/// - Roles are `user` and `model` (not `assistant`); there is no `system`
///   role — the system prompt goes in a top-level `systemInstruction`.
/// - The response is `candidates[].content.parts[].text`, with token usage in
///   `usageMetadata`.
///
/// This module converts a common `ChatRequest` into a Gemini body and parses a
/// Gemini response back into the common `ChatResponse`.

///|
/// Build a Gemini `generateContent` request body from a `ChatRequest`.
pub fn gemini_request_body(request : ChatRequest) -> Json {
  let contents = []
  let system = StringBuilder::new()
  for msg in request.messages {
    match msg.role {
      System =>
        match msg.content {
          Str(s) => {
            if system.to_string().length() > 0 {
              system.write_string("\n\n")
            }
            system.write_string(s)
          }
          Parts(_) => ()
        }
      _ =>
        contents.push(
          Json::object({
            "role": Json::string(gemini_role(msg.role)),
            "parts": gemini_parts(msg.content),
          }),
        )
    }
  }
  let obj : Map[String, Json] = { "contents": Json::array(contents) }
  let sys = system.to_string()
  if sys.length() > 0 {
    obj["systemInstruction"] = Json::object({
      "parts": Json::array([Json::object({ "text": Json::string(sys) })]),
    })
  }
  // Generation config maps the sampling params.
  let gen_config : Map[String, Json] = {}
  if request.temperature is Some(t) {
    gen_config["temperature"] = Json::number(t)
  }
  if request.top_p is Some(p) {
    gen_config["topP"] = Json::number(p)
  }
  if request.max_tokens is Some(m) {
    gen_config["maxOutputTokens"] = Json::number(m.to_double())
  }
  if request.stop is Some(s) {
    gen_config["stopSequences"] = s.to_json()
  }
  if gen_config.length() > 0 {
    obj["generationConfig"] = Json::object(gen_config)
  }
  Json::object(obj)
}

///|
/// Map a common role to a Gemini role (`user` or `model`).
fn gemini_role(role : Role) -> String {
  match role {
    Assistant => "model"
    _ => "user"
  }
}

///|
/// Convert message content into Gemini `parts`.
fn gemini_parts(content : Content) -> Json {
  match content {
    Str(s) => Json::array([Json::object({ "text": Json::string(s) })])
    Parts(parts) => {
      let arr = []
      for p in parts {
        match p {
          Text(t) => arr.push(Json::object({ "text": Json::string(t) }))
          ImageUrl(u) =>
            // Gemini uses fileData for URLs (best-effort mime).
            arr.push(
              Json::object({
                "fileData": Json::object({
                  "mimeType": Json::string("image/*"),
                  "fileUri": Json::string(u),
                }),
              }),
            )
        }
      }
      Json::array(arr)
    }
  }
}

///|
/// Parse a Gemini `generateContent` response into the common `ChatResponse`.
pub fn parse_gemini_response(json : Json) -> ChatResponse raise LLMError {
  guard json is Object(obj) else {
    raise Decode("gemini response: expected object")
  }
  let text = StringBuilder::new()
  let mut finish_reason : String? = None
  match obj.get("candidates") {
    Some(Array(cands)) =>
      if cands.get(0) is Some(Object(cand)) {
        match cand.get("content") {
          Some(Object(content)) =>
            match content.get("parts") {
              Some(Array(parts)) =>
                for part in parts {
                  match part {
                    Object(p) =>
                      match p.get("text") {
                        Some(String(t)) => text.write_string(t)
                        _ => ()
                      }
                    _ => ()
                  }
                }
              _ => ()
            }
          _ => ()
        }
        match cand.get("finishReason") {
          Some(String(r)) => finish_reason = Some(gemini_finish_reason(r))
          _ => ()
        }
      }
    _ => ()
  }
  let usage = match obj.get("usageMetadata") {
    Some(Object(u)) => {
      let prompt = match u.get("promptTokenCount") {
        Some(Number(n, ..)) => n.to_int()
        _ => 0
      }
      let completion = match u.get("candidatesTokenCount") {
        Some(Number(n, ..)) => n.to_int()
        _ => 0
      }
      let total = match u.get("totalTokenCount") {
        Some(Number(n, ..)) => n.to_int()
        _ => prompt + completion
      }
      Some(Usage::{
        prompt_tokens: prompt,
        completion_tokens: completion,
        total_tokens: total,
      })
    }
    _ => None
  }
  let message = Message::assistant(text.to_string())
  {
    id: "",
    object: "chat.completion",
    created: 0L,
    model: "",
    choices: [{ index: 0, message, finish_reason }],
    usage,
    system_fingerprint: None,
  }
}

///|
/// Map a Gemini `finishReason` to the OpenAI-style `finish_reason`.
fn gemini_finish_reason(reason : String) -> String {
  match reason {
    "STOP" => "stop"
    "MAX_TOKENS" => "length"
    "SAFETY" => "content_filter"
    "RECITATION" => "content_filter"
    other => other
  }
}