// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The attributes a measurement carries, kept as a set rather than a list.
///
/// A series is identified by its attribute set, so two writes that name the same
/// facts in a different order must land in one series. Keys are sorted, a key
/// written twice keeps the last value, and the identity string is length-prefixed
/// so a key containing a separator cannot make two different sets collide.
///
/// Dots in a key are ordinary characters: `gen_ai.request.model` travels
/// unchanged, as the semantic conventions spell it. Nothing here rewrites a name
/// into the underscores an exposition format would need.
#derive(Eq, Debug)
pub(all) struct Attributes {
  entries : Array[(String, @trace.Value)]
}

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

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

///|
/// The empty attribute set: the series with no dimensions.
pub let empty_attributes : Attributes = { entries: [], }

///|
/// An attribute set built from these entries.
pub fn Attributes::new(
  entries~ : Array[(String, @trace.Value)] = [],
) -> Attributes {
  normalize(entries)
}

///|
/// The same set with `key` set to `value`, replacing an earlier value under that
/// key. Returns a new value, so a set a caller shares with several instruments
/// cannot change underneath one of them.
pub fn Attributes::with_attribute(
  self : Attributes,
  key : String,
  value : @trace.Value,
) -> Attributes {
  let entries = self.entries.copy()
  entries.push((key, value))
  normalize(entries)
}

///|
/// The value under `key`.
pub fn Attributes::get(self : Attributes, key : String) -> @trace.Value? {
  for entry in self.entries {
    if entry.0 == key {
      return Some(entry.1)
    }
  }
  None
}

///|
/// The entries, sorted by key.
pub fn Attributes::to_array(self : Attributes) -> Array[(String, @trace.Value)] {
  self.entries.copy()
}

///|
/// How many attributes the set holds.
pub fn Attributes::len(self : Attributes) -> Int32 {
  self.entries.length()
}

///|
/// A string that is equal for equal attribute sets and different otherwise.
///
/// Used as the key of a series, so it has to be unambiguous: each key and value
/// carries its length, and the type of the value is part of the text — a string
/// `"12"` and the integer `12` are two different dimensions.
pub fn Attributes::identity(self : Attributes) -> String {
  let out = StringBuilder()
  for entry in self.entries {
    let key = entry.0
    let value = value_key(entry.1)
    out.write_string(key.length().to_string())
    out.write_char(':')
    out.write_string(key)
    out.write_char('=')
    out.write_string(value.length().to_string())
    out.write_char(':')
    out.write_string(value)
  }
  out.to_string()
}

///|
/// The attribute keys the GenAI metric conventions attach to a client operation.
/// A dashboard matches on these spellings; writing them by hand is how a near
/// miss becomes a value no dashboard shows.
pub let gen_ai_metric_keys : Array[String] = [
  @semconv.gen_ai_provider_name, @semconv.gen_ai_operation_name, @semconv.gen_ai_request_model,
  @semconv.gen_ai_response_model, @semconv.gen_ai_agent_name, @semconv.gen_ai_conversation_id,
]

///|
/// The attribute group for one GenAI client operation.
///
/// The three required names come from the metric's attribute group; the optional
/// ones are the parts a caller knows only after the response arrives. Every key
/// is a constant from `semconv`, so a rename upstream moves the pin and the
/// generated constants rather than a string in instrumented code.
pub fn gen_ai_client_attributes(
  provider~ : String,
  operation~ : String,
  request_model~ : String,
  response_model? : String,
  agent_name? : String,
  conversation_id? : String,
) -> Attributes {
  let entries : Array[(String, @trace.Value)] = [
    (@semconv.gen_ai_provider_name, @trace.Str(provider)),
    (@semconv.gen_ai_operation_name, @trace.Str(operation)),
    (@semconv.gen_ai_request_model, @trace.Str(request_model)),
  ]
  match response_model {
    Some(value) =>
      entries.push((@semconv.gen_ai_response_model, @trace.Str(value)))
    None => ()
  }
  match agent_name {
    Some(value) => entries.push((@semconv.gen_ai_agent_name, @trace.Str(value)))
    None => ()
  }
  match conversation_id {
    Some(value) =>
      entries.push((@semconv.gen_ai_conversation_id, @trace.Str(value)))
    None => ()
  }
  Attributes::new(entries~)
}

// ------------------------------------------------------------------ internals

///|
/// Sort by key, keep the last value under a duplicate key. Selection sort is
/// enough for the 128 attributes a set can hold, and it needs no comparator from
/// outside this package.
fn normalize(entries : Array[(String, @trace.Value)]) -> Attributes {
  let sorted = entries.copy()
  for i in 0.. Bool {
  let a = codes(left)
  let b = codes(right)
  let shared = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. Array[Int32] {
  let out : Array[Int32] = []
  for c in text {
    out.push(c.to_int())
  }
  out
}

///|
fn value_key(value : @trace.Value) -> String {
  match value {
    @trace.Bool(v) => "b\{v}"
    @trace.Int(v) => "i\{v}"
    @trace.Double(v) => "d\{v}"
    @trace.Str(v) => "s\{v}"
    @trace.Bools(vs) => typed_array("ba", vs.length(), i => "\{vs[i]}")
    @trace.Ints(vs) => typed_array("ia", vs.length(), i => "\{vs[i]}")
    @trace.Doubles(vs) => typed_array("da", vs.length(), i => "\{vs[i]}")
    @trace.Strs(vs) => typed_array("sa", vs.length(), i => vs[i])
  }
}

///|
fn typed_array(tag : String, length : Int32, at : (Int32) -> String) -> String {
  let out = StringBuilder()
  out.write_string(tag)
  for i in 0..