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

///|
/// What a span is about, from the point of view of the process that made it.
/// The kind is part of the sampling input and of what a backend draws, so it is
/// settled when the span starts.
#derive(Eq, Debug)
pub(all) enum SpanKind {
  Internal
  Server
  Client
  Producer
  Consumer
}

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

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

///|
/// How the operation ended, as the caller judges it.
///
/// The three values form a total order, `Ok` above `Error` above `Unset`, and a
/// status is judged against that order rather than by "last write wins": `Ok` is
/// final, `Error` may replace another `Error` because the message may improve, and
/// `Unset` is what a span already has.
///
/// `Unset` means "the caller has no opinion", which is not a failure: an
/// instrumentation library leaves it alone unless the semantic conventions say
/// otherwise.
#derive(Eq, Debug)
pub(all) enum Status {
  Unset
  Ok
  Error(String)
}

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

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

///|
/// The status as text, with the description beside the code that carries it.
pub impl Show for Status with fn output(self, logger) {
  match self {
    Unset => logger.write_string("Unset")
    Ok => logger.write_string("Ok")
    Error(message) => {
      logger.write_string("Error(")
      logger.write_string(message)
      logger.write_char(')')
    }
  }
}

///|
pub extend Status with Show::{to_string, output}

///|
/// One attribute value.
///
/// The arrays are separate variants rather than one array of `Value` because
/// OpenTelemetry requires an array attribute to be homogeneous, and because that is
/// what an exporter has to write: a mixed array has no single type tag.
#derive(Eq, Debug)
pub(all) enum Value {
  Bool(Bool)
  /// A signed 64-bit integer. Nanosecond timestamps and byte counts both fit; a
  /// `Double` would lose the low digits of a nanosecond clock.
  Int(Int64)
  Double(Double)
  Str(String)
  Bools(Array[Bool])
  Ints(Array[Int64])
  Doubles(Array[Double])
  Strs(Array[String])
}

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

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

///|
/// The value as text, for a log line or a debug page: strings quoted so that an
/// empty one is visible, arrays bracketed and comma-separated.
pub impl Show for Value with fn output(self, logger) {
  match self {
    Bool(value) => logger.write_string(value.to_string())
    Int(value) => logger.write_string(value.to_string())
    Double(value) => logger.write_string(value.to_string())
    Str(value) => {
      logger.write_char('"')
      logger.write_string(value)
      logger.write_char('"')
    }
    Bools(values) =>
      logger.write_string(text_of(values.map(v => v.to_string())))
    Ints(values) => logger.write_string(text_of(values.map(v => v.to_string())))
    Doubles(values) =>
      logger.write_string(text_of(values.map(v => v.to_string())))
    Strs(values) =>
      logger.write_string(text_of(values.map(v => "\"" + v + "\"")))
  }
}

///|
pub extend Value with Show::{to_string, output}

///|
fn text_of(items : Array[String]) -> String {
  "[" + items.join(", ") + "]"
}

///|
/// Something that happened during a span: a name, when it happened, and what it
/// carried. Events keep the order they were added in, which for timestamps handed
/// in by the caller is not necessarily the order of those timestamps.
#derive(Eq, Debug)
pub(all) struct Event {
  name : String
  at : Int64
  attributes : Array[(String, Value)]
}

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

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

///|
/// A causal relationship to another span that is not this one's parent — a batch
/// item, a message consumed later, a fan-in. The linked context travels with the
/// link so a reader can join the two traces.
#derive(Eq, Debug)
pub(all) struct Link {
  context : TraceContext
  attributes : Array[(String, Value)]
}

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

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

///|
/// How much a span gave up at its limits. OpenTelemetry asks for the counts rather
/// than for the dropped values, so a reader can tell "this span had no attributes"
/// from "this span had three hundred and we kept 128".
#derive(Eq, Debug)
pub(all) struct Dropped {
  attributes : Int32
  events : Int32
  links : Int32
}

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

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

///|
/// Nothing dropped, the value a span starts with.
pub let dropped_nothing : Dropped = { attributes: 0, events: 0, links: 0, }

///|
/// A finished span, as a `Sink` receives it.
///
/// It is a value, not a handle: everything in it is settled, and a sink may keep
/// it, queue it or throw it away. Nothing here is normalised — a span may begin and
/// end at the same nanosecond, and an event may carry a time outside the span.
#derive(Eq, Debug)
pub(all) struct SpanData {
  context : TraceContext
  /// The parent's span id, or `None` for the first span of a trace. The parent's
  /// own context is deliberately not carried: a child needs the id to say where it
  /// hangs, and nothing else about the parent.
  parent : Bytes?
  name : String
  kind : SpanKind
  start : Int64
  end : Int64
  attributes : Array[(String, Value)]
  events : Array[Event]
  links : Array[Link]
  status : Status
  dropped : Dropped
}

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

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

///|
/// Whether this span carries the `sampled` bit, which is the one thing a sink
/// needs in order to choose between exporting and merely counting.
pub fn SpanData::sampled(self : SpanData) -> Bool {
  self.context.sampled()
}

///|
/// How long the span lasted, in nanoseconds. Negative only if the caller handed in
/// an end before the start, which is left as it came.
pub fn SpanData::duration(self : SpanData) -> Int64 {
  self.end - self.start
}

///|
/// The span a caller holds: an identity fixed at creation, and fields that fill in
/// until it is finished.
///
/// Everything that changes after creation sits behind a `Ref`, so one span may be
/// filled in from the task that opened it and read from another. A non-recording
/// span — one the sampler dropped — accepts the same calls and forgets them, which
/// is what lets instrumented code stay the same whatever the sampler decided.
pub struct Span {
  context : TraceContext
  parent : Bytes?
  kind : SpanKind
  start : Int64
  recording : Bool
  sink : &Sink
  limits : Limits
  span_name : Ref[String]
  attribute_list : Ref[Array[(String, Value)]]
  event_list : Ref[Array[Event]]
  link_list : Ref[Array[Link]]
  span_status : Ref[Status]
  end_time : Ref[Int64?]
  dropped : Ref[Dropped]
}

///|
/// The identity of the span. It does not change for the span's whole life and
/// stays usable after it ends, because a later span may still name this one as its
/// parent.
pub fn Span::context(self : Span) -> TraceContext {
  self.context
}

///|
/// Whether the span is collecting anything: false after it ends, and false for a
/// span the sampler dropped. Instrumented code can use it to skip building
/// expensive attributes.
pub fn Span::is_recording(self : Span) -> Bool {
  self.recording && self.end_time.val is None
}

///|
/// Whether the span has been finished. A finished span takes no more writes and is
/// not handed to the sink a second time.
pub fn Span::is_ended(self : Span) -> Bool {
  self.end_time.val is Some(_)
}

///|
/// The name the span carries now, which `set_name` may have changed since it
/// started.
pub fn Span::name(self : Span) -> String {
  self.span_name.val
}

///|
/// A copy of the attributes set so far, in the order their keys first appeared.
pub fn Span::attributes(self : Span) -> Array[(String, Value)] {
  self.attribute_list.val.copy()
}

///|
/// A copy of the events added so far, in the order they were added.
pub fn Span::events(self : Span) -> Array[Event] {
  self.event_list.val.copy()
}

///|
/// A copy of the links added so far.
pub fn Span::links(self : Span) -> Array[Link] {
  self.link_list.val.copy()
}

///|
/// The status as it stands: `Unset` until a caller sets one.
pub fn Span::status(self : Span) -> Status {
  self.span_status.val
}

///|
/// Set one attribute. A key already present is overwritten in place, so the
/// attribute list keeps the order in which keys first appeared rather than the
/// order of their last write.
///
/// Past the attribute limit the write is dropped and counted rather than applied: a
/// span with a runaway loop of attributes must not take the process down with it.
pub fn Span::set_attribute(self : Span, key : String, value : Value) -> Unit {
  self.set_attributes([(key, value)])
}

///|
/// Set several attributes at once, each following the same rule as
/// `set_attribute`.
pub fn Span::set_attributes(
  self : Span,
  attributes : Array[(String, Value)],
) -> Unit {
  guard self.is_recording() else { return }
  let kept = self.attribute_list.val
  let mut dropped = self.dropped.val
  for attribute in attributes {
    let value = truncate(attribute.1, self.limits.attribute_value_length)
    let mut at = None
    for i, existing in kept {
      if existing.0 == attribute.0 {
        at = Some(i)
        break
      }
    }
    match at {
      // Replacing an attribute is not a new attribute: the count does not move.
      Some(i) => kept[i] = (attribute.0, value)
      None =>
        if kept.length() < self.limits.attributes {
          kept.push((attribute.0, value))
        } else {
          dropped = { ..dropped, attributes: dropped.attributes + 1, }
        }
    }
  }
  self.dropped.val = dropped
}

///|
/// Add an event. `at` is the moment the caller says it happened, so a span recorded
/// after the fact keeps its real timeline.
pub fn Span::add_event(
  self : Span,
  name : String,
  attributes~ : Array[(String, Value)] = [],
  at~ : Int64,
) -> Unit {
  guard self.is_recording() else { return }
  let events = self.event_list.val
  if events.length() >= self.limits.events {
    let dropped = self.dropped.val
    self.dropped.val = { ..dropped, events: dropped.events + 1, }
    return
  }
  let kept : Array[(String, Value)] = []
  for attribute in attributes {
    if kept.length() >= self.limits.attributes_per_event {
      break
    }
    kept.push(
      (attribute.0, truncate(attribute.1, self.limits.attribute_value_length)),
    )
  }
  events.push({ name, at, attributes: kept, })
}

///|
/// Record an error the way OpenTelemetry's exception conventions ask: an event
/// named `exception` carrying `exception.type`, `exception.message` and
/// `exception.stacktrace`, with any `attributes` of the caller's own taking
/// precedence over those three.
pub fn Span::record_exception(
  self : Span,
  message~ : String,
  at~ : Int64,
  type_~ : String = "",
  stacktrace~ : String = "",
  attributes~ : Array[(String, Value)] = [],
) -> Unit {
  let own : Array[(String, Value)] = []
  if type_ != "" {
    own.push(("exception.type", Str(type_)))
  }
  own.push(("exception.message", Str(message)))
  if stacktrace != "" {
    own.push(("exception.stacktrace", Str(stacktrace)))
  }
  // The caller's own attributes take precedence over the conventional three, so a
  // key given twice ends up with the caller's value rather than doubled.
  for attribute in attributes {
    let mut at = None
    for i, existing in own {
      if existing.0 == attribute.0 {
        at = Some(i)
        break
      }
    }
    match at {
      Some(i) => own[i] = attribute
      None => own.push(attribute)
    }
  }
  self.add_event("exception", attributes=own, at~)
}

///|
/// Set the status, judged against the order `Ok` above `Error` above `Unset`:
/// `Unset` changes nothing, `Ok` is final, and a later `Error` replaces an earlier
/// one so the message can improve.
pub fn Span::set_status(self : Span, status : Status) -> Unit {
  guard self.is_recording() else { return }
  let current = self.span_status.val
  self.span_status.val = match (current, status) {
    (Ok, _) => Ok
    (_, Unset) => current
    _ => status
  }
}

///|
/// Rename the span. The sampler has already decided, so a better name found during
/// the request cannot change that decision — it changes what the backend draws.
pub fn Span::set_name(self : Span, name : String) -> Unit {
  guard self.is_recording() else { return }
  self.span_name.val = name
}

///|
/// Add a link to another span after creation, past the link limit being counted
/// rather than applied.
pub fn Span::add_link(self : Span, link : Link) -> Unit {
  guard self.is_recording() else { return }
  let links = self.link_list.val
  if links.length() >= self.limits.links {
    let dropped = self.dropped.val
    self.dropped.val = { ..dropped, links: dropped.links + 1, }
    return
  }
  links.push(link)
}

///|
/// Finish the span at `at`, handing it to the sink once.
///
/// Finishing twice does nothing the second time, and finishing a span the sampler
/// dropped hands nothing over. Child spans are untouched: they may still be
/// running, and they keep naming this one as their parent.
pub fn Span::finish(self : Span, at~ : Int64) -> Unit {
  guard self.end_time.val is None else { return }
  self.end_time.val = Some(at)
  guard self.recording else { return }
  self.sink.accept({
    context: self.context,
    parent: self.parent,
    name: self.span_name.val,
    kind: self.kind,
    start: self.start,
    end: at,
    attributes: self.attribute_list.val.copy(),
    events: self.event_list.val.copy(),
    links: self.link_list.val.copy(),
    status: self.span_status.val,
    dropped: self.dropped.val,
  })
}

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

///|
/// A string attribute longer than the limit is cut to it, counting each character
/// as one, as OpenTelemetry's attribute limits ask. Every other type passes
/// through: an integer has nothing to truncate.
fn truncate(value : Value, limit : Int32?) -> Value {
  match (value, limit) {
    (Str(s), Some(n)) if s.length() > n =>
      Str(s.exact_view(start=0, end=n).to_owned())
    (Strs(xs), Some(n)) =>
      Strs(
        xs.map(s => {
          if s.length() > n {
            s.exact_view(start=0, end=n).to_owned()
          } else {
            s
          }
        }),
      )
    _ => value
  }
}

///|
fn Span::new(
  context : TraceContext,
  parent : Bytes?,
  kind : SpanKind,
  start : Int64,
  name : String,
  recording : Bool,
  sink : &Sink,
  limits : Limits,
) -> Span {
  {
    context,
    parent,
    kind,
    start,
    recording,
    sink,
    limits,
    span_name: Ref(name),
    attribute_list: Ref([]),
    event_list: Ref([]),
    link_list: Ref([]),
    span_status: Ref(Unset),
    end_time: Ref(None),
    dropped: Ref(dropped_nothing),
  }
}