// The HTTP server metrics, on the Prometheus registry the process already owns.
//
// go-zero registers two families: a duration histogram and a code counter, both
// labelled `path`/`method`/`code`. The three stay three here — folding them into
// one `"GET /ping 200"` string makes the series unqueryable, because Prometheus
// cannot select on a substring of a label value.

///|
/// The latency buckets go-zero registers for a server route, in milliseconds.
pub let default_latency_buckets : Array[Double] = [
  5, 10, 25, 50, 100, 250, 500, 750, 1000,
]

///|
/// The content type a `/metrics` scrape carries so a Prometheus server reads the
/// body as the text exposition format.
pub let exposition_content_type : String = "text/plain; version=0.0.4; charset=utf-8"

///|
/// The label names both families carry, in the order `observe` fills them.
let server_labels : Array[String] = ["path", "method", "code"]

///|
/// The request metrics an HTTP service exposes: `http_server_requests_duration_ms`
/// and `http_server_requests_code_total`, each partitioned by `path`, `method` and
/// `code`. Held by the caller so it can be scraped after serving, and shared with
/// `RestEngine` so the `prometheus` layer and the `/metrics` route read one set.
pub struct ServerMetrics {
  registry : @moonp8s.Registry
  requests : @moonp8s.Family[@moonp8s.Counter]
  latency : @moonp8s.Family[@moonp8s.Histogram]
}

///|
/// A fresh metric set on its own registry — the two families ready, nothing
/// observed yet. Raises `Refused` only if a metric or label name is invalid,
/// which cannot happen for the fixed names here.
pub fn ServerMetrics::new() -> ServerMetrics raise @metric.Refused {
  let registry = @moonp8s.Registry::new()
  {
    registry,
    requests: @moonp8s.Family::new(server_labels, () => @moonp8s.Counter::new()),
    latency: @moonp8s.Family::new(server_labels, () => {
      @moonp8s.Histogram::new(buckets=default_latency_buckets)
    }),
  }
}

///|
/// The registry the families hang off, so a caller can add collectors of its own
/// and publish everything through one scrape.
pub fn ServerMetrics::registry(self : ServerMetrics) -> @moonp8s.Registry {
  self.registry
}

///|
/// The request counter family, labelled `path`, `method`, `code`.
pub fn ServerMetrics::requests(
  self : ServerMetrics,
) -> @moonp8s.Family[@moonp8s.Counter] {
  self.requests
}

///|
/// The request-duration family, in milliseconds, labelled `path`, `method`,
/// `code`.
pub fn ServerMetrics::latency(
  self : ServerMetrics,
) -> @moonp8s.Family[@moonp8s.Histogram] {
  self.latency
}

///|
/// Record one finished request: count it under its `path`/`method`/`code` and
/// observe `duration_ms`. A label set whose arity does not match the registered
/// names is dropped rather than raised — both families always carry three labels,
/// so this is unreachable from `observe_request`.
pub fn ServerMetrics::observe(
  self : ServerMetrics,
  path : String,
  verb : String,
  code : Int32,
  duration_ms : Double,
) -> Unit {
  let labels = [path, verb, code.to_string()]
  self.requests.labels(labels).inc() catch {
    _ => ()
  }
  self.latency.labels(labels).observe(duration_ms) catch {
    _ => ()
  }
}

///|
/// The counter value recorded for one route and status, `0` before the first
/// request. A read-out for tests and for a health endpoint that reports its own
/// traffic without scraping.
pub fn ServerMetrics::request_count(
  self : ServerMetrics,
  path : String,
  verb : String,
  code : Int32,
) -> Double {
  self.requests.labels([path, verb, code.to_string()]).get() catch {
    _ => 0.0
  }
}

///|
/// The latency observations recorded for one route and status.
pub fn ServerMetrics::latency_total(
  self : ServerMetrics,
  path : String,
  verb : String,
  code : Int32,
) -> Int64 {
  self.latency.labels([path, verb, code.to_string()]).total() catch {
    _ => 0L
  }
}

///|
/// Render this set as the Prometheus text exposition format — the body
/// `metrics_handler` serves.
pub fn ServerMetrics::to_exposition(self : ServerMetrics) -> String {
  let families : Array[@moonp8s.MetricFamily] = []
  families.push(@moonp8s.MetricFamily::{
    name: "http_server_requests_duration_ms",
    kind: Histogram,
    help: "http server requests duration(ms).",
    unit: None,
    metrics: self.latency.points(),
  })
  families.push(@moonp8s.MetricFamily::{
    name: "http_server_requests_code",
    kind: Counter,
    help: "http server requests code count.",
    unit: None,
    metrics: self.requests.points(),
  })
  for family in self.registry.collect() {
    families.push(family)
  }
  @moonp8s.text(families)
}

///|
/// A handler that serves the current metric set in the text exposition format,
/// with the Prometheus content type.
pub fn metrics_handler(m : ServerMetrics) -> @moonapi.ApiHandler {
  _ctx => {
    @moonasgi.Response::new(
      200,
      [("content-type", exposition_content_type)],
      @utf8.encode(m.to_exposition()),
    )
  }
}

///|
/// Register the `GET /metrics` scrape endpoint on `app`, so a Prometheus server
/// can pull the exposition.
pub fn mount_metrics(app : @moonapi.App, m : ServerMetrics) -> Unit {
  app.get("/metrics", metrics_handler(m))
}

///|
/// Metrics middleware: time each HTTP request on the shared clock and, when the
/// response starts, count it under its path, method and status and record its
/// latency in milliseconds. The record is taken once per request even if a
/// downstream emits a second start. Non-HTTP scopes pass through unmeasured.
pub fn metrics(m : ServerMetrics, clock : @clock.Clock) -> Middleware {
  inner => {
    (scope, receive, send) => {
      match scope {
        Http(hs) => {
          let start = clock.now()
          let verb = hs.http_method
          let path = hs.path
          let recorded : Ref[Bool] = { val: false, }
          let observed : @moonasgi.Send = event => {
            match event {
              HttpResponseStart(status~, headers~, trailers~) => {
                if !recorded.val {
                  recorded.val = true
                  m.observe(
                    path,
                    verb,
                    status,
                    (clock.now() - start).to_double(),
                  )
                }
                send(
                  @moonasgi.Event::HttpResponseStart(
                    status~,
                    headers~,
                    trailers~,
                  ),
                )
              }
              other => send(other)
            }
          }
          inner(scope, receive, observed)
        }
        _ => inner(scope, receive, send)
      }
    }
  }
}