// Copyright 2022 The OpenZipkin Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package b3

import (
	"strconv"
	"strings"

	"github.com/openzipkin/zipkin-go/model"
)

// ParseHeaders takes values found from B3 Headers and tries to reconstruct a
// SpanContext.
func ParseHeaders(
	hdrTraceID, hdrSpanID, hdrParentSpanID, hdrSampled, hdrFlags string,
) (*model.SpanContext, error) {
	var (
		err           error
		spanID        uint64
		requiredCount int
		sc            = &model.SpanContext{}
	)

	// correct values for an existing sampled header are "0" and "1".
	// For legacy support and  being lenient to other tracing implementations we
	// allow "true" and "false" as inputs for interop purposes.
	switch strings.ToLower(hdrSampled) {
	case "0", "false":
		sampled := false
		sc.Sampled = &sampled
	case "1", "true":
		sampled := true
		sc.Sampled = &sampled
	case "":
		// sc.Sampled = nil
	default:
		return nil, ErrInvalidSampledHeader
	}

	// The only accepted value for Flags is "1". This will set Debug to true. All
	// other values and omission of header will be ignored.
	if hdrFlags == "1" {
		sc.Debug = true
		sc.Sampled = nil
	}

	if hdrTraceID != "" {
		requiredCount++
		if sc.TraceID, err = model.TraceIDFromHex(hdrTraceID); err != nil {
			return nil, ErrInvalidTraceIDHeader
		}
	}

	if hdrSpanID != "" {
		requiredCount++
		if spanID, err = strconv.ParseUint(hdrSpanID, 16, 64); err != nil {
			return nil, ErrInvalidSpanIDHeader
		}
		sc.ID = model.ID(spanID)
	}

	if requiredCount != 0 && requiredCount != 2 {
		return nil, ErrInvalidScope
	}

	if hdrParentSpanID != "" {
		if requiredCount == 0 {
			return nil, ErrInvalidScopeParent
		}
		if spanID, err = strconv.ParseUint(hdrParentSpanID, 16, 64); err != nil {
			return nil, ErrInvalidParentSpanIDHeader
		}
		parentSpanID := model.ID(spanID)
		sc.ParentID = &parentSpanID
	}

	return sc, nil
}

// ParseSingleHeader takes values found from B3 Single Header and tries to reconstruct a
// SpanContext.
func ParseSingleHeader(contextHeader string) (*model.SpanContext, error) {
	if contextHeader == "" {
		return nil, ErrEmptyContext
	}

	var (
		sc       = model.SpanContext{}
		sampling string
	)

	headerLen := len(contextHeader)

	if headerLen == 1 {
		sampling = contextHeader
	} else if headerLen == 16 || headerLen == 32 {
		return nil, ErrInvalidScope
	} else if headerLen >= 16+16+1 {
		var high, low uint64
		pos := 0
		if string(contextHeader[16]) != "-" {
			// traceID must be 128 bits
			var err error
			high, err = strconv.ParseUint(contextHeader[0:16], 16, 64)
			if err != nil {
				return nil, ErrInvalidTraceIDValue
			}
			pos = 16
		}

		low, err := strconv.ParseUint(contextHeader[pos:pos+16], 16, 64)
		if err != nil {
			return nil, ErrInvalidTraceIDValue
		}

		sc.TraceID = model.TraceID{High: high, Low: low}

		rawID, err := strconv.ParseUint(contextHeader[pos+16+1:pos+16+1+16], 16, 64)
		if err != nil {
			return nil, ErrInvalidSpanIDValue
		}

		sc.ID = model.ID(rawID)

		if headerLen > pos+16+1+16 {
			if headerLen == pos+16+1+16+1 {
				return nil, ErrInvalidSampledByte
			}

			if headerLen == pos+16+1+16+1+1 {
				sampling = string(contextHeader[pos+16+1+16+1])
			} else if headerLen == pos+16+1+16+1+16 {
				return nil, ErrInvalidScopeParentSingle
			} else if headerLen == pos+16+1+16+1+1+1+16 {
				sampling = string(contextHeader[pos+16+1+16+1])

				var rawParentID uint64
				rawParentID, err = strconv.ParseUint(contextHeader[pos+16+1+16+1+1+1:], 16, 64)
				if err != nil {
					return nil, ErrInvalidParentSpanIDValue
				}

				parentID := model.ID(rawParentID)
				sc.ParentID = &parentID
			} else {
				return nil, ErrInvalidParentSpanIDValue
			}
		}
	} else {
		return nil, ErrInvalidTraceIDValue
	}
	switch sampling {
	case "d":
		sc.Debug = true
	case "1":
		trueVal := true
		sc.Sampled = &trueVal
	case "0":
		falseVal := false
		sc.Sampled = &falseVal
	case "":
	default:
		return nil, ErrInvalidSampledByte
	}

	return &sc, nil
}

// BuildSingleHeader takes the values from the SpanContext and builds the B3 header
func BuildSingleHeader(sc model.SpanContext) string {
	var header []string
	if !sc.TraceID.Empty() && sc.ID > 0 {
		header = append(header, sc.TraceID.String(), sc.ID.String())
	}

	if sc.Debug {
		header = append(header, "d")
	} else if sc.Sampled != nil {
		if *sc.Sampled {
			header = append(header, "1")
		} else {
			header = append(header, "0")
		}
	}

	if sc.ParentID != nil {
		header = append(header, sc.ParentID.String())
	}

	return strings.Join(header, "-")
}
