///|
/// Write side of a text-map carrier used for outbound propagation.
///
/// Implement this for HTTP header maps or other text carriers that need
/// outgoing trace context and baggage injection.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#inject
pub(open) trait Injector {
  fn set(Self, String, String) -> Unit
}

///|
/// Read side of a text-map carrier used for inbound propagation.
///
/// Implement this for HTTP header maps or other text carriers that need
/// incoming trace context and baggage extraction. `keys()` is optional but lets
/// propagators handle case-insensitive carriers more efficiently.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#extract
pub(open) trait Extractor {
  fn get(Self, StringView) -> String?
  fn keys(Self) -> Array[String] = _
}

///|
impl Extractor with fn keys(_) {
  []
}

///|
fn normalize_ascii_lower(value : StringView) -> String {
  let builder = StringBuilder::new()
  for _, ch in value {
    builder.write_char(ch.to_ascii_lowercase())
  }
  builder.to_string()
}

///|
fn get_case_insensitive(
  values : Map[String, String],
  key : StringView,
) -> String? {
  let normalized_key = normalize_ascii_lower(key)
  match values.get_from_string(normalized_key) {
    Some(value) => Some(value)
    None => {
      for candidate_key, candidate_value in values {
        if normalize_ascii_lower(candidate_key) == normalized_key {
          return Some(candidate_value)
        }
      }
      None
    }
  }
}

///|
pub impl Injector for Map[String, String] with fn set(self, key, value) {
  self[normalize_ascii_lower(key)] = value
}

///|
pub impl Extractor for Map[String, String] with fn get(self, key) {
  get_case_insensitive(self, key)
}

///|
fn merge_baggage(
  current : @baggage.Baggage,
  incoming : @baggage.Baggage,
) -> @baggage.Baggage {
  let mut merged = current
  for entry in incoming.entries() {
    merged = merged.insert_with_metadata(
      entry.key.as_string(),
      entry.value,
      entry.metadata,
    )
  }
  merged
}

///|
pub impl Extractor for Map[String, String] with fn keys(self) {
  self.keys().to_array()
}

///|
/// Erased text-map propagator built from inject/extract callbacks.
///
/// Use this as the common type for concrete propagators and custom propagation
/// formats.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-propagator
pub struct TextMapPropagator {
  inject_fn : (@context.Context, &Injector) -> Unit
  extract_fn : (@context.Context, &Extractor) -> @context.Context
  fields_fn : () -> Array[String]
}

///|
/// W3C baggage propagator wrapper.
///
/// Reads and writes the `baggage` header.
/// Spec: https://opentelemetry.io/docs/specs/otel/baggage/api/#propagation
pub struct BaggagePropagator {
  inner : TextMapPropagator
}

///|
/// W3C trace-context propagator wrapper.
///
/// Reads and writes `traceparent` and `tracestate`.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#w3c-trace-context-requirements
pub struct TraceContextPropagator {
  inner : TextMapPropagator
}

///|
/// Composite propagator that applies multiple propagators in sequence.
///
/// The default global propagator uses this to combine trace context and
/// baggage.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
pub struct TextMapCompositePropagator {
  inner : TextMapPropagator
}

///|
/// Creates a custom text-map propagator from inject/extract/fields callbacks.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-propagator
pub fn TextMapPropagator::from_functions(
  inject_fn : (@context.Context, &Injector) -> Unit,
  extract_fn : (@context.Context, &Extractor) -> @context.Context,
  fields_fn : () -> Array[String],
) -> TextMapPropagator {
  { inject_fn, extract_fn, fields_fn }
}

///|
/// Injects values from `context` into the supplied carrier.
///
/// Call this before making an outbound request so downstream services can join
/// the same trace and receive baggage.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-inject
pub fn TextMapPropagator::inject_context(
  self : TextMapPropagator,
  context : @context.Context,
  injector : &Injector,
) -> Unit {
  (self.inject_fn)(context, injector)
}

///|
/// Extracts values from the carrier and returns an updated context.
///
/// Call this when handling an inbound request, then use the returned context as
/// the parent context for server spans and downstream work.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-extract
pub fn TextMapPropagator::extract_with_context(
  self : TextMapPropagator,
  context : @context.Context,
  extractor : &Extractor,
) -> @context.Context {
  (self.extract_fn)(context, extractor)
}

///|
/// Returns the header names this propagator may read or write.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#fields
pub fn TextMapPropagator::fields(self : TextMapPropagator) -> Array[String] {
  (self.fields_fn)()
}

///|
fn hex_digit(value : Int) -> String {
  if value < 10 {
    value.to_string()
  } else {
    (value - 10 + 'A'.to_int()).unsafe_to_char().to_string()
  }
}

///|
fn write_percent_encoded(builder : StringBuilder, ch : Char) -> Unit {
  let value = ch.to_int()
  builder.write_char('%')
  builder.write_string(hex_digit(value / 16))
  builder.write_string(hex_digit(value % 16))
}

///|
fn is_unreserved_baggage_char(ch : Char) -> Bool {
  (ch >= 'a' && ch <= 'z') ||
  (ch >= 'A' && ch <= 'Z') ||
  (ch >= '0' && ch <= '9') ||
  ch == '-' ||
  ch == '.' ||
  ch == '_' ||
  ch == '~'
}

///|
fn encode_baggage_component(value : StringView) -> String {
  let builder = StringBuilder::new()
  for _, ch in value {
    if is_unreserved_baggage_char(ch) {
      builder.write_char(ch)
    } else if ch.to_int() <= 127 {
      write_percent_encoded(builder, ch)
    } else {
      builder.write_char(ch)
    }
  }
  builder.to_string()
}

///|
fn is_hex_digit(value : StringView) -> Bool {
  value.length() == 1 &&
  (
    (value >= "0" && value <= "9") ||
    (value >= "a" && value <= "f") ||
    (value >= "A" && value <= "F")
  )
}

///|
fn decode_baggage_component(value : StringView) -> String {
  let builder = StringBuilder::new()
  let mut index = 0
  while index < value.length() {
    let ch = value[index:index + 1]
    if ch == "%" &&
      index + 2 < value.length() &&
      is_hex_digit(value[index + 1:index + 2]) &&
      is_hex_digit(value[index + 2:index + 3]) {
      let part = value[index + 1:index + 3].to_owned()
      let decoded = @string.parse_int(part, base=16) catch { _ => -1 }
      if decoded >= 0 && decoded < 128 {
        match decoded.to_char() {
          Some(decoded_char) => builder.write_char(decoded_char)
          None => builder.write_string(value[index:index + 3].to_owned())
        }
      } else {
        builder.write_string(value[index:index + 3].to_owned())
      }
      index += 3
    } else {
      builder.write_string(ch.to_owned())
      index += 1
    }
  }
  builder.to_string()
}

///|
fn encode_baggage(baggage : @baggage.Baggage) -> String? {
  if baggage.is_empty() {
    return None
  }
  let builder = StringBuilder::new()
  let mut is_first = true
  for entry in baggage.entries() {
    let key = entry.key.as_string().trim()
    if key == "" {
      continue
    }
    if !is_first {
      builder.write_char(',')
    }
    is_first = false
    builder.write_string(encode_baggage_component(key))
    builder.write_char('=')
    builder.write_string(encode_baggage_component(entry.value.trim()))
    let metadata = entry.metadata.as_string()
    if metadata != "" {
      builder.write_char(';')
      builder.write_string(metadata)
    }
  }
  Some(builder.to_string())
}

