// Server-side tracing, as the OpenTelemetry ASGI layer the rest of the family
// already uses. The span, the sampling decision and the propagation headers are
// moontel's; what is here is the wiring a framework owes its users — the
// `ignore_paths` blacklist and the convenience `x-trace-id` header.

///|
/// The `traceparent` header name a trace id is read out of.
let traceparent_header : String = "traceparent"

///|
/// The trace id carried by a W3C `traceparent` value,
/// `00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>`, or `None` when the
/// value is not one.
pub fn trace_id_of(traceparent : String) -> String? {
  let parts = traceparent.split("-").collect()
  guard parts.length() == 4 else { None }
  guard parts[1].length() == 32 else { None }
  Some(parts[1].to_owned())
}

///|
/// Add `x-trace-id` next to the `traceparent` a tracing layer already wrote, so
/// a client that only wants the id does not have to parse the standard header.
/// Headers that already carry the convenience name keep their value.
fn with_trace_id_header(
  headers : Array[(String, String)],
  header : String,
) -> Array[(String, String)] {
  for pair in headers {
    if pair.0 == header {
      return headers
    }
    if pair.0 == traceparent_header {
      match trace_id_of(pair.1) {
        Some(id) => return [(header, id), ..headers]
        None => ()
      }
    }
  }
  headers
}

///|
/// Tracing middleware: join the trace the request arrived on, or start one, and
/// open a server span that reaches `sink` when the request finishes. Paths in
/// `ignore_paths` are left untraced. The span and the propagation headers come
/// from moontel's ASGI layer; this adds the convenience `x-trace-id` response
/// header on top.
///
/// The defaults — a discarding sink, always sampling, ids from the platform
/// entropy source — make `tracing()` a working layer with nothing configured; a
/// service that wants spans kept hands in a tracer over its own sink.
pub fn tracing(
  tracer~ : @trace.Tracer = @telemetry.tracer(),
  now~ : () -> Int64 = @telemetry.trace_clock(),
  header~ : String = "x-trace-id",
  ignore_paths~ : Array[String] = [],
) -> Middleware {
  let layer = @moontel.Tracing::new(tracer~, now~, ignore=ignore_paths)
  inner => {
    let traced = layer.wrap(inner)
    (scope, receive, send) => {
      traced(scope, receive, event => {
        let event = match event {
          HttpResponseStart(status~, headers~, trailers~) =>
            @moonasgi.Event::HttpResponseStart(
              status~,
              headers=with_trace_id_header(headers, header),
              trailers~,
            )
          other => other
        }
        send(event)
      })
    }
  }
}