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

///|
/// Encode log records as an OTLP/JSON `ExportLogsServiceRequest`, the body
/// `POST /v1/logs` takes.
///
/// Records are grouped by the scope that wrote them, because a scope belongs to
/// the request rather than to a record: one exporter can carry the lines of
/// several loggers and the backend still sees which one wrote what.
pub fn encode_logs(
  records : Array[@log.LogRecord],
  resource~ : Resource = unknown_service,
  scope~ : Scope = moonbit_scope,
) -> String {
  let names : Array[String] = []
  for record in records {
    if !names.contains(record.scope) {
      names.push(record.scope)
    }
  }
  let w = JsonWriter::new()
  w.open_object()
  w.name("resourceLogs")
  w.open_array()
  w.open_object()
  w.name("resource")
  write_resource(w, resource)
  w.name("scopeLogs")
  w.open_array()
  for name in names {
    w.open_object()
    w.name("scope")
    // A record written without a scope of its own belongs to the exporter's: a
    // scope is what a backend groups by, and an empty name groups nothing.
    write_scope(w, {
      ..scope,
      name: if name == "" {
        scope.name
      } else {
        name
      },
    })
    w.name("logRecords")
    w.open_array()
    for record in records {
      if record.scope == name {
        write_log_record(w, record)
      }
    }
    w.close_array()
    w.close_object()
  }
  w.close_array()
  w.close_object()
  w.close_array()
  w.close_object()
  w.text()
}

///|
fn write_log_record(w : JsonWriter, record : @log.LogRecord) -> Unit {
  w.open_object()
  match record.time {
    Some(time) => {
      w.name("timeUnixNano")
      w.int64(time)
    }
    None => ()
  }
  w.name("observedTimeUnixNano")
  w.int64(record.observed)
  w.name("severityNumber")
  w.raw(record.severity.number().to_string())
  w.name("severityText")
  w.string(record.severity_text())
  w.name("body")
  write_any_value(w, record.body)
  if !record.attributes.is_empty() {
    w.name("attributes")
    write_attributes(w, record.attributes)
  }
  match record.context() {
    Some(context) => {
      w.name("traceId")
      w.string(@base16.encode(context.trace_id[:]))
      w.name("spanId")
      w.string(@base16.encode(context.span_id[:]))
      if context.flags != b'\x00' {
        w.name("flags")
        w.raw(context.flags.to_int().to_string())
      }
    }
    None => ()
  }
  match record.event_name {
    Some(name) =>
      if name != "" {
        w.name("eventName")
        w.string(name)
      }
    None => ()
  }
  if record.dropped > 0 {
    w.name("droppedAttributesCount")
    w.raw(record.dropped.to_string())
  }
  w.close_object()
}