// Helpers shared by the provider dialects: the parts of lifting/lowering
// that are genuinely provider-independent. Anything keyed by provider JSON
// key names stays inside each dialect.

///|
pub impl Show for DialectError with fn output(self, logger) {
  match self {
    MalformedReply(msg) => logger.write_string("malformed reply: \{msg}")
  }
}

///|
/// Provider API errors are data on the message, never exceptions (pi
/// convention).
///
/// Two shapes, because "OpenAI-compatible" is not compatible about this one.
/// The wrapped `{"error": {"message": ...}}` is what the vendors with their
/// own protocol answer with (Anthropic wraps it again in `{"type": "error",
/// ...}`, which the same pattern matches). Cerebras answers with the error
/// object BARE — `{"message": ..., "type": "payment_required_error",
/// "code": ...}`, no envelope — and reading that as a malformed reply told
/// an operator their mapping was broken when their billing was.
///
/// Requiring a top-level STRING `message` beside an error marker is what
/// keeps the bare arm from shadowing a good reply: a success is keyed by
/// `choices`, `content`, `candidates` or `output` on the protocols here, and
/// none of them carries one. `{"message": {...}}` as an object — which some
/// compatible servers do return — does not match either.
pub fn api_error(reply : Json, model~ : String) -> ContextMessage? {
  let message = match reply {
    { "error": { "message": String(message), .. }, .. } => message
    { "message": String(message), "code": _, .. } => message
    { "message": String(message), "type": String(_), .. } => message
    _ => return None
  }
  Some(
    AssistantMsg(
      content=[],
      stop=Errored,
      model~,
      response_id=None,
      usage=None,
      error=Some(message),
    ),
  )
}

///|
/// What an image degrades to where a wire path takes text only — tool-result
/// content, which no provider models as an image block. User turns carry
/// images natively (see each dialect's user-content mapping).
fn image_marker(media_type : String) -> String {
  "[image \{media_type} omitted]"
}

///|
/// Flatten user blocks to one text: text passes through, images degrade to
/// a marker (v1 wire formats are text-first).
pub fn user_text(blocks : Array[UserBlock]) -> String {
  blocks
  .map(block => {
    match block {
      UserText(text) => text
      UserImage(media_type~, ..) => image_marker(media_type)
    }
  })
  .join("\n\n")
}

///|
/// Which model answered, and what the provider called the call.
///
/// The tail of every `lift_message`, and identical in all of them: all four
/// providers name the model in `model` and the call in `id`, at the top level
/// of the reply. `fallback` is what the dialect was configured with, for a
/// provider that echoes neither.
///
/// This is here and the `usage` block above it is not, because usage is keyed
/// by provider JSON key names — `input_tokens` against `prompt_tokens` — and
/// this is not keyed by anything. That is the line the header draws.
pub fn reply_identity(reply : Json, fallback~ : String) -> (String, String?) {
  let model = match reply {
    { "model": String(m), .. } => m
    _ => fallback
  }
  let response_id = match reply {
    { "id": String(id), .. } => Some(id)
    _ => None
  }
  (model, response_id)
}