package telemetry

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"time"

	"github.com/charmingruby/homunc/apps/fraud/config"
	"go.opentelemetry.io/contrib/bridges/otelslog"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/log/global"
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/propagation"
	sdklog "go.opentelemetry.io/otel/sdk/log"
	sdkmeter "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
	"go.opentelemetry.io/otel/trace"
)

type Logger = slog.Logger

var (
	Tracer      trace.Tracer
	Meter       metric.Meter
	Log         = newBootstrapLogger()
	fallbackLog *slog.Logger
	otelLog     *slog.Logger
)

func New(ctx context.Context, cfg config.Config) (func(context.Context) error, error) {
	return setupOTELSDK(ctx, cfg)
}

func UseFallbackLogger() {
	Log = fallbackLog
}

func setupOTELSDK(ctx context.Context, cfg config.Config) (shutdown func(context.Context) error, err error) {
	var shutdownFuncs []func(context.Context) error

	shutdown = func(ctx context.Context) error {
		var err error
		for _, fn := range shutdownFuncs {
			err = errors.Join(err, fn(ctx))
		}

		shutdownFuncs = nil

		return err
	}

	handleErr := func(inErr error) {
		err = errors.Join(inErr, shutdown(ctx))
	}

	res := resource.NewWithAttributes(
		semconv.SchemaURL,
		semconv.ServiceNameKey.String(cfg.ServiceName),
		semconv.ServiceVersionKey.String(cfg.ServiceVersion),
	)

	prop := newPropagator()
	otel.SetTextMapPropagator(prop)

	tracerProvider, err := newTraceProvider(res, cfg)
	if err != nil {
		handleErr(err)
		return
	}
	shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
	otel.SetTracerProvider(tracerProvider)

	meterProvider, err := newMeterProvider(res, cfg)
	if err != nil {
		handleErr(err)
		return
	}
	shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
	otel.SetMeterProvider(meterProvider)

	loggerProvider, err := newLoggerProvider(res, cfg)
	if err != nil {
		handleErr(err)
		return
	}
	shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
	global.SetLoggerProvider(loggerProvider)
	otelLog = otelslog.NewLogger(cfg.ServiceScope)
	Log = otelLog

	return
}

func newPropagator() propagation.TextMapPropagator {
	return propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	)
}

func newTraceProvider(resource *resource.Resource, cfg config.Config) (*sdktrace.TracerProvider, error) {
	traceExporter, err := otlptracegrpc.New(
		context.Background(),
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint(cfg.OTLPExporterEndpoint),
	)
	if err != nil {
		return nil, err
	}

	traceProvider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(
			traceExporter,
			sdktrace.WithBatchTimeout(time.Second),
		),
		sdktrace.WithResource(resource),
	)

	Tracer = otel.Tracer(cfg.ServiceScope)

	return traceProvider, nil
}

func newMeterProvider(resource *resource.Resource, cfg config.Config) (*sdkmeter.MeterProvider, error) {
	metricExporter, err := otlpmetricgrpc.New(
		context.Background(),
		otlpmetricgrpc.WithInsecure(),
		otlpmetricgrpc.WithEndpoint(cfg.OTLPExporterEndpoint),
	)
	if err != nil {
		return nil, err
	}
	meterProvider := sdkmeter.NewMeterProvider(
		sdkmeter.WithReader(
			sdkmeter.NewPeriodicReader(
				metricExporter,
				sdkmeter.WithInterval(3*time.Second),
			),
		),
		sdkmeter.WithResource(resource),
	)

	Meter = otel.Meter(cfg.ServiceScope)

	return meterProvider, nil
}

func newLoggerProvider(resource *resource.Resource, cfg config.Config) (*sdklog.LoggerProvider, error) {
	logExporter, err := otlploggrpc.New(
		context.Background(),
		otlploggrpc.WithInsecure(),
		otlploggrpc.WithEndpoint(cfg.OTLPExporterEndpoint),
	)
	if err != nil {
		return nil, err
	}

	loggerProvider := sdklog.NewLoggerProvider(
		sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
		sdklog.WithResource(resource),
	)

	return loggerProvider, nil
}

func newBootstrapLogger() *slog.Logger {
	fallbackLog = slog.New(slog.NewJSONHandler(os.Stdout, nil))
	return fallbackLog
}
