///|
/// The input to an embeddings request: one or more strings to embed.
pub(all) enum EmbeddingInput {
  /// A single text to embed.
  Single(String)
  /// A batch of texts to embed in one request.
  Batch(Array[String])
} derive(Eq, Debug)

///|
pub impl ToJson for EmbeddingInput with fn to_json(self : EmbeddingInput) -> Json {
  match self {
    Single(s) => Json::string(s)
    Batch(arr) => arr.to_json()
  }
}

///|
/// A request to the `/embeddings` endpoint.
pub(all) struct EmbeddingRequest {
  model : String
  input : EmbeddingInput
  /// Optional number of dimensions to truncate the embedding to
  /// (supported by newer models).
  mut dimensions : Int?
  /// Optional end-user identifier for abuse monitoring.
  mut user : String?
  /// The encoding format: `"float"` (default) or `"base64"`.
  mut encoding_format : String?
}

///|
/// Create an embeddings request for a single text.
pub fn EmbeddingRequest::new(
  model : String,
  input : EmbeddingInput,
) -> EmbeddingRequest {
  { model, input, dimensions: None, user: None, encoding_format: None }
}

///|
/// Convenience: build a request embedding a single string.
pub fn EmbeddingRequest::of_text(
  model : String,
  text : String,
) -> EmbeddingRequest {
  EmbeddingRequest::new(model, Single(text))
}

///|
/// Convenience: build a request embedding a batch of strings.
pub fn EmbeddingRequest::of_batch(
  model : String,
  texts : Array[String],
) -> EmbeddingRequest {
  EmbeddingRequest::new(model, Batch(texts))
}

///|
/// Set the target dimensionality of the returned embeddings.
pub fn EmbeddingRequest::dimensions(
  self : EmbeddingRequest,
  n : Int,
) -> EmbeddingRequest {
  self.dimensions = Some(n)
  self
}

///|
/// Set the end-user identifier.
pub fn EmbeddingRequest::user(
  self : EmbeddingRequest,
  u : String,
) -> EmbeddingRequest {
  self.user = Some(u)
  self
}

///|
pub impl ToJson for EmbeddingRequest with fn to_json(self : EmbeddingRequest) -> Json {
  let obj : Map[String, Json] = {
    "model": Json::string(self.model),
    "input": self.input.to_json(),
  }
  if self.dimensions is Some(d) {
    obj["dimensions"] = Json::number(d.to_double())
  }
  if self.user is Some(u) {
    obj["user"] = Json::string(u)
  }
  if self.encoding_format is Some(f) {
    obj["encoding_format"] = Json::string(f)
  }
  Json::object(obj)
}

///|
/// A single embedding vector in an embeddings response.
pub(all) struct Embedding {
  index : Int
  embedding : Array[Double]
} derive(Debug)

///|
pub impl @json.FromJson for Embedding with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> Embedding {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError((path, "Embedding: expected object"))
  }
  let index = match obj.get("index") {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
  }
  let embedding = match obj.get("embedding") {
    Some(Array(arr)) => {
      let out = []
      for v in arr {
        match v {
          Number(n, ..) => out.push(n)
          _ => ()
        }
      }
      out
    }
    _ => []
  }
  { index, embedding }
}

///|
/// The response from the `/embeddings` endpoint.
pub(all) struct EmbeddingResponse {
  model : String
  data : Array[Embedding]
  usage : Usage?
} derive(Debug)

///|
pub impl @json.FromJson for EmbeddingResponse with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> EmbeddingResponse {
  guard json is Object(obj) else {
    raise @json.JsonDecodeError((path, "EmbeddingResponse: expected object"))
  }
  let model = match obj.get("model") {
    Some(String(s)) => s
    _ => ""
  }
  let data = match obj.get("data") {
    Some(Array(_) as d) => @json.from_json(d)
    _ => []
  }
  let usage = match obj.get("usage") {
    Some(Object(_) as u) => Some(@json.from_json(u))
    _ => None
  }
  { model, data, usage }
}

///|
/// The first embedding vector, if any.
pub fn EmbeddingResponse::vector(self : EmbeddingResponse) -> Array[Double] {
  match self.data.get(0) {
    Some(e) => e.embedding
    None => []
  }
}

///|
/// Perform an embeddings request.
pub async fn Client::embeddings(
  self : Client,
  request : EmbeddingRequest,
) -> EmbeddingResponse raise LLMError {
  let json = self.post_json("/embeddings", request.to_json())
  @json.from_json(json) catch {
    err => raise Decode(err.to_string())
  }
}

///|
/// Compute the cosine similarity between two equal-length vectors.
///
/// Returns 0.0 if either vector is empty or their lengths differ.
pub fn cosine_similarity(a : Array[Double], b : Array[Double]) -> Double {
  if a.length() != b.length() || a.length() == 0 {
    return 0.0
  }
  let mut dot = 0.0
  let mut norm_a = 0.0
  let mut norm_b = 0.0
  for i in 0..