## Tracing — lightweight OpenTelemetry-compatible span recording
import std/json
import std/times
import std/monotimes
import std/tables
import std/httpclient
import std/strutils

type
  SpanStatus* = enum
    ssOk = "OK"
    ssError = "ERROR"

  Span* = ref object
    traceId*: string
    spanId*: string
    parentSpanId*: string
    name*: string
    startTime*: int64         # monotonic ticks (for duration)
    startTimeEpochNs*: int64  # Unix epoch nanoseconds (for OTLP)
    endTime*: int64
    endTimeEpochNs*: int64
    durationMs*: float64
    status*: SpanStatus
    attributes*: Table[string, string]

  Tracer* = ref object
    spans*: seq[Span]
    activeSpan*: Span
    enabled*: bool
    nextId*: uint64

var defaultTracer* = Tracer(spans: @[], activeSpan: nil, enabled: false, nextId: 1)

proc genId(tracer: Tracer): string =
  result = $tracer.nextId
  inc tracer.nextId

proc beginSpan*(tracer: Tracer, name: string, attributes: Table[string, string] = initTable[string, string]()): Span =
  if not tracer.enabled: return nil
  let nowNs = int64(epochTime() * 1_000_000_000)
  let span = Span(
    traceId: if tracer.activeSpan != nil: tracer.activeSpan.traceId else: genId(tracer),
    spanId: genId(tracer),
    parentSpanId: if tracer.activeSpan != nil: tracer.activeSpan.spanId else: "",
    name: name,
    startTime: getMonoTime().ticks(),
    startTimeEpochNs: nowNs,
    attributes: attributes,
  )
  tracer.spans.add(span)
  tracer.activeSpan = span
  return span

proc endSpan*(tracer: Tracer, span: Span, status: SpanStatus = ssOk) =
  if span == nil or not tracer.enabled: return
  span.endTime = getMonoTime().ticks()
  span.endTimeEpochNs = int64(epochTime() * 1_000_000_000)
  span.durationMs = float64(span.endTime - span.startTime) / 1_000_000.0
  span.status = status
  if span.parentSpanId.len > 0:
    for s in tracer.spans:
      if s.spanId == span.parentSpanId:
        tracer.activeSpan = s
        return
  tracer.activeSpan = nil

proc setSpanError*(span: Span, msg: string) =
  if span == nil: return
  span.status = ssError
  span.attributes["error.message"] = msg

proc flushSpans*(tracer: Tracer): JsonNode =
  result = newJArray()
  for span in tracer.spans:
    result.add(%*{
      "traceId": span.traceId,
      "spanId": span.spanId,
      "parentSpanId": span.parentSpanId,
      "name": span.name,
      "durationMs": span.durationMs,
      "status": $span.status,
      "attributes": span.attributes,
    })
  tracer.spans = @[]

proc enable*(tracer: Tracer) =
  tracer.enabled = true

proc disable*(tracer: Tracer) =
  tracer.enabled = false

proc exportOtlp*(tracer: Tracer, endpoint: string = "http://localhost:4318/v1/traces"): bool =
  ## Export spans via OTLP/HTTP (JSON format).
  ## Returns true on success.
  if tracer.spans.len == 0: return true
  var otlpSpans = newJArray()
  for span in tracer.spans:
    var attrs = newJArray()
    for k, v in span.attributes:
      attrs.add(%*{"key": k, "value": {"stringValue": v}})
    otlpSpans.add(%*{
      "traceId": span.traceId,
      "spanId": span.spanId,
      "parentSpanId": span.parentSpanId,
      "name": span.name,
      "kind": "SPAN_KIND_INTERNAL",
      "startTimeUnixNano": $span.startTimeEpochNs,
      "endTimeUnixNano": $span.endTimeEpochNs,
      "status": {"code": if span.status == ssOk: "STATUS_CODE_OK" else: "STATUS_CODE_ERROR"},
      "attributes": attrs,
    })
  let body = %*{
    "resourceSpans": [{
      "resource": {"attributes": [
        {"key": "service.name", "value": {"stringValue": "baradadb"}}
      ]},
      "scopeSpans": [{
        "scope": {"name": "baradadb-tracer", "version": "1.1.6"},
        "spans": otlpSpans
      }]
    }]
  }
  try:
    let client = newHttpClient()
    client.headers["Content-Type"] = "application/json"
    discard client.postContent(endpoint, body = $body)
    client.close()
    tracer.spans = @[]
    return true
  except CatchableError:
    return false
