///|
/// One HTTP request a dialect produced from a `Request` — verb, path, headers, body.
/// A client only has to send it; nothing in it is provider-specific beyond what the
/// dialect already decided.
#derive(Eq, Debug)
pub(all) struct Call {
  verb : String
  path : String
  headers : Array[(String, String)]
  body : Json
}

///|
/// A call with the provider's JSON content type and no extra header.
pub fn Call::new(verb : String, path : String, body : Json) -> Call {
  { verb, path, headers: [("content-type", "application/json")], body, }
}

///|
/// A copy with a header set, replacing any previous value of the same name.
pub fn Call::header(self : Call, name : String, value : String) -> Call {
  let headers = self.headers.filter(pair => pair.0 != name)
  { ..self, headers: [..headers, (name, value)], }
}

///|
/// One frame of a streamed response: the server-sent-events `event` name when the
/// endpoint uses named events, and the payload. JSON endpoints that stream newline
/// delimited objects leave `event` empty.
#derive(Eq, Debug)
pub(all) struct Frame {
  event : String?
  data : String
}

///|
/// A frame with no event name.
pub fn Frame::new(data : String) -> Frame {
  { event: None, data, }
}

///|
/// A named frame.
pub fn Frame::named(event : String, data : String) -> Frame {
  { event: Some(event), data, }
}

///|
/// The frame's payload as JSON, or `Decode` when it is not JSON.
pub fn Frame::json(self : Frame) -> Json raise LlmError {
  @json.parse(self.data) catch {
    _ => raise Decode("frame is not JSON: " + self.data)
  }
}

///|
/// State carried across the frames of one streamed answer. Vendors number content
/// blocks differently — OpenAI by tool-call index, Anthropic by block ordinal, Gemini
/// by part ordinal — so the dialect keeps the counters here instead of in the client,
/// and the same `Turn` serves the client reading a stream and the server writing one.
pub struct Turn {
  /// How many frames have been consumed or produced.
  mut seq : Int32
  /// The ordinal of the next content block.
  mut block : Int32
  /// Per-vendor scratch: a tool-call index to its id, a block ordinal to its kind.
  slots : Map[String, String]
  /// Numeric scratch: a running count, a tool-call index.
  nums : Map[String, Int32]
}

///|
/// A fresh stream state.
pub fn Turn::new() -> Turn {
  { seq: 0, block: 0, slots: Map([]), nums: Map([]), }
}

///|
/// Count one frame and return its ordinal.
pub fn Turn::tick(self : Turn) -> Int32 {
  let current = self.seq
  self.seq = self.seq + 1
  current
}

///|
/// Take the next content-block ordinal.
pub fn Turn::next_block(self : Turn) -> Int32 {
  let current = self.block
  self.block = self.block + 1
  current
}

///|
/// A scratch value, or the empty string.
pub fn Turn::slot(self : Turn, key : String) -> String {
  self.slots.get(key).unwrap_or("")
}

///|
/// Record a scratch value.
pub fn Turn::set_slot(self : Turn, key : String, value : String) -> Unit {
  self.slots[key] = value
}

///|
/// Append to a scratch value.
pub fn Turn::add_slot(self : Turn, key : String, chunk : String) -> Unit {
  self.slots[key] = self.slots.get(key).unwrap_or("") + chunk
}

///|
/// Whether a scratch value was recorded.
pub fn Turn::has_slot(self : Turn, key : String) -> Bool {
  self.slots.contains(key)
}

///|
/// A numeric scratch value, or zero.
pub fn Turn::num(self : Turn, key : String) -> Int32 {
  self.nums.get(key).unwrap_or(0)
}

///|
/// Set a numeric scratch value.
pub fn Turn::set_num(self : Turn, key : String, value : Int32) -> Unit {
  self.nums[key] = value
}

///|
/// Add to a numeric scratch value and return the new total.
pub fn Turn::add_num(self : Turn, key : String, delta : Int32) -> Int32 {
  let value = self.num(key) + delta
  self.nums[key] = value
  value
}

///|
/// The kinds of endpoint a dialect can both call and serve.
#derive(Eq, Debug)
pub(all) enum RouteKind {
  Chat
  Complete
  Responses
  Embed
  Models
  Count
  Show
  Version
  Generate
}

///|
/// One endpoint a dialect serves. `path` may contain a single `*`, which matches any
/// run of characters — Gemini puts the model inside the path.
#derive(Eq, Debug)
pub(all) struct Route {
  kind : RouteKind
  verb : String
  path : String
}

///|
/// A route for a verb and a path pattern.
pub fn Route::new(kind : RouteKind, verb : String, path : String) -> Route {
  { kind, verb, path, }
}

///|
/// Whether this route answers `verb` at `path`.
pub fn Route::matches(self : Route, verb : String, path : String) -> Bool {
  if self.verb != verb {
    return false
  }
  match self.path.find("*") {
    None => self.path == path
    Some(at) => {
      let prefix = self.path.exact_view(start=0, end=at)
      let suffix = self.path.exact_view(start=at + 1)
      path.length() >= prefix.length() + suffix.length() &&
      path.has_prefix(prefix) &&
      path.has_suffix(suffix)
    }
  }
}

///|
/// What a server-side request asked for, decoded into the shared vocabulary.
#derive(Eq, Debug)
pub(all) enum Asked {
  Chat(Request)
  Embed(model~ : String, inputs~ : Array[String])
  Models
  Count(Request)
}

///|
/// One vendor's wire format, read and written in the same place. The client half turns
/// a `Request` into a `Call` and a body into a `Reply`; the server half turns a body
/// into a `Request` and a `Reply` into a body. Because both halves live here, a gateway
/// that speaks a format out cannot drift from the client that speaks it in.
pub(open) trait Dialect {
  /// The format's name, for diagnostics and for telling two formats apart when they share a
  /// path.
  fn name(Self) -> String
  /// The HTTP call that asks for this request.
  fn request(Self, Request) -> Call raise LlmError
  /// The reply a response body describes.
  fn reply(Self, status : Int32, body : Json) -> Reply raise LlmError
  /// The deltas one streamed frame carries.
  fn delta(Self, Frame, Turn) -> Array[Delta] raise LlmError
  /// The endpoints this dialect serves.
  fn routes(Self) -> Array[Route]
  /// The request a served body describes.
  fn parse(Self, Route, Json) -> Asked raise LlmError
  /// The body that answers a reply.
  fn answer(Self, Reply) -> Json
  /// The body that answers an embeddings request. Kept apart from `answer` because the
  /// five endpoints do not share a response shape.
  fn answer_embed(Self, String, Array[Array[Double]]) -> Json
  /// The body that answers a token-count request.
  fn answer_count(Self, Reply) -> Json
  /// The body that answers a model list.
  fn answer_models(Self, Array[ModelInfo]) -> Json
  /// The body that answers a reply at a particular endpoint. Most formats answer every
  /// endpoint the same way; the OpenAI one has three shapes.
  fn answer_at(Self, RouteKind, Reply) -> Json
  /// The frames that carry one delta out.
  fn frame(Self, Delta, Turn) -> Array[Frame]
  /// The status and body for an error.
  fn error(Self, LlmError) -> (Int32, Json)
}

///|
pub extend Call with Eq::{not_equal, equal}

///|
pub extend Call with Debug::{to_repr}

///|
pub extend Frame with Eq::{not_equal, equal}

///|
pub extend Frame with Debug::{to_repr}

///|
pub extend RouteKind with Eq::{not_equal, equal}

///|
pub extend RouteKind with Debug::{to_repr}

///|
pub extend Route with Eq::{not_equal, equal}

///|
pub extend Route with Debug::{to_repr}

///|
pub extend Asked with Eq::{not_equal, equal}

///|
pub extend Asked with Debug::{to_repr}