// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package otel exports spans and metrics to an OpenTelemetry
// collector using the OTLP HTTP/JSON protocol.
//
// Use [NewExporter] to create an exporter, then chain its
// [Exporter.ProcessEvent] method with other exporters:
//
//	otelExporter := otel.NewExporter(ctx,
//		otel.WithEndpoint("http://localhost:4318"),
//		otel.WithServiceName("myservice"),
//	)
//	event.SetExporter(otelExporter.ProcessEvent)
//
// The exporter batches telemetry and flushes periodically in a
// background goroutine. Call [Exporter.Flush] to force an
// immediate export.
package otel

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"runtime"
	"strconv"
	"sync"
	"time"

	"golang.org/x/tools/internal/event"
	"golang.org/x/tools/internal/event/core"
	"golang.org/x/tools/internal/event/export"
	"golang.org/x/tools/internal/event/export/metric"
	"golang.org/x/tools/internal/event/label"
)

// Exporter exports spans and metrics to an OTLP HTTP endpoint.
type Exporter struct {
	mu             sync.Mutex
	endpoint       string
	serviceName    string
	serviceVersion string
	resource       otlpResource
	client         *http.Client
	flushPeriod    time.Duration
	spans          []*export.Span
	metrics        map[string]metric.Data
}

// NewExporter creates an exporter that sends spans to an OTLP endpoint.
// Spans are collected and exported periodically in a background goroutine.
// When the context is done, remaining spans are flushed.
func NewExporter(ctx context.Context, opts ...Option) *Exporter {
	e := &Exporter{
		endpoint:    DefaultEndpoint,
		serviceName: DefaultServiceName,
		client:      &http.Client{Timeout: DefaultTimeout},
		flushPeriod: DefaultFlushPeriod,
	}

	for _, opt := range opts {
		opt(e)
	}

	e.resource = e.buildResource()

	go func() {
		ticker := time.NewTicker(e.flushPeriod)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				e.Flush() // final flush before exiting
				return
			case <-ticker.C:
				e.Flush()
			}
		}
	}()

	return e
}

// ProcessEvent handles events and collects completed spans and metrics.
func (e *Exporter) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
	e.mu.Lock()
	defer e.mu.Unlock()

	if event.IsEnd(ev) {
		if span := export.GetSpan(ctx); span != nil {
			e.spans = append(e.spans, span)
		}
	}

	if event.IsMetric(ev) {
		if e.metrics == nil {
			e.metrics = make(map[string]metric.Data)
		}
		if entries := metric.Entries.Get(lm); entries != nil {
			for _, data := range entries.([]metric.Data) {
				e.metrics[data.Handle()] = data
			}
		}
	}

	return ctx
}

// Flush exports all collected spans and metrics to the OTLP endpoint.
func (e *Exporter) Flush() {
	e.mu.Lock()
	spans := e.spans
	e.spans = nil
	metrics := e.metrics
	e.metrics = nil
	e.mu.Unlock()

	// Export spans
	if len(spans) > 0 {
		otlpSpans := make([]otlpSpan, 0, len(spans))
		for _, span := range spans {
			otlpSpans = append(otlpSpans, convertSpan(span))
		}
		if err := e.exportTraces(otlpSpans); err != nil {
			log.Printf("post spans: %v", err)
			return
		}
	}

	// Export metrics
	if len(metrics) > 0 {
		otlpMetrics := make([]otlpMetric, 0, len(metrics))
		for _, data := range metrics {
			otlpMetrics = append(otlpMetrics, convertMetric(data))
		}
		if err := e.exportMetrics(otlpMetrics); err != nil {
			log.Printf("post metrics: %v", err)
		}
	}
}

// post sends a JSON payload to the OTLP endpoint.
func (e *Exporter) post(path string, payload any) error {
	data, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	// The background context is appropriate here since post is called only
	// from Flush, which is called only by the Exporter's background goroutine.
	// Notably it may be called after cancellation of the client's context.
	req, err := http.NewRequestWithContext(context.Background(), "POST", e.endpoint+path, bytes.NewReader(data))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := e.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return fmt.Errorf("OTLP export to %s failed with status %d", path, resp.StatusCode)
	}
	return nil
}

func (e *Exporter) buildResource() otlpResource {
	attrs := []otlpAttribute{
		{Key: "service.name", Value: otlpAttributeValue{StringValue: &e.serviceName}},
		{Key: "process.runtime.version", Value: otlpAttributeValue{StringValue: varOf(runtime.Version())}},
		{Key: "process.pid", Value: otlpAttributeValue{StringValue: varOf(strconv.Itoa(os.Getpid()))}},
		{Key: "host.arch", Value: otlpAttributeValue{StringValue: varOf(runtime.GOARCH)}},
		{Key: "os.type", Value: otlpAttributeValue{StringValue: varOf(runtime.GOOS)}},
	}

	if hostname, err := os.Hostname(); err == nil {
		attrs = append(attrs, otlpAttribute{
			Key:   "host.name",
			Value: otlpAttributeValue{StringValue: &hostname},
		})
	}

	if e.serviceVersion != "" {
		attrs = append(attrs, otlpAttribute{
			Key:   "service.version",
			Value: otlpAttributeValue{StringValue: &e.serviceVersion},
		})
	}

	return otlpResource{Attributes: attrs}
}
