package obs

import (
	"errors"
	"strings"
)

// SpanContext is the propagated trace identity (W3C Trace Context).
type SpanContext struct {
	TraceID string // 32 hex chars
	SpanID  string // 16 hex chars
	Sampled bool
}

// ParseTraceparent parses a W3C `traceparent` header:
//
//	00-<32hex trace-id>-<16hex span-id>-<2hex flags>
func ParseTraceparent(s string) (SpanContext, error) {
	f := strings.Split(strings.TrimSpace(s), "-")
	if len(f) != 4 {
		return SpanContext{}, errors.New("obs: traceparent must have 4 fields")
	}
	if f[0] != "00" {
		return SpanContext{}, errors.New("obs: unsupported traceparent version")
	}
	if len(f[1]) != 32 || !isHex(f[1]) || allZero(f[1]) {
		return SpanContext{}, errors.New("obs: bad trace-id")
	}
	if len(f[2]) != 16 || !isHex(f[2]) || allZero(f[2]) {
		return SpanContext{}, errors.New("obs: bad span-id")
	}
	if len(f[3]) != 2 || !isHex(f[3]) {
		return SpanContext{}, errors.New("obs: bad flags")
	}
	sampled := (hexNibble(f[3][1]) & 0x1) != 0
	return SpanContext{TraceID: f[1], SpanID: f[2], Sampled: sampled}, nil
}

// Traceparent formats the SpanContext as a W3C traceparent header.
func (sc SpanContext) Traceparent() string {
	flags := "00"
	if sc.Sampled {
		flags = "01"
	}
	return "00-" + sc.TraceID + "-" + sc.SpanID + "-" + flags
}

// NewChild creates the child span the gateway emits for its proxy hop:
// SAME trace-id and sampling decision, a NEW span-id. This is what a
// proxy must do — take the baton, run its leg, pass it on. Dropping the
// trace-id (or starting a fresh trace) severs the downstream trace.
func NewChild(parent SpanContext, newSpanID string) SpanContext {
	return SpanContext{TraceID: parent.TraceID, SpanID: newSpanID, Sampled: parent.Sampled}
}

// Extract reads a traceparent from a header getter (e.g. http.Header.Get).
func Extract(get func(string) string) (SpanContext, bool) {
	sc, err := ParseTraceparent(get("traceparent"))
	if err != nil {
		return SpanContext{}, false
	}
	return sc, true
}

// Inject writes a traceparent via a header setter (e.g. http.Header.Set).
func Inject(set func(k, v string), sc SpanContext) {
	set("traceparent", sc.Traceparent())
}

func isHex(s string) bool {
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}

func allZero(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] != '0' {
			return false
		}
	}
	return true
}

func hexNibble(c byte) int {
	switch {
	case c >= '0' && c <= '9':
		return int(c - '0')
	case c >= 'a' && c <= 'f':
		return int(c-'a') + 10
	case c >= 'A' && c <= 'F':
		return int(c-'A') + 10
	}
	return 0
}
