package detect

import (
	"context"
	"os"
	"slices"
	"strconv"

	"github.com/pkg/errors"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/metric/metricdata"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type ExporterDetector interface {
	DetectTraceExporter() (sdktrace.SpanExporter, error)
	DetectMetricExporter() (sdkmetric.Exporter, error)
}

type detector struct {
	f        ExporterDetector
	priority int
}

var detectors map[string]detector

func Register(name string, exp ExporterDetector, priority int) {
	if detectors == nil {
		detectors = map[string]detector{}
	}
	detectors[name] = detector{
		f:        exp,
		priority: priority,
	}
}

type TraceExporterDetector func() (sdktrace.SpanExporter, error)

func (fn TraceExporterDetector) DetectTraceExporter() (sdktrace.SpanExporter, error) {
	return fn()
}

func (fn TraceExporterDetector) DetectMetricExporter() (sdkmetric.Exporter, error) {
	return nil, nil
}

func detectExporter[T any](envVar string, fn func(d ExporterDetector) (T, bool, error)) (exp T, err error) {
	ignoreErrors, _ := strconv.ParseBool("OTEL_IGNORE_ERROR")

	if n := os.Getenv(envVar); n != "" {
		d, ok := detectors[n]
		if !ok {
			if !ignoreErrors {
				err = errors.Errorf("unsupported opentelemetry exporter %v", n)
			}
			return exp, err
		}
		exp, _, err = fn(d.f)
		if err != nil && ignoreErrors {
			err = nil
		}
		return exp, err
	}

	arr := make([]detector, 0, len(detectors))
	for _, d := range detectors {
		arr = append(arr, d)
	}
	slices.SortFunc(arr, func(a, b detector) int {
		return a.priority - b.priority
	})

	var ok bool
	for _, d := range arr {
		exp, ok, err = fn(d.f)
		if err != nil && !ignoreErrors {
			return exp, err
		}

		if ok {
			break
		}
	}
	return exp, nil
}

func NewSpanExporter(_ context.Context) (sdktrace.SpanExporter, error) {
	return detectExporter("OTEL_TRACES_EXPORTER", func(d ExporterDetector) (sdktrace.SpanExporter, bool, error) {
		exp, err := d.DetectTraceExporter()
		return exp, exp != nil, err
	})
}

func NewMetricExporter(_ context.Context) (sdkmetric.Exporter, error) {
	return detectExporter("OTEL_METRICS_EXPORTER", func(d ExporterDetector) (sdkmetric.Exporter, bool, error) {
		exp, err := d.DetectMetricExporter()
		return exp, exp != nil, err
	})
}

type noneDetector struct{}

func (n noneDetector) DetectTraceExporter() (sdktrace.SpanExporter, error) {
	return noneSpanExporter{}, nil
}

func (n noneDetector) DetectMetricExporter() (sdkmetric.Exporter, error) {
	return noneMetricExporter{}, nil
}

type noneSpanExporter struct{}

func (n noneSpanExporter) ExportSpans(_ context.Context, _ []sdktrace.ReadOnlySpan) error {
	return nil
}

func (n noneSpanExporter) Shutdown(_ context.Context) error {
	return nil
}

func IsNoneSpanExporter(exp sdktrace.SpanExporter) bool {
	_, ok := exp.(noneSpanExporter)
	return ok
}

type noneMetricExporter struct{}

func (n noneMetricExporter) Temporality(kind sdkmetric.InstrumentKind) metricdata.Temporality {
	return sdkmetric.DefaultTemporalitySelector(kind)
}

func (n noneMetricExporter) Aggregation(kind sdkmetric.InstrumentKind) sdkmetric.Aggregation {
	return sdkmetric.DefaultAggregationSelector(kind)
}

func (n noneMetricExporter) Export(_ context.Context, _ *metricdata.ResourceMetrics) error {
	return nil
}

func (n noneMetricExporter) ForceFlush(_ context.Context) error {
	return nil
}

func (n noneMetricExporter) Shutdown(_ context.Context) error {
	return nil
}

func IsNoneMetricExporter(exp sdkmetric.Exporter) bool {
	_, ok := exp.(noneMetricExporter)
	return ok
}

func init() {
	// Register a none detector. This will never be chosen if there's another suitable
	// exporter that can be detected, but exists to allow telemetry to be explicitly
	// disabled.
	Register("none", noneDetector{}, 1000)
}
