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

///|
/// One server span per HTTP request, joined to the trace the request arrived with.
///
/// The layer does three things and no more: it reads `traceparent` and `baggage`
/// off the request, it opens a span of kind `Server` as a child of what it read, and
/// it writes the resulting context back on the response. A request that carried no
/// context starts a trace; one that carried a broken context also starts a trace,
/// because refusing the request over a header would be worse than a missing link in
/// the trace.
///
/// The span reaches the sink when the application returns, with the response status
/// on it. A request the application fails is recorded as an exception event before
/// the error is passed on to the server, which is what an ASGI server keeps.
pub(all) struct Tracing {
  tracer : @trace.Tracer
  /// The clock. This library never reads one itself, so a replay can drive a request
  /// through a timeline it chose and a service can hand in whichever clock it
  /// already trusts. Nanoseconds since any fixed origin.
  now : () -> Int64
  /// How a request is named. The default is the method and the path, which is what
  /// the middleware can see; a site that knows its routes should pass the route
  /// template instead, so that `/orders/42` and `/orders/43` are one span name
  /// rather than a thousand.
  name : (String, String) -> String
  /// Whether `baggage` rides along to the response. On by default: a hop that
  /// quietly drops what the caller attached is worse than one that never had it.
  baggage : Bool
  /// Paths to leave untraced. A health check every second is noise a backend pays
  /// for and nobody reads.
  ignore : Array[String]
}

///|
/// A tracing layer over `tracer`.
pub fn Tracing::new(
  tracer~ : @trace.Tracer,
  now~ : () -> Int64,
  name~ : (String, String) -> String = method_and_path,
  baggage~ : Bool = true,
  ignore~ : Array[String] = [],
) -> Tracing {
  { tracer, now, name, baggage, ignore, }
}

///|
/// The default span name: the method and the path, e.g. `GET /orders/42`.
pub fn method_and_path(http_method : String, path : String) -> String {
  http_method + " " + path
}

///|
/// Wraps an application in a server span per request. `wrap` is the `Layer` method,
/// so the middleware can sit in a stack beside the framework's own layers as well as
/// around a single application.
pub impl @mid.Layer for Tracing with fn wrap(self, app) {
  (scope, receive, send) => {
    guard scope is Http(http) else {
      // A WebSocket or a lifespan run is not an HTTP request: a server span for
      // either would need a different set of attributes, and guessing them here
      // would make the traces wrong rather than merely absent.
      app(scope, receive, send)
      return
    }
    guard !self.ignore.contains(http.path) else {
      app(scope, receive, send)
      return
    }
    let extracted = @propagate.extract(http.headers)
    let started = (self.now)()
    let span = self.start(http, extracted.context, started)
    let failure : Ref[Error?] = Ref(None)
    app(scope, receive, event => {
      let event = match event {
        @spec.HttpResponseStart(status~, headers~, trailers~) => {
          match span {
            Some(span) => {
              span.set_attribute(
                "http.response.status_code",
                @trace.Int(status.to_int64()),
              )
              // The semantic conventions leave a 5xx as the only status a server
              // span calls an error: a 404 is an answer, not a failure.
              if status >= 500 {
                span.set_status(@trace.Error("HTTP \{status}"))
              }
            }
            None => ()
          }
          let headers = match span {
            Some(span) => self.write_back(headers, span, extracted.baggage)
            None => headers
          }
          @spec.HttpResponseStart(status~, headers~, trailers~)
        }
        other => other
      }
      send(event)
    }) catch {
      error => failure.val = Some(error)
    }
    match span {
      None => ()
      Some(span) => {
        match failure.val {
          Some(error) =>
            span.record_exception(message="\{error}", at=(self.now)())
          None => ()
        }
        span.finish(at=(self.now)())
      }
    }
    match failure.val {
      Some(error) => raise error
      None => ()
    }
  }
}

///|
pub extend Tracing with @mid.Layer::{wrap}

///|
/// The server span for `http`, or `None` when the tracer's source of randomness
/// is broken — in which case the request runs untraced rather than failing, and
/// the failure shows up the next time anything else asks that generator for an id.
fn Tracing::start(
  self : Tracing,
  http : @spec.HttpScope,
  parent : @trace.TraceContext?,
  at : Int64,
) -> @trace.Span? {
  let attributes : Array[(String, @trace.Value)] = [
    ("http.request.method", @trace.Str(http.http_method)),
    ("url.path", @trace.Str(http.path)),
    ("url.scheme", @trace.Str(http.scheme)),
    ("network.protocol.version", @trace.Str(http.http_version)),
  ]
  let attributes = match http.server {
    Some((host, port)) => {
      let with_host = attributes.copy()
      with_host.push(("server.address", @trace.Str(host)))
      match port {
        Some(port) =>
          with_host.push(("server.port", @trace.Int(port.to_int64())))
        None => ()
      }
      with_host
    }
    None => attributes
  }
  let attributes = match http.client {
    Some((host, port)) => {
      let with_client = attributes.copy()
      with_client.push(("client.address", @trace.Str(host)))
      with_client.push(("client.port", @trace.Int(port.to_int64())))
      with_client
    }
    None => attributes
  }
  let span_name = (self.name)(http.http_method, http.path)
  try {
    match parent {
      Some(parent) =>
        Some(
          self.tracer.start(span_name, parent~, kind=Server, at~, attributes~),
        )
      None => Some(self.tracer.start(span_name, kind=Server, at~, attributes~))
    }
  } catch {
    _ => None
  }
}

///|
/// The response headers with this hop's context on them.
fn Tracing::write_back(
  self : Tracing,
  headers : Array[(String, String)],
  span : @trace.Span,
  baggage : @propagate.Baggage,
) -> Array[(String, String)] {
  @propagate.inject(headers, {
    context: Some(span.context()),
    baggage: if self.baggage {
      baggage
    } else {
      @propagate.empty_baggage
    },
  })
}