package tracing

import (
	"context"
	"encoding/base64"
	"errors"
	"sync"

	coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
	tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
	"google.golang.org/protobuf/proto"
)

const (
	MaxTraceAmount = 20
)

var (
	errNoTraces   = errors.New("no traces recorded to be exported")
	errNoopTracer = errors.New("noop tracer has no traces")
)

type InMemoryClient interface {
	// Spans returns a copy of the list of in-memory stored spans as a base64
	// encoded otlp protobuf string.
	Spans() (string, error)
	// ExportProtoSpans returns a copy of the list of in-memory stored spans as otlp
	// protobuf byte array and clears the in-memory spans.
	ExportProtoSpans() ([]byte, error)
}

// InMemoryOtlpClient is a client implementation for otlptrace.Client
type InMemoryOtlpClient struct {
	mu    sync.Mutex
	spans []*tracepb.ResourceSpans
}

func (mc *InMemoryOtlpClient) Start(_ context.Context) error {
	return nil
}

func (mc *InMemoryOtlpClient) Stop(_ context.Context) error {
	return nil
}

// UploadTraces adds the provided list of spans to the in-memory list.
func (mc *InMemoryOtlpClient) UploadTraces(_ context.Context, protoSpans []*tracepb.ResourceSpans) error {
	mc.mu.Lock()
	defer mc.mu.Unlock()
	// Catch to make sure too many traces aren't being added to response header.
	// Returning nil makes sure we don't fail to send the traces we already recorded.
	if len(mc.spans)+len(protoSpans) > MaxTraceAmount {
		return nil
	}
	mc.spans = append(mc.spans, protoSpans...)
	return nil
}

// Spans returns the list of in-memory stored spans as a base64 encoded otlp protobuf string.
func (mc *InMemoryOtlpClient) Spans() (string, error) {
	data, err := mc.ExportProtoSpans()
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(data), nil
}

// ProtoSpans returns the list of in-memory stored spans as the protobuf byte array.
func (mc *InMemoryOtlpClient) ExportProtoSpans() ([]byte, error) {
	mc.mu.Lock()
	defer mc.mu.Unlock()
	if len(mc.spans) <= 0 {
		return nil, errNoTraces
	}
	pbRequest := &coltracepb.ExportTraceServiceRequest{
		ResourceSpans: mc.spans,
	}
	serializedSpans, err := proto.Marshal(pbRequest)
	if err != nil {
		return nil, err
	}
	mc.spans = make([]*tracepb.ResourceSpans, 0)
	return serializedSpans, nil
}

// NoopOtlpClient is a client implementation for otlptrace.Client that does nothing
type NoopOtlpClient struct{}

func (mc *NoopOtlpClient) Start(_ context.Context) error {
	return nil
}

func (mc *NoopOtlpClient) Stop(_ context.Context) error {
	return nil
}

func (mc *NoopOtlpClient) UploadTraces(_ context.Context, _ []*tracepb.ResourceSpans) error {
	return nil
}

// Spans always returns no traces error
func (mc *NoopOtlpClient) Spans() (string, error) {
	return "", errNoopTracer
}

// Spans always returns no traces error
func (mc *NoopOtlpClient) ExportProtoSpans() ([]byte, error) {
	return nil, errNoopTracer
}

func (mc *NoopOtlpClient) ClearSpans() {}