//! W3C Trace Context propagation (plan §10.2; bead P3-6 / oracle-qmwz.4.6,
//! sub-feature 3). Parse the inbound `traceparent` (+ `tracestate`) headers and
//! produce a child context so the server participates in a distributed trace
//! (the call shows up under the agent's span). Pure parsing/formatting per the
//! W3C Trace Context spec; the transport reads/writes the headers.

/// A parsed W3C `traceparent`: `version "-" trace-id "-" parent-id "-" flags`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TraceContext {
    /// 32-hex-char trace id.
    pub trace_id: String,
    /// 16-hex-char parent (span) id.
    pub parent_id: String,
    /// 2-hex-char trace flags (`01` = sampled).
    pub flags: String,
    /// Opaque vendor `tracestate`, if present.
    pub trace_state: Option<String>,
}

fn is_hex(s: &str, len: usize) -> bool {
    s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit())
}

impl TraceContext {
    /// Parse a `traceparent` header value (and optional `tracestate`). Returns
    /// `None` on a malformed header (the caller then starts a fresh trace).
    #[must_use]
    pub fn parse(traceparent: &str, tracestate: Option<&str>) -> Option<TraceContext> {
        let parts: Vec<&str> = traceparent.trim().split('-').collect();
        let [version, trace_id, parent_id, flags] = parts.as_slice() else {
            return None;
        };
        // version 00; trace-id 16 bytes (not all-zero); parent-id 8 bytes (not
        // all-zero); flags 1 byte.
        if !is_hex(version, 2)
            || !is_hex(trace_id, 32)
            || !is_hex(parent_id, 16)
            || !is_hex(flags, 2)
            || trace_id.bytes().all(|b| b == b'0')
            || parent_id.bytes().all(|b| b == b'0')
        {
            return None;
        }
        Some(TraceContext {
            trace_id: trace_id.to_ascii_lowercase(),
            parent_id: parent_id.to_ascii_lowercase(),
            flags: flags.to_ascii_lowercase(),
            trace_state: tracestate
                .map(|s| s.trim().to_owned())
                .filter(|s| !s.is_empty()),
        })
    }

    /// Whether this trace is sampled (low bit of the flags set).
    #[must_use]
    pub fn sampled(&self) -> bool {
        u8::from_str_radix(&self.flags, 16)
            .map(|f| f & 0x01 == 0x01)
            .unwrap_or(false)
    }

    /// Produce the outbound `traceparent` for a child span generated server-side.
    /// `child_span_id` must be a fresh 16-hex-char span id (generated by the
    /// caller's tracer); the trace id + flags are preserved (same trace).
    #[must_use]
    pub fn to_child_traceparent(&self, child_span_id: &str) -> String {
        format!("00-{}-{}-{}", self.trace_id, child_span_id, self.flags)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const TP: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";

    #[test]
    fn parses_a_valid_traceparent() {
        let tc = TraceContext::parse(TP, Some("vendor=abc")).expect("valid");
        assert_eq!(tc.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736");
        assert_eq!(tc.parent_id, "00f067aa0ba902b7");
        assert_eq!(tc.flags, "01");
        assert!(tc.sampled());
        assert_eq!(tc.trace_state.as_deref(), Some("vendor=abc"));
    }

    #[test]
    fn rejects_malformed_headers() {
        assert!(TraceContext::parse("garbage", None).is_none());
        assert!(TraceContext::parse("00-tooShort-00f067aa0ba902b7-01", None).is_none());
        // All-zero trace id is invalid per the spec.
        assert!(
            TraceContext::parse(
                "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
                None
            )
            .is_none()
        );
        // All-zero parent id is invalid.
        assert!(
            TraceContext::parse(
                "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
                None
            )
            .is_none()
        );
    }

    #[test]
    fn unsampled_flag_detected() {
        let tc = TraceContext::parse(
            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
            None,
        )
        .unwrap();
        assert!(!tc.sampled());
    }

    #[test]
    fn child_traceparent_preserves_trace_and_flags() {
        let tc = TraceContext::parse(TP, None).unwrap();
        let child = tc.to_child_traceparent("b9c7c989f97918e1");
        assert_eq!(
            child,
            "00-4bf92f3577b34da6a3ce929d0e0e4736-b9c7c989f97918e1-01"
        );
        // The child is itself a valid traceparent.
        assert!(TraceContext::parse(&child, None).is_some());
    }
}
