package middleware

import (
	"context"
	"io"
	"net/http"

	"github.com/containous/alice"
	"github.com/rs/zerolog/log"
	"github.com/traefik/traefik/v3/pkg/config/dynamic"
	"github.com/traefik/traefik/v3/pkg/config/static"
	"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
	"github.com/traefik/traefik/v3/pkg/middlewares/capture"
	mmetrics "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
	"github.com/traefik/traefik/v3/pkg/middlewares/observability"
	"github.com/traefik/traefik/v3/pkg/observability/logs"
	"github.com/traefik/traefik/v3/pkg/observability/metrics"
	"github.com/traefik/traefik/v3/pkg/observability/tracing"
	otypes "github.com/traefik/traefik/v3/pkg/observability/types"
)

// ObservabilityMgr is a manager for observability (AccessLogs, Metrics and Tracing) enablement.
type ObservabilityMgr struct {
	config                 static.Configuration
	accessLoggerMiddleware *accesslog.Handler
	metricsRegistry        metrics.Registry
	semConvMetricRegistry  *metrics.SemConvMetricsRegistry
	tracer                 *tracing.Tracer
	tracerCloser           io.Closer
}

// NewObservabilityMgr creates a new ObservabilityMgr.
func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Registry, semConvMetricRegistry *metrics.SemConvMetricsRegistry, accessLoggerMiddleware *accesslog.Handler, tracer *tracing.Tracer, tracerCloser io.Closer) *ObservabilityMgr {
	return &ObservabilityMgr{
		config:                 config,
		metricsRegistry:        metricsRegistry,
		semConvMetricRegistry:  semConvMetricRegistry,
		accessLoggerMiddleware: accessLoggerMiddleware,
		tracer:                 tracer,
		tracerCloser:           tracerCloser,
	}
}

// BuildEPChain an observability middleware chain by entry point.
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, internal bool, config dynamic.RouterObservabilityConfig) alice.Chain {
	chain := alice.New()

	if o == nil {
		return chain
	}

	// Injection of the observability variables in the request context.
	// This injection must be the first step in order for other observability middlewares to rely on it.
	chain = chain.Append(func(next http.Handler) (http.Handler, error) {
		return o.observabilityContextHandler(next, internal, config), nil
	})

	// Capture middleware for accessLogs or metrics.
	if o.shouldAccessLog(internal, config) || o.shouldMeter(internal, config) || o.shouldMeterSemConv(internal, config) {
		chain = chain.Append(capture.Wrap)
	}

	// As the Entry point observability middleware ensures that the tracing is added to the request and logger context,
	// it needs to be added before the access log middleware to ensure that the trace ID is logged.
	chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))

	// Access log handlers.
	chain = chain.Append(o.accessLoggerMiddleware.AliceConstructor())
	chain = chain.Append(func(next http.Handler) (http.Handler, error) {
		return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil
	})

	// Entrypoint metrics handler.
	metricsHandler := mmetrics.EntryPointMetricsHandler(ctx, o.metricsRegistry, entryPointName)
	chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))

	// Semantic convention server metrics handler.
	chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))

	return chain
}

// MetricsRegistry is an accessor to the metrics registry.
func (o *ObservabilityMgr) MetricsRegistry() metrics.Registry {
	if o == nil {
		return nil
	}

	return o.metricsRegistry
}

// SemConvMetricsRegistry is an accessor to the semantic conventions metrics registry.
func (o *ObservabilityMgr) SemConvMetricsRegistry() *metrics.SemConvMetricsRegistry {
	if o == nil {
		return nil
	}

	return o.semConvMetricRegistry
}

// Close closes the accessLogger and tracer.
func (o *ObservabilityMgr) Close() {
	if o == nil {
		return
	}

	if o.accessLoggerMiddleware != nil {
		if err := o.accessLoggerMiddleware.Close(); err != nil {
			log.Error().Err(err).Msg("Could not close the access log file")
		}
	}

	if o.tracerCloser != nil {
		if err := o.tracerCloser.Close(); err != nil {
			log.Error().Err(err).Msg("Could not close the tracer")
		}
	}
}

func (o *ObservabilityMgr) RotateAccessLogs() error {
	if o.accessLoggerMiddleware == nil {
		return nil
	}

	return o.accessLoggerMiddleware.Rotate()
}

func (o *ObservabilityMgr) observabilityContextHandler(next http.Handler, internal bool, config dynamic.RouterObservabilityConfig) http.Handler {
	return observability.WithObservabilityHandler(next, observability.Observability{
		AccessLogsEnabled:      o.shouldAccessLog(internal, config),
		MetricsEnabled:         o.shouldMeter(internal, config),
		SemConvMetricsEnabled:  o.shouldMeterSemConv(internal, config),
		TracingEnabled:         o.shouldTrace(internal, config, otypes.MinimalVerbosity),
		DetailedTracingEnabled: o.shouldTrace(internal, config, otypes.DetailedVerbosity),
		Metadata:               config.Metadata,
	})
}

// shouldAccessLog returns whether the access logs should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldAccessLog(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
	if o == nil {
		return false
	}

	if o.config.AccessLog == nil {
		return false
	}

	if internal && !o.config.AccessLog.AddInternals {
		return false
	}

	return observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}

// shouldMeter returns whether the metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeter(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
	if o == nil || o.metricsRegistry == nil {
		return false
	}

	if !o.metricsRegistry.IsEpEnabled() && !o.metricsRegistry.IsRouterEnabled() && !o.metricsRegistry.IsSvcEnabled() {
		return false
	}

	if o.config.Metrics == nil {
		return false
	}

	if internal && !o.config.Metrics.AddInternals {
		return false
	}

	return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}

// shouldMeterSemConv returns whether the OTel semantic convention metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeterSemConv(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
	if o == nil || o.semConvMetricRegistry == nil {
		return false
	}

	if o.config.Metrics == nil {
		return false
	}

	if internal && !o.config.Metrics.AddInternals {
		return false
	}

	return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}

// shouldTrace returns whether the tracing should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldTrace(internal bool, observabilityConfig dynamic.RouterObservabilityConfig, verbosity otypes.TracingVerbosity) bool {
	if o == nil {
		return false
	}

	if o.config.Tracing == nil {
		return false
	}

	if internal && !o.config.Tracing.AddInternals {
		return false
	}

	if !observabilityConfig.TraceVerbosity.Allows(verbosity) {
		return false
	}

	return observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}