///|
fn decode_baggage(header : StringView) -> @baggage.Baggage {
  let mut baggage : @baggage.Baggage = Default::default()
  for item in header.split(",") {
    let item = item.trim()
    if item.is_empty() {
      continue
    }
    let baggage_item = match item.find(";") {
      Some(index) => (item[:index], item[index + 1:])
      None => (item, "")
    }
    guard baggage_item.0.find("=") is Some(index)
    let key = decode_baggage_component(baggage_item.0[:index].trim())
      .trim()
      .to_owned()
    let value = decode_baggage_component(baggage_item.0[index + 1:].trim())
      .trim()
      .to_owned()
    baggage = baggage.insert_with_metadata(
      key,
      value,
      @baggage.BaggageMetadata::new(baggage_item.1),
    )
  }
  baggage
}

///|
fn is_lower_hex(value : StringView) -> Bool {
  for _, ch in value {
    if !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')) {
      return false
    }
  }
  true
}

///|
fn masked_trace_flags(trace_flags : @common.TraceFlags) -> @common.TraceFlags {
  let masked : @common.TraceFlags = Default::default()
  masked.with_sampled(trace_flags.is_sampled())
}

///|
fn extract_span_context(extractor : &Extractor) -> @common.SpanContext? {
  guard extractor.get("traceparent") is Some(traceparent) else { return None }
  let traceparent = traceparent.trim()
  let parts = traceparent.split("-").to_array()
  if parts.length() < 4 {
    return None
  }
  let version = parts[0]
  if version.length() != 2 || !is_lower_hex(version) {
    return None
  }
  let version = @string.parse_int(version.to_owned(), base=16) catch {
    _ => return None
  }
  if version > 254 || (version == 0 && parts.length() != 4) {
    return None
  }
  let trace_id_part = parts[1]
  let span_id_part = parts[2]
  let trace_flags_part = parts[3]
  if trace_id_part.length() != 32 ||
    span_id_part.length() != 16 ||
    trace_flags_part.length() != 2 ||
    !is_lower_hex(trace_id_part) ||
    !is_lower_hex(span_id_part) ||
    !is_lower_hex(trace_flags_part) {
    return None
  }
  guard @common.TraceId::from_hex(trace_id_part) is Some(trace_id) else {
    return None
  }
  guard @common.SpanId::from_hex(span_id_part) is Some(span_id) else {
    return None
  }
  guard @common.TraceFlags::from_hex(trace_flags_part) is Some(trace_flags) else {
    return None
  }
  let trace_state = match extractor.get("tracestate") {
    Some(value) => @common.TraceState::from_header(value)
    None => Default::default()
  }
  let span_context = @common.SpanContext::new(
    trace_id,
    span_id,
    trace_flags=masked_trace_flags(trace_flags),
    is_remote=true,
    trace_state~,
  )
  if !span_context.is_valid() {
    None
  } else {
    Some(span_context)
  }
}

///|
/// Creates a baggage propagator that reads and writes the `baggage` header.
///
/// Extraction is forgiving: malformed entries are ignored and valid entries are
/// merged into the provided context.
/// Spec: https://opentelemetry.io/docs/specs/otel/baggage/api/#propagation
pub fn BaggagePropagator::new() -> BaggagePropagator {
  {
    inner: TextMapPropagator::from_functions(
      (context, injector) => {
        if encode_baggage(context.baggage()) is Some(header) {
          injector.set("baggage", header)
        }
      },
      (context, extractor) => {
        if extractor.get("baggage") is Some(header) {
          context.with_baggage(
            merge_baggage(context.baggage(), decode_baggage(header)),
          )
        } else {
          context
        }
      },
      () => ["baggage"],
    ),
  }
}

///|
/// Injects baggage from `context` into the carrier.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-inject
pub fn BaggagePropagator::inject_context(
  self : BaggagePropagator,
  context : @context.Context,
  injector : &Injector,
) -> Unit {
  self.inner.inject_context(context, injector)
}

///|
/// Extracts baggage from the carrier and returns an updated context.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-extract
pub fn BaggagePropagator::extract_with_context(
  self : BaggagePropagator,
  context : @context.Context,
  extractor : &Extractor,
) -> @context.Context {
  self.inner.extract_with_context(context, extractor)
}

///|
/// Erases the concrete baggage propagator type into `TextMapPropagator`.
/// Spec: https://opentelemetry.io/docs/specs/otel/baggage/api/#propagation
pub fn BaggagePropagator::into_text_map(
  self : BaggagePropagator,
) -> TextMapPropagator {
  self.inner
}

///|
/// Creates a trace-context propagator that reads and writes `traceparent` and
/// `tracestate`.
///
/// Invalid headers are ignored and leave the incoming context unchanged.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#w3c-trace-context-requirements
pub fn TraceContextPropagator::new() -> TraceContextPropagator {
  {
    inner: TextMapPropagator::from_functions(
      (context, injector) => {
        match context.span_context() {
          Some(span_context) if span_context.is_valid() => {
            injector.set(
              "traceparent",
              "00-\{span_context.trace_id().to_hex()}-\{span_context.span_id().to_hex()}-\{span_context.trace_flags().to_hex()}",
            )
            if span_context.trace_state().to_header() is Some(tracestate) {
              injector.set("tracestate", tracestate)
            }
          }
          _ => ()
        }
      },
      (context, extractor) => {
        match extract_span_context(extractor) {
          Some(span_context) => context.with_remote_span_context(span_context)
          None => context
        }
      },
      () => ["traceparent", "tracestate"],
    ),
  }
}

///|
/// Injects the current span context into the carrier when it is valid.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-inject
pub fn TraceContextPropagator::inject_context(
  self : TraceContextPropagator,
  context : @context.Context,
  injector : &Injector,
) -> Unit {
  self.inner.inject_context(context, injector)
}

///|
/// Extracts a remote span context from the carrier when the headers are valid.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#textmap-extract
pub fn TraceContextPropagator::extract_with_context(
  self : TraceContextPropagator,
  context : @context.Context,
  extractor : &Extractor,
) -> @context.Context {
  self.inner.extract_with_context(context, extractor)
}

///|
/// Erases the concrete trace-context propagator type into `TextMapPropagator`.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#w3c-trace-context-requirements
pub fn TraceContextPropagator::into_text_map(
  self : TraceContextPropagator,
) -> TextMapPropagator {
  self.inner
}

///|
/// Creates a propagator that runs multiple propagators in order.
///
/// Injection runs every propagator. Extraction threads the context through the
/// propagators from left to right. Reported fields are de-duplicated.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
pub fn TextMapCompositePropagator::new(
  propagators : ArrayView[TextMapPropagator],
) -> TextMapCompositePropagator {
  let propagators = propagators.to_owned()
  {
    inner: TextMapPropagator::from_functions(
      (context, injector) => {
        for propagator in propagators {
          propagator.inject_context(context, injector)
        }
      },
      (context, extractor) => {
        let mut current = context
        for propagator in propagators {
          current = propagator.extract_with_context(current, extractor)
        }
        current
      },
      () => {
        let fields : Map[String, Unit] = {}
        for propagator in propagators {
          for field in propagator.fields() {
            fields[field] = ()
          }
        }
        fields.keys().to_array()
      },
    ),
  }
}

///|
/// Injects using every child propagator in order.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
pub fn TextMapCompositePropagator::inject_context(
  self : TextMapCompositePropagator,
  context : @context.Context,
  injector : &Injector,
) -> Unit {
  self.inner.inject_context(context, injector)
}

///|
/// Extracts using every child propagator in order.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
pub fn TextMapCompositePropagator::extract_with_context(
  self : TextMapCompositePropagator,
  context : @context.Context,
  extractor : &Extractor,
) -> @context.Context {
  self.inner.extract_with_context(context, extractor)
}

///|
/// Erases the concrete composite propagator type into `TextMapPropagator`.
/// Spec: https://opentelemetry.io/docs/specs/otel/context/api-propagators/#composite-propagator
pub fn TextMapCompositePropagator::into_text_map(
  self : TextMapCompositePropagator,
) -> TextMapPropagator {
  self.inner
}