package otelutil

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"time"

	"github.com/pkg/errors"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/sdk/instrumentation"
	"go.opentelemetry.io/otel/sdk/resource"
	tracesdk "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/trace"
)

// Span is a type similar to otel's SpanStub, but with the correct types needed
// for handle marshaling and unmarshalling.
type Span struct {
	// Name is the name of a specific span
	Name string
	// SpanContext is the unique SpanContext that identifies the span
	SpanContext trace.SpanContext
	// Parten is the unique SpanContext that identifies the parent of the span.
	// If the span has no parent, this span context will be invalid.
	Parent trace.SpanContext
	// SpanKind is the role the span plays in a Trace
	SpanKind trace.SpanKind
	// StartTime is the time the span started recording
	StartTime time.Time
	// EndTime returns the time the span stopped recording
	EndTime time.Time
	// Attributes are the defining attributes of a span
	Attributes []attribute.KeyValue
	// Events are all the events that occurred within the span
	Events []tracesdk.Event
	// Links are all the links the span has to other spans
	Links []tracesdk.Link
	// Status is that span status
	Status tracesdk.Status
	// DroppedAttributes is the number of attributes dropped by the span due to
	// a limit being reached
	DroppedAttributes int
	// DroppedEvents is the number of attributes dropped by the span due to a
	// limit being reached
	DroppedEvents int
	// DroppedLinks is the number of links dropped by the span due to a limit
	// being reached
	DroppedLinks int
	// ChildSpanCount is the count of spans that consider the span a direct
	// parent
	ChildSpanCount int
	// Resource is the information about the entity that produced the span
	// We have to change this type from the otel type to make this struct
	// marshallable
	Resource []attribute.KeyValue
	// InstrumentationLibrary is information about the library that produced
	// the span
	//nolint:staticcheck
	InstrumentationLibrary instrumentation.Library
}

type Spans []Span

// Len return the length of the Spans.
func (s Spans) Len() int {
	return len(s)
}

// ReadOnlySpans return a list of tracesdk.ReadOnlySpan from span stubs.
func (s Spans) ReadOnlySpans() []tracesdk.ReadOnlySpan {
	roSpans := make([]tracesdk.ReadOnlySpan, len(s))
	for i := range s {
		roSpans[i] = s[i].Snapshot()
	}
	return roSpans
}

// ParseSpanStubs parses BuildKit trace data into a list of SpanStubs.
func ParseSpanStubs(rdr io.Reader) (Spans, error) {
	var spanStubs []Span
	decoder := json.NewDecoder(rdr)
	for {
		var span Span
		if err := decoder.Decode(&span); err == io.EOF {
			break
		} else if err != nil {
			return nil, errors.Wrapf(err, "error decoding JSON")
		}
		spanStubs = append(spanStubs, span)
	}
	return spanStubs, nil
}

// spanData is data that we need to unmarshal in custom ways.
type spanData struct {
	Name              string
	SpanContext       spanContext
	Parent            spanContext
	SpanKind          trace.SpanKind
	StartTime         time.Time
	EndTime           time.Time
	Attributes        []keyValue
	Events            []event
	Links             []link
	Status            tracesdk.Status
	DroppedAttributes int
	DroppedEvents     int
	DroppedLinks      int
	ChildSpanCount    int
	Resource          []keyValue // change this type from the otel type to make this struct marshallable
	//nolint:staticcheck
	InstrumentationLibrary instrumentation.Library
}

// spanContext is a custom type used to unmarshal otel SpanContext correctly.
type spanContext struct {
	TraceID    string
	SpanID     string
	TraceFlags string
	TraceState string // TODO: implement, currently dropped
	Remote     bool
}

// event is a custom type used to unmarshal otel Event correctly.
type event struct {
	Name                  string
	Attributes            []keyValue
	DroppedAttributeCount int
	Time                  time.Time
}

// link is a custom type used to unmarshal otel Link correctly.
type link struct {
	SpanContext           spanContext
	Attributes            []keyValue
	DroppedAttributeCount int
}

// keyValue is a custom type used to unmarshal otel KeyValue correctly.
type keyValue struct {
	Key   string
	Value value
}

// value is a custom type used to unmarshal otel Value correctly.
type value struct {
	Type  string
	Value interface{}
}

// UnmarshalJSON implements json.Unmarshaler for Span which allows correctly
// retrieving attribute.KeyValue values.
func (s *Span) UnmarshalJSON(data []byte) error {
	var sd spanData
	if err := json.NewDecoder(bytes.NewReader(data)).Decode(&sd); err != nil {
		return errors.Wrap(err, "unable to decode to spanData")
	}

	s.Name = sd.Name
	s.SpanKind = sd.SpanKind
	s.StartTime = sd.StartTime
	s.EndTime = sd.EndTime
	s.Status = sd.Status
	s.DroppedAttributes = sd.DroppedAttributes
	s.DroppedEvents = sd.DroppedEvents
	s.DroppedLinks = sd.DroppedLinks
	s.ChildSpanCount = sd.ChildSpanCount
	s.InstrumentationLibrary = sd.InstrumentationLibrary

	spanCtx, err := sd.SpanContext.asTraceSpanContext()
	if err != nil {
		return errors.Wrap(err, "unable to decode spanCtx")
	}
	s.SpanContext = spanCtx

	parent, err := sd.Parent.asTraceSpanContext()
	if err != nil {
		return errors.Wrap(err, "unable to decode parent")
	}
	s.Parent = parent

	var attributes []attribute.KeyValue
	for _, a := range sd.Attributes {
		kv, err := a.asAttributeKeyValue()
		if err != nil {
			return errors.Wrapf(err, "unable to decode attribute (%s)", a.Key)
		}
		attributes = append(attributes, kv)
	}
	s.Attributes = attributes

	var events []tracesdk.Event
	for _, e := range sd.Events {
		var eventAttributes []attribute.KeyValue
		for _, a := range e.Attributes {
			kv, err := a.asAttributeKeyValue()
			if err != nil {
				return errors.Wrapf(err, "unable to decode event attribute (%s)", a.Key)
			}
			eventAttributes = append(eventAttributes, kv)
		}
		events = append(events, tracesdk.Event{
			Name:                  e.Name,
			Attributes:            eventAttributes,
			DroppedAttributeCount: e.DroppedAttributeCount,
			Time:                  e.Time,
		})
	}
	s.Events = events

	var links []tracesdk.Link
	for _, l := range sd.Links {
		linkSpanCtx, err := l.SpanContext.asTraceSpanContext()
		if err != nil {
			return errors.Wrap(err, "unable to decode linkSpanCtx")
		}
		var linkAttributes []attribute.KeyValue
		for _, a := range l.Attributes {
			kv, err := a.asAttributeKeyValue()
			if err != nil {
				return errors.Wrapf(err, "unable to decode link attribute (%s)", a.Key)
			}
			linkAttributes = append(linkAttributes, kv)
		}
		links = append(links, tracesdk.Link{
			SpanContext:           linkSpanCtx,
			Attributes:            linkAttributes,
			DroppedAttributeCount: l.DroppedAttributeCount,
		})
	}
	s.Links = links

	var resources []attribute.KeyValue
	for _, r := range sd.Resource {
		kv, err := r.asAttributeKeyValue()
		if err != nil {
			return errors.Wrapf(err, "unable to decode resource (%s)", r.Key)
		}
		resources = append(resources, kv)
	}
	s.Resource = resources

	return nil
}

// asTraceSpanContext converts the internal spanContext representation to an
// otel one.
func (sc *spanContext) asTraceSpanContext() (trace.SpanContext, error) {
	traceID, err := traceIDFromHex(sc.TraceID)
	if err != nil {
		return trace.SpanContext{}, errors.Wrap(err, "unable to parse trace id")
	}
	spanID, err := spanIDFromHex(sc.SpanID)
	if err != nil {
		return trace.SpanContext{}, errors.Wrap(err, "unable to parse span id")
	}
	traceFlags := trace.TraceFlags(0x00)
	if sc.TraceFlags == "01" {
		traceFlags = trace.TraceFlags(0x01)
	}
	config := trace.SpanContextConfig{
		TraceID:    traceID,
		SpanID:     spanID,
		TraceFlags: traceFlags,
		Remote:     sc.Remote,
	}
	return trace.NewSpanContext(config), nil
}

// asAttributeKeyValue converts the internal keyValue representation to an
// otel one.
func (kv *keyValue) asAttributeKeyValue() (attribute.KeyValue, error) {
	// value types get encoded as string
	switch kv.Value.Type {
	case attribute.INVALID.String():
		return attribute.KeyValue{}, errors.New("invalid value type")
	case attribute.BOOL.String():
		return attribute.Bool(kv.Key, kv.Value.Value.(bool)), nil
	case attribute.INT64.String():
		// value could be int64 or float64, so handle both cases (float64 comes
		// from json unmarshal)
		var v int64
		switch i := kv.Value.Value.(type) {
		case int64:
			v = i
		case float64:
			v = int64(i)
		}
		return attribute.Int64(kv.Key, v), nil
	case attribute.FLOAT64.String():
		return attribute.Float64(kv.Key, kv.Value.Value.(float64)), nil
	case attribute.STRING.String():
		return attribute.String(kv.Key, kv.Value.Value.(string)), nil
	case attribute.BOOLSLICE.String():
		return attribute.BoolSlice(kv.Key, kv.Value.Value.([]bool)), nil
	case attribute.INT64SLICE.String():
		// handle both float64 and int64 (float64 comes from json unmarshal)
		var v []int64
		switch sli := kv.Value.Value.(type) {
		case []int64:
			v = sli
		case []float64:
			for i := range sli {
				v = append(v, int64(sli[i]))
			}
		}
		return attribute.Int64Slice(kv.Key, v), nil
	case attribute.FLOAT64SLICE.String():
		return attribute.Float64Slice(kv.Key, kv.Value.Value.([]float64)), nil
	case attribute.STRINGSLICE.String():
		var strSli []string
		// sometimes we can get an []interface{} instead of a []string, so
		// always cast to []string if that happens.
		switch sli := kv.Value.Value.(type) {
		case []string:
			strSli = sli
		case []interface{}:
			for i := range sli {
				var v string
				// best case we have a string, otherwise, cast it using
				// fmt.Sprintf
				if str, ok := sli[i].(string); ok {
					v = str
				} else {
					v = fmt.Sprintf("%v", sli[i])
				}
				// add the string to the slice
				strSli = append(strSli, v)
			}
		default:
			return attribute.KeyValue{}, errors.Errorf("got unsupported type %q for %s", reflect.ValueOf(kv.Value.Value).Kind(), attribute.STRINGSLICE.String())
		}
		return attribute.StringSlice(kv.Key, strSli), nil
	default:
		return attribute.KeyValue{}, errors.Errorf("unknown value type %s", kv.Value.Type)
	}
}

// traceIDFromHex returns a TraceID from a hex string if it is compliant with
// the W3C trace-context specification and removes the validity check.
// https://www.w3.org/TR/trace-context/#trace-id
func traceIDFromHex(h string) (trace.TraceID, error) {
	t := trace.TraceID{}
	if len(h) != 32 {
		return t, errors.New("unable to parse trace id")
	}
	if err := decodeHex(h, t[:]); err != nil {
		return t, err
	}
	return t, nil
}

// spanIDFromHex returns a SpanID from a hex string if it is compliant with the
// W3C trace-context specification and removes the validity check.
// https://www.w3.org/TR/trace-context/#parent-id
func spanIDFromHex(h string) (trace.SpanID, error) {
	s := trace.SpanID{}
	if len(h) != 16 {
		return s, errors.New("unable to parse span id of length: %d")
	}
	if err := decodeHex(h, s[:]); err != nil {
		return s, err
	}
	return s, nil
}

// decodeHex decodes hex in a manner compliant with otel.
func decodeHex(h string, b []byte) error {
	for _, r := range h {
		switch {
		case 'a' <= r && r <= 'f':
			continue
		case '0' <= r && r <= '9':
			continue
		default:
			return errors.New("unable to parse hex id")
		}
	}
	decoded, err := hex.DecodeString(h)
	if err != nil {
		return err
	}
	copy(b, decoded)
	return nil
}

