package observability

import (
	"context"
	"fmt"
	"net/url"
	"strings"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"

	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/versioning"
)

const traceExporterTimeout = 5 * time.Second

func newConfiguredTracerProvider(ctx context.Context, cfg config.ObservabilityConfig, environment string) (trace.TracerProvider, func(context.Context) error, error) {
	endpoint := strings.TrimSpace(cfg.OTLPEndpoint)
	if endpoint == "" {
		return noop.NewTracerProvider(), nil, nil
	}
	if cfg.TraceSampleRatio < 0 || cfg.TraceSampleRatio > 1 {
		return nil, nil, fmt.Errorf("AGENTTEAM_OBSERVABILITY_TRACE_SAMPLE_RATIO must be in [0, 1]")
	}

	traceEndpoint, err := otlpTraceEndpointURL(endpoint)
	if err != nil {
		return nil, nil, err
	}
	traceResource, err := newTraceResource(environment)
	if err != nil {
		return nil, nil, err
	}
	exporter, err := otlptracehttp.New(
		ctx,
		otlptracehttp.WithEndpointURL(traceEndpoint),
		otlptracehttp.WithHeaders(copyOTLPHeaders(cfg.OTLPHeaders)),
		otlptracehttp.WithTimeout(traceExporterTimeout),
	)
	if err != nil {
		return nil, nil, fmt.Errorf("initialize OTLP trace exporter: %w", err)
	}
	tracerProvider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter, sdktrace.WithExportTimeout(traceExporterTimeout)),
		sdktrace.WithResource(traceResource),
		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(cfg.TraceSampleRatio))),
	)
	return tracerProvider, tracerProvider.Shutdown, nil
}

func newTraceResource(environment string) (*resource.Resource, error) {
	attrs := []attribute.KeyValue{
		semconv.ServiceName("agentteam-server"),
		semconv.ServiceVersion(versioning.ServerVersion()),
	}
	if env := strings.TrimSpace(environment); env != "" {
		attrs = append(attrs, semconv.DeploymentEnvironmentName(env))
	}
	traceResource, err := resource.Merge(
		resource.Default(),
		resource.NewSchemaless(attrs...),
	)
	if err != nil {
		return nil, fmt.Errorf("create trace resource: %w", err)
	}
	return traceResource, nil
}

func otlpTraceEndpointURL(raw string) (string, error) {
	const envName = "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT"
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", nil
	}
	parsedEndpoint, err := url.Parse(trimmed)
	if err != nil {
		return "", fmt.Errorf("%s must be a valid URL: %w", envName, err)
	}
	switch strings.ToLower(parsedEndpoint.Scheme) {
	case "http", "https":
	default:
		return "", fmt.Errorf("%s must use http(s)", envName)
	}
	if strings.TrimSpace(parsedEndpoint.Host) == "" {
		return "", fmt.Errorf("%s must include a host", envName)
	}
	if parsedEndpoint.User != nil {
		return "", fmt.Errorf("%s must not include user info", envName)
	}
	if parsedEndpoint.RawQuery != "" {
		return "", fmt.Errorf("%s must not include a query string", envName)
	}
	if parsedEndpoint.Fragment != "" {
		return "", fmt.Errorf("%s must not include a fragment", envName)
	}

	traceEndpoint := *parsedEndpoint
	traceEndpoint.RawPath = ""
	traceEndpoint.Path = otlpTracePath(traceEndpoint.Path)
	return traceEndpoint.String(), nil
}

func otlpTracePath(basePath string) string {
	path := strings.TrimRight(basePath, "/")
	if path == "" {
		return "/v1/traces"
	}
	if strings.HasSuffix(path, "/v1/traces") {
		return path
	}
	return path + "/v1/traces"
}

func copyOTLPHeaders(headers map[string]string) map[string]string {
	if len(headers) == 0 {
		return nil
	}
	copied := make(map[string]string, len(headers))
	for key, value := range headers {
		copied[key] = value
	}
	return copied
}
