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

///|
/// The header `traceparent` travels in (W3C Trace Context §3.2.1). Lowercase on
/// the wire; lookups here compare without regard to case.
pub let traceparent_header : String = "traceparent"

///|
/// The header `tracestate` travels in (W3C Trace Context §3.3.1).
pub let tracestate_header : String = "tracestate"

///|
/// The header `baggage` travels in (W3C Baggage §3.2.1).
pub let baggage_header : String = "baggage"

///|
/// A parsed `traceparent`, plus the vendor state that came with it.
///
/// `version` is kept even though nothing here acts on it: a caller debugging a
/// failed handshake wants to know which version of the header the other side
/// spoke, and a version above zero may one day carry fields this library does not
/// read.
///
/// The ids are validated on the way in — sixteen and eight octets, neither all
/// zero — because a context that fails that is not a trace to join, and passing it
/// on would make a broken hop look like a working one.
pub fn parse(
  traceparent : StringView,
  tracestate? : StringView,
) -> @trace.TraceContext raise @trace.Refused {
  let context = parse_traceparent(traceparent)
  let state = match tracestate {
    None => @trace.empty_trace_state
    Some(text) => @trace.TraceState::parse(text)
  }
  { ..context, state, }
}

///|
/// Parse a `traceparent` header value, without any vendor state.
///
/// The grammar is W3C Trace Context §3.2:
///
/// ```text
/// version(2)-trace-id(32)-parent-id(16)-trace-flags(2)
/// ```
///
/// A version this library does not know may append fields, but only after a `-`:
/// `00` headers end at 55 characters, and `00-…-01.whatever` is refused rather
/// than read as a version-0 header with a suffix. Version `ff` is refused by the
/// specification, as is an all-zero id.
///
/// Whitespace around the value is dropped. HTTP allows OWS around a field value,
/// and W3C Trace Context's examples send it; whitespace anywhere inside is refused
/// as the illegal character it is.
pub fn parse_traceparent(
  input : StringView,
) -> @trace.TraceContext raise @trace.Refused {
  let text = strip_ows(input)
  let n = text.length()
  if n < 55 {
    raise @trace.Refused::Field(at=n, want="the 55 characters of a traceparent")
  }
  let version = hex_byte(text, 0)
  if version == 0xff {
    raise @trace.Refused::Field(at=0, want="a version below ff")
  }
  expect_dash(text, 2)
  let trace_id = hex_id(text, 3, 32)
  expect_dash(text, 35)
  let span_id = hex_id(text, 36, 16)
  expect_dash(text, 52)
  let flags = hex_byte(text, 53)
  if n > 55 {
    // A future version may add fields, and only after a dash. Refusing a version-0
    // header with a suffix keeps a typo in the flags from looking like a newer
    // header that this hop can safely ignore.
    if version == 0 {
      raise @trace.Refused::Field(at=55, want="the end of the header")
    }
    if text[55].to_int() != 0x2d {
      raise @trace.Refused::Field(at=55, want="a dash before the extra fields")
    }
  }
  @trace.TraceContext::new(
    trace_id~,
    span_id~,
    flags=flags.to_byte(),
    state=@trace.empty_trace_state,
    remote=true,
  )
}

///|
/// Parse a `tracestate` header value. The grammar lives with the type that holds
/// the state; this is the name a caller reaching for the header uses.
pub fn parse_tracestate(
  input : StringView,
) -> @trace.TraceState raise @trace.Refused {
  @trace.TraceState::parse(input)
}

///|
fn expect_dash(text : StringView, at : Int32) -> Unit raise @trace.Refused {
  if text[at].to_int() != 0x2d {
    raise @trace.Refused::Field(at~, want="a dash")
  }
}

///|
/// Two hexadecimal digits at `at`, lowercase only, as a number.
fn hex_byte(text : StringView, at : Int32) -> Int32 raise @trace.Refused {
  let high = hex_nibble(text, at)
  let low = hex_nibble(text, at + 1)
  (high << 4) | low
}

///|
/// An id of `width` hexadecimal digits starting at `at`, decoded to octets.
fn hex_id(
  text : StringView,
  at : Int32,
  width : Int32,
) -> Bytes raise @trace.Refused {
  let bytes : Array[Byte] = []
  let mut non_zero = false
  let mut i = 0
  while i < width {
    let high = hex_nibble(text, at + i)
    let low = hex_nibble(text, at + i + 1)
    let byte = ((high << 4) | low).to_byte()
    if byte != b'\x00' {
      non_zero = true
    }
    bytes.push(byte)
    i = i + 2
  }
  if !non_zero {
    raise @trace.Refused::Zero(at~)
  }
  Bytes::from_array(bytes)
}

///|
/// One lowercase hexadecimal digit. Uppercase is refused: W3C Trace Context writes
/// the fields in lowercase, and a hop that accepted both spellings would hand the
/// next hop a value it might not accept.
fn hex_nibble(text : StringView, at : Int32) -> Int32 raise @trace.Refused {
  let c = text[at].to_int()
  if c >= 0x30 && c <= 0x39 {
    c - 0x30
  } else if c >= 0x61 && c <= 0x66 {
    c - 0x61 + 10
  } else {
    raise @trace.Refused::NotHex(at~)
  }
}