///|
/// content_encoding.mbt — Shared chat-completions content-block encoding for
/// the delegating adapters (openai-compatible, deepseek, kimi, zai,
/// openrouter).
///
/// Chat-completions `content` accepts either a plain string or a typed parts
/// array. Some compatible servers reject arrays, so the string form stays the
/// default; the parts form is used only when image blocks are present AND the
/// selected model accepts image input. An image that cannot be sent is never
/// dropped silently: it downgrades to an explicit placeholder so the model
/// knows an image exists that it cannot see.

///|
/// Result of encoding one message's content blocks.
pub(all) enum EncodedChatContent {
  /// Plain string form: no image blocks were present, or every image was
  /// downgraded to a placeholder appended to the text.
  TextContent(String)
  /// Typed parts form for image-capable models: one `text` part per Text
  /// block, one `image_url` part per Image block.
  PartsContent(Array[Json])
}

///|
/// Wire form of an Image block's payload. Kernel `Content::Image` data is
/// opaque: adapters may receive base64 or an http(s) URL; both pass through
/// as `image_url.url` in the shape chat-completions endpoints expect.
fn image_url_for(media_type : String, data : String) -> String {
  if data.has_prefix("https://") || data.has_prefix("http://") {
    data
  } else {
    "data:" + media_type + ";base64," + data
  }
}

///|
/// Placeholder for an image the endpoint cannot accept. The size is the
/// decoded-byte estimate (base64 length × 3/4) so the model can gauge how
/// much was attached without seeing it.
pub fn image_placeholder(media_type : String, data : String) -> String {
  let approx_kb = data.length() * 3 / 4096
  "[Attached " +
  media_type +
  ", ~" +
  approx_kb.to_string() +
  " KB, omitted: this model does not accept image input]"
}

///|
/// Encode content blocks for a chat-completions message `content` field.
///
/// - No image blocks → `TextContent` (concatenated text).
/// - Image blocks with `supports_images=true` → `PartsContent`.
/// - Image blocks with `supports_images=false` → `TextContent` with each
///   image downgraded to an explicit placeholder (never a silent drop).
pub fn encode_chat_message_content(
  blocks : Array[@posoco.Content],
  supports_images? : Bool = false,
) -> EncodedChatContent {
  let has_image = blocks.exists(fn(b) { b is @posoco.Content::Image(..) })
  if !has_image {
    let builder = StringBuilder()
    for block in blocks {
      match block {
        @posoco.Content::Text(s) => builder.write_string(s)
        @posoco.Content::Image(..) => ()
      }
    }
    return TextContent(builder.to_string())
  }
  if supports_images {
    let parts : Array[Json] = []
    for block in blocks {
      match block {
        @posoco.Content::Text(s) =>
          parts.push(
            Json::object(
              Map::from_array([
                ("type", Json::string("text")),
                ("text", Json::string(s)),
              ]),
            ),
          )
        @posoco.Content::Image(media_type~, data~) =>
          parts.push(
            Json::object(
              Map::from_array([
                ("type", Json::string("image_url")),
                (
                  "image_url",
                  Json::object(
                    Map::from_array([
                      ("url", Json::string(image_url_for(media_type, data))),
                    ]),
                  ),
                ),
              ]),
            ),
          )
      }
    }
    return PartsContent(parts)
  }
  let builder = StringBuilder()
  let mut wrote = false
  for block in blocks {
    match block {
      @posoco.Content::Text(s) => {
        builder.write_string(s)
        wrote = true
      }
      @posoco.Content::Image(media_type~, data~) => {
        if wrote {
          builder.write_char('\n')
        }
        builder.write_string(image_placeholder(media_type, data))
        wrote = true
      }
    }
  }
  TextContent(builder.to_string())
}