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

///|
/// One log record, as OpenTelemetry's data model defines it: when it happened,
/// what it says, how urgent it is, the trace it belongs to, and the attributes it
/// carries.
///
/// It is a value. A sink may keep it, queue it, copy it or throw it away, and a
/// logger that has handed one over never sees it again.
///
/// `time` is `None` when the caller has no event time of its own — a record read
/// out of another logging library may know only when it was collected. The
/// observed time is always present, because a record that cannot say when it was
/// seen cannot be ordered at all.
#derive(Eq, Debug)
pub(all) struct LogRecord {
  time : Int64?
  observed : Int64
  severity : Severity
  /// The severity as the producer spelled it. `None` means the short name of
  /// `severity`, which is what most producers mean.
  severity_text : String?
  body : @trace.Value
  attributes : Array[(String, @trace.Value)]
  /// The trace this record was written inside, when it was written inside one.
  /// Both ids are present or neither is: a span id without its trace names
  /// nothing.
  trace_id : Bytes?
  span_id : Bytes?
  flags : Byte
  /// The name of the event this record describes, for a record that is also an
  /// event. Only a record with a name can become a span event.
  event_name : String?
  /// The instrumentation scope: which logger in the program wrote this.
  scope : String
  dropped : Int32
}

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

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

///|
/// A record at `at`, observed at the same moment unless the caller says otherwise.
pub fn LogRecord::new(
  severity~ : Severity,
  body~ : @trace.Value,
  at~ : Int64,
  observed? : Int64,
  scope~ : String = "",
) -> LogRecord {
  {
    time: Some(at),
    observed: observed.unwrap_or(at),
    severity,
    severity_text: None,
    body,
    attributes: [],
    trace_id: None,
    span_id: None,
    flags: b'\x00',
    event_name: None,
    scope,
    dropped: 0,
  }
}

///|
/// The same record with one attribute added, replacing a value under the same key.
pub fn LogRecord::with_attribute(
  self : LogRecord,
  key : String,
  value : @trace.Value,
) -> LogRecord {
  let attributes : Array[(String, @trace.Value)] = []
  let mut replaced = false
  for attribute in self.attributes {
    if attribute.0 == key {
      attributes.push((key, value))
      replaced = true
    } else {
      attributes.push(attribute)
    }
  }
  if !replaced {
    attributes.push((key, value))
  }
  { ..self, attributes, }
}

///|
/// The same record carrying `context`'s trace and span ids and its flags. A record
/// written inside a span names that span, which is what lets a backend show the
/// log line beside the work it belongs to.
pub fn LogRecord::with_context(
  self : LogRecord,
  context : @trace.TraceContext,
) -> LogRecord {
  {
    ..self,
    trace_id: Some(context.trace_id),
    span_id: Some(context.span_id),
    flags: context.flags,
  }
}

///|
/// The same record with an event name, which is what makes it eligible to become a
/// span event.
pub fn LogRecord::with_event_name(
  self : LogRecord,
  event_name : String,
) -> LogRecord {
  { ..self, event_name: Some(event_name), }
}

///|
/// The same record timed at `at` instead of where it was.
pub fn LogRecord::with_time(self : LogRecord, at~ : Int64) -> LogRecord {
  { ..self, time: Some(at), }
}

///|
/// The severity as text: what the producer wrote, or the short name of the
/// severity when it wrote nothing.
pub fn LogRecord::severity_text(self : LogRecord) -> String {
  match self.severity_text {
    Some(text) => text
    None => self.severity.text()
  }
}

///|
/// When the record says it happened: its own time when it has one, and the moment
/// it was observed otherwise. This is the timestamp a backend orders on, so it is
/// never absent.
pub fn LogRecord::timestamp(self : LogRecord) -> Int64 {
  match self.time {
    Some(at) => at
    None => self.observed
  }
}

///|
/// The trace and span the record belongs to, or `None` when it names no trace or
/// names one that is not a trace: an id of the wrong width, or all zeros, is a
/// producer's mistake rather than a link.
pub fn LogRecord::context(self : LogRecord) -> @trace.TraceContext? {
  match (self.trace_id, self.span_id) {
    (Some(trace_id), Some(span_id)) => {
      let context = @trace.TraceContext::new(
        trace_id~,
        span_id~,
        flags=self.flags,
      )
      if context.is_valid() {
        Some(context)
      } else {
        None
      }
    }
    _ => None
  }
}