package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/propagation"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/metric/metricdata"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/sdk/trace/tracetest"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"
)

type report struct {
	Propagated                                              bool
	NoSDKRecording, ExtractedRemote, SameTrace, ChildParent bool
	UnsampledRecording, CanceledContext, SpanStillRecording bool
	AfterEndChild, AsyncNewTrace, AsyncLinked               bool
	LogHasTrace, PlainLogHasTrace                           bool
	Exported                                                int
	Requests                                                int64
	HistogramCount                                          uint64
	HistogramSum                                            float64
}

// A logger does not inspect ctx for trace IDs unless a bridge or our code does it.
func writeLog(b *bytes.Buffer, ctx context.Context, addTrace bool) error {
	logger := slog.New(slog.NewJSONHandler(b, &slog.HandlerOptions{
		ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey {
				return slog.Attr{}
			}
			return a
		},
	}))
	attrs := []slog.Attr{slog.String("service", "checkout"), slog.String("route", "/orders/{id}")}
	if sc := trace.SpanContextFromContext(ctx); addTrace && sc.IsValid() {
		attrs = append(attrs, slog.String("trace_id", sc.TraceID().String()), slog.String("span_id", sc.SpanID().String()))
	}
	logger.LogAttrs(ctx, slog.LevelInfo, "order checked", attrs...)
	var got map[string]any
	return json.Unmarshal(b.Bytes(), &got)
}

func experiment() (r report, err error) {
	bg := context.Background()
	_, noSDK := noop.NewTracerProvider().Tracer("demo").Start(bg, "noop")
	r.NoSDKRecording = noSDK.IsRecording()
	noSDK.End()

	exp := tracetest.NewInMemoryExporter()
	res := resource.NewSchemaless(attribute.String("service.name", "checkout"), attribute.String("service.version", "demo-1"))
	tp := sdktrace.NewTracerProvider(sdktrace.WithResource(res), sdktrace.WithSyncer(exp),
		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())))
	defer func() {
		ctx, cancel := context.WithTimeout(bg, time.Second)
		defer cancel()
		err = errors.Join(err, tp.Shutdown(ctx))
	}()
	tracer := tp.Tracer("example.com/checkout")
	p := propagation.TraceContext{}
	carrier := propagation.MapCarrier{"traceparent": "00-11111111111111111111111111111111-2222222222222222-01"}
	incoming := p.Extract(bg, carrier)
	r.ExtractedRemote = trace.SpanContextFromContext(incoming).IsRemote()
	ctx, server := tracer.Start(incoming, "POST /orders", trace.WithSpanKind(trace.SpanKindServer))
	childCtx, child := tracer.Start(ctx, "inventory.check")
	r.SameTrace = child.SpanContext().TraceID() == server.SpanContext().TraceID()
	outgoing := propagation.MapCarrier{}
	p.Inject(childCtx, outgoing)
	recovered := trace.SpanContextFromContext(p.Extract(bg, outgoing))
	r.Propagated = recovered.TraceID() == child.SpanContext().TraceID() && recovered.SpanID() == child.SpanContext().SpanID() && recovered.IsRemote()
	var logBuf, plainBuf bytes.Buffer
	if err = writeLog(&logBuf, childCtx, true); err != nil {
		return
	}
	if err = writeLog(&plainBuf, childCtx, false); err != nil {
		return
	}
	var logged, plain map[string]any
	if err = json.Unmarshal(logBuf.Bytes(), &logged); err != nil {
		return
	}
	if err = json.Unmarshal(plainBuf.Bytes(), &plain); err != nil {
		return
	}
	r.LogHasTrace = logged["trace_id"] == child.SpanContext().TraceID().String()
	_, r.PlainLogHasTrace = plain["trace_id"]
	child.RecordError(errors.New("synthetic unavailable"))
	child.SetStatus(codes.Error, "inventory unavailable")
	child.End()
	server.End()
	// Ending a span does not erase its SpanContext from ctx.
	_, late := tracer.Start(ctx, "late-child")
	r.AfterEndChild = late.SpanContext().TraceID() == server.SpanContext().TraceID()
	late.End()

	canceled, cancel := context.WithCancel(bg)
	cancel()
	_, active := tracer.Start(canceled, "cancellation-is-not-End")
	r.CanceledContext = canceled.Err() != nil
	r.SpanStillRecording = active.IsRecording()
	active.End()

	// A detached job uses its own lifetime and a link to the triggering span.
	jobCtx, stop := context.WithTimeout(bg, time.Second)
	defer stop()
	_, job := tracer.Start(jobCtx, "job", trace.WithLinks(trace.Link{SpanContext: server.SpanContext()}))
	r.AsyncNewTrace = job.SpanContext().TraceID() != server.SpanContext().TraceID()
	job.End()

	unsampled := p.Extract(bg, propagation.MapCarrier{"traceparent": "00-33333333333333333333333333333333-4444444444444444-00"})
	_, dropped := tracer.Start(unsampled, "remote-unsampled")
	r.UnsampledRecording = dropped.IsRecording()
	dropped.End()
	spans := exp.GetSpans()
	r.Exported = len(spans)
	for _, s := range spans {
		if s.Name == "inventory.check" {
			r.ChildParent = s.Parent.SpanID() == server.SpanContext().SpanID()
		}
		if s.Name == "job" {
			r.AsyncLinked = len(s.Links) == 1 && s.Links[0].SpanContext.TraceID() == server.SpanContext().TraceID() && s.Links[0].SpanContext.SpanID() == server.SpanContext().SpanID()
		}
	}

	reader := sdkmetric.NewManualReader()
	mp := sdkmetric.NewMeterProvider(sdkmetric.WithResource(res), sdkmetric.WithReader(reader))
	defer func() { err = errors.Join(err, mp.Shutdown(bg)) }()
	meter := mp.Meter("example.com/checkout")
	count, e := meter.Int64Counter("demo.requests", metric.WithUnit("{request}"))
	if e != nil {
		return r, e
	}
	latency, e := meter.Float64Histogram("demo.request.duration", metric.WithUnit("s"), metric.WithExplicitBucketBoundaries(.1, .3, 1))
	if e != nil {
		return r, e
	}
	opts := metric.WithAttributes(attribute.String("http.route", "/orders/{id}"))
	count.Add(ctx, 2, opts)
	latency.Record(ctx, .05, opts)
	latency.Record(ctx, .45, opts)
	var data metricdata.ResourceMetrics
	if e = reader.Collect(bg, &data); e != nil {
		return r, e
	}
	for _, sm := range data.ScopeMetrics {
		for _, m := range sm.Metrics {
			switch d := m.Data.(type) {
			case metricdata.Sum[int64]:
				for _, point := range d.DataPoints {
					r.Requests += point.Value
				}
			case metricdata.Histogram[float64]:
				for _, point := range d.DataPoints {
					r.HistogramCount += point.Count
					r.HistogramSum += point.Sum
				}
			}
		}
	}
	return
}
func main() {
	r, err := experiment()
	if err != nil {
		panic(err)
	}
	fmt.Printf("carrier_roundtrip=%v\n", r.Propagated)
	fmt.Printf("no_sdk_recording=%v remote=%v same_trace=%v child_parent=%v\n", r.NoSDKRecording, r.ExtractedRemote, r.SameTrace, r.ChildParent)
	fmt.Printf("unsampled_recording=%v canceled=%v still_recording=%v\n", r.UnsampledRecording, r.CanceledContext, r.SpanStillRecording)
	fmt.Printf("after_end_child=%v async_new_trace=%v async_link=%v exported=%d\n", r.AfterEndChild, r.AsyncNewTrace, r.AsyncLinked, r.Exported)
	fmt.Printf("explicit_log_trace=%v plain_slog_trace=%v\n", r.LogHasTrace, r.PlainLogHasTrace)
	fmt.Printf("requests=%d histogram_count=%d histogram_sum=%.2f\n", r.Requests, r.HistogramCount, r.HistogramSum)
}