// Snapshot turns a Span into a ReadOnlySpan which is exportable by otel.
func (s *Span) Snapshot() tracesdk.ReadOnlySpan {
	return spanSnapshot{
		name:                 s.Name,
		spanContext:          s.SpanContext,
		parent:               s.Parent,
		spanKind:             s.SpanKind,
		startTime:            s.StartTime,
		endTime:              s.EndTime,
		attributes:           s.Attributes,
		events:               s.Events,
		links:                s.Links,
		status:               s.Status,
		droppedAttributes:    s.DroppedAttributes,
		droppedEvents:        s.DroppedEvents,
		droppedLinks:         s.DroppedLinks,
		childSpanCount:       s.ChildSpanCount,
		resource:             resource.NewSchemaless(s.Resource...),
		instrumentationScope: s.InstrumentationLibrary,
	}
}

// spanSnapshot is a helper type for transforming a Span into a ReadOnlySpan.
type spanSnapshot struct {
	// Embed the interface to implement the private method.
	tracesdk.ReadOnlySpan

	name                 string
	spanContext          trace.SpanContext
	parent               trace.SpanContext
	spanKind             trace.SpanKind
	startTime            time.Time
	endTime              time.Time
	attributes           []attribute.KeyValue
	events               []tracesdk.Event
	links                []tracesdk.Link
	status               tracesdk.Status
	droppedAttributes    int
	droppedEvents        int
	droppedLinks         int
	childSpanCount       int
	resource             *resource.Resource
	instrumentationScope instrumentation.Scope
}

// Name returns the Name of the snapshot
func (s spanSnapshot) Name() string { return s.name }

// SpanContext returns the SpanContext of the snapshot
func (s spanSnapshot) SpanContext() trace.SpanContext { return s.spanContext }

// Parent returns the Parent of the snapshot
func (s spanSnapshot) Parent() trace.SpanContext { return s.parent }

// SpanKind returns the SpanKind of the snapshot
func (s spanSnapshot) SpanKind() trace.SpanKind { return s.spanKind }

// StartTime returns the StartTime of the snapshot
func (s spanSnapshot) StartTime() time.Time { return s.startTime }

// EndTime returns the EndTime of the snapshot
func (s spanSnapshot) EndTime() time.Time { return s.endTime }

// Attributes returns the Attributes of the snapshot
func (s spanSnapshot) Attributes() []attribute.KeyValue { return s.attributes }

// Links returns the Links of the snapshot
func (s spanSnapshot) Links() []tracesdk.Link { return s.links }

// Events return the Events of the snapshot
func (s spanSnapshot) Events() []tracesdk.Event { return s.events }

// Status returns the Status of the snapshot
func (s spanSnapshot) Status() tracesdk.Status { return s.status }

// DroppedAttributes returns the DroppedAttributes of the snapshot
func (s spanSnapshot) DroppedAttributes() int { return s.droppedAttributes }

// DroppedLinks returns the DroppedLinks of the snapshot
func (s spanSnapshot) DroppedLinks() int { return s.droppedLinks }

// DroppedEvents returns the DroppedEvents of the snapshot
func (s spanSnapshot) DroppedEvents() int { return s.droppedEvents }

// ChildSpanCount returns the ChildSpanCount of the snapshot
func (s spanSnapshot) ChildSpanCount() int { return s.childSpanCount }

// Resource returns the Resource of the snapshot
func (s spanSnapshot) Resource() *resource.Resource { return s.resource }

// InstrumentationScope returns the InstrumentationScope of the snapshot
func (s spanSnapshot) InstrumentationScope() instrumentation.Scope {
	return s.instrumentationScope
}

// InstrumentationLibrary returns the InstrumentationLibrary of the snapshot
//
//nolint:staticcheck
func (s spanSnapshot) InstrumentationLibrary() instrumentation.Library {
	return s.instrumentationScope
}
