// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tracing

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/go-logr/stdr"
	"go.opentelemetry.io/contrib/exporters/autoexport"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"

	// This *MUST* always be updated to the latest version when OTEL dependencies are updated in OpenTofu
	// Failing to do so will prevent OpenTofu from initializing tracing.
	semconv "go.opentelemetry.io/otel/semconv/v1.37.0"

	"github.com/opentofu/opentofu/version"
)

/*
BEWARE! This is not a committed external interface.

Everything about this is experimental and subject to change in future
releases. Do not depend on anything about the structure of this output.
This mechanism might be removed altogether if a different strategy seems
better based on experience with this experiment.

*/

// OTELExporterEnvVar is the env var that should be used to instruct opentofu which
// exporter to use
// If this environment variable is set to "otlp" when running OpenTofu CLI
// then we'll enable an experimental OTLP trace exporter.
const OTELExporterEnvVar = "OTEL_TRACES_EXPORTER"

// traceParentEnvVar is the env var that should be used to instruct opentofu which
// trace parent to use.
// If this environment variable is set when running OpenTofu CLI
// then we'll extract the traceparent from the environment and add it to the context.
// This ensures that all opentofu traces are linked to the trace that invoked
// this command.
const traceParentEnvVar = "TRACEPARENT"

// traceStateEnvVar is the env var that should be used to instruct opentofu which
// trace state to use.
const traceStateEnvVar = "TRACESTATE"

// ServiceNameEnvVar is the standard OpenTelemetry environment variable for specifying the service name
const ServiceNameEnvVar = "OTEL_SERVICE_NAME"

// DefaultServiceName is the default service name to use if not specified in the environment
const DefaultServiceName = "OpenTofu CLI"

// isTracingEnabled is true if OpenTelemetry is enabled.
var isTracingEnabled bool

// OpenTelemetryInit initializes the optional OpenTelemetry exporter.
//
// By default, we don't export telemetry information at all, since OpenTofu is
// a CLI tool, and so we don't assume we're running in an environment with
// a telemetry collector available.
//
// However, for those running OpenTofu in automation we allow setting
// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp
// to enable an OTLP exporter, which is in turn configured by all the
// standard OTLP exporter environment variables:
//
//	https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options
//
// We don't currently support any other telemetry export protocols, because
// OTLP has emerged as a de-facto standard and each other exporter we support
// means another relatively-heavy external dependency. OTLP happens to use
// protocol buffers and gRPC, which OpenTofu would depend on for other reasons
// anyway.
//
// Returns the context with trace context extracted from environment variables
// if TRACEPARENT is set.
func OpenTelemetryInit(ctx context.Context) (context.Context, error) {
	isTracingEnabled = false

	// We'll check the environment variable ourselves first, because the
	// "autoexport" helper we're about to use is built under the assumption
	// that exporting should always be enabled and so will expect to find
	// an OTLP server on localhost if no environment variables are set at all.
	if os.Getenv(OTELExporterEnvVar) != "otlp" {
		log.Printf("[TRACE] OpenTelemetry: %s not set, OTel tracing is not enabled", OTELExporterEnvVar)
		return ctx, nil // By default, we just discard all telemetry calls
	}

	isTracingEnabled = true

	log.Printf("[TRACE] OpenTelemetry: enabled")

	// Get service name from environment variable or use default
	serviceName := DefaultServiceName
	if envServiceName := os.Getenv(ServiceNameEnvVar); envServiceName != "" {
		log.Printf("[TRACE] OpenTelemetry: using service name from %s: %s", ServiceNameEnvVar, envServiceName)
		serviceName = envServiceName
	}

	otelResource, err := resource.New(context.Background(),
		// Use built-in detectors to simplify the collation of the racing information
		resource.WithOS(),
		resource.WithHost(),
		resource.WithProcess(),
		resource.WithSchemaURL(semconv.SchemaURL),
		resource.WithAttributes(),

		// Add custom service attributes
		resource.WithAttributes(
			semconv.ServiceName(serviceName),
			semconv.ServiceVersion(version.Version),

			// We add in the telemetry SDK information so that we don't end up with
			// duplicate schema urls that clash
			semconv.TelemetrySDKName("opentelemetry"),
			semconv.TelemetrySDKLanguageGo,
			semconv.TelemetrySDKVersion(sdk.Version()),
		),
	)
	if err != nil {
		return ctx, fmt.Errorf("failed to create resource: %w", err)
	}

	// Check if the trace parent/state environment variable is set and extract it into our context
	if traceparent := os.Getenv(traceParentEnvVar); traceparent != "" {
		log.Printf("[TRACE] OpenTelemetry: found trace parent in environment: %s", traceparent)
		// Create a carrier that contains the traceparent from environment variables
		// The key is lowercase because the TraceContext propagator expects lowercase keys
		propCarrier := make(propagation.MapCarrier)
		propCarrier.Set("traceparent", traceparent)

		if tracestate := os.Getenv(traceStateEnvVar); tracestate != "" {
			log.Printf("[TRACE] OpenTelemetry: found trace state in environment: %s", traceparent)
			propCarrier.Set("tracestate", tracestate)
		}

		// Extract the trace context into the context
		tc := propagation.TraceContext{}
		ctx = tc.Extract(ctx, propCarrier)
	}

	exporter, err := autoexport.NewSpanExporter(ctx)
	if err != nil {
		return ctx, err
	}

	// Set the global tracer provider, this allows us to use this global TracerProvider
	// to create tracers around the project
	provider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter,
			sdktrace.WithBlocking(),
		),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithResource(otelResource),
	)
	otel.SetTracerProvider(provider)

	// Create a composite propagator that includes both TraceContext and Baggage
	prop := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
	otel.SetTextMapPropagator(prop)

	logger := stdr.New(log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile))
	otel.SetLogger(logger)

	otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
		log.Printf("[ERROR] OpenTelemetry error: %s", err)
	}))

	return ctx, nil
}
