package service

import (
	"sync"

	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
)

const (
	singleRequestLifecycleMetric   = "iop_edge_single_request_lifecycle_total"
	singleRequestDurationMetric    = "iop_edge_single_request_duration_seconds"
	singleRequestObservationLogKey = "edge_single_request_observation"
)

// singleRequestMetrics contains only fixed-label collectors. Correlation is
// intentionally a log-only field and is never admitted as a metric label.
type singleRequestMetrics struct {
	lifecycle *prometheus.CounterVec
	duration  *prometheus.HistogramVec
}

type singleRequestObservability struct {
	metrics *singleRequestMetrics
	mu      sync.RWMutex
	logger  *zap.Logger
}

var defaultSingleRequestMetrics struct {
	once    sync.Once
	metrics *singleRequestMetrics
}

func defaultSingleRequestCollectorSet() *singleRequestMetrics {
	defaultSingleRequestMetrics.once.Do(func() {
		defaultSingleRequestMetrics.metrics = newSingleRequestMetrics(prometheus.DefaultRegisterer)
	})
	return defaultSingleRequestMetrics.metrics
}

func newSingleRequestObservability(reg prometheus.Registerer, logger *zap.Logger) *singleRequestObservability {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &singleRequestObservability{metrics: newSingleRequestMetrics(reg), logger: logger}
}

func newDefaultSingleRequestObservability(logger *zap.Logger) *singleRequestObservability {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &singleRequestObservability{metrics: defaultSingleRequestCollectorSet(), logger: logger}
}

func newSingleRequestMetrics(reg prometheus.Registerer) *singleRequestMetrics {
	labels := []string{"event_class", "stage", "operation", "outcome", "error_class"}
	metrics := &singleRequestMetrics{
		lifecycle: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: singleRequestLifecycleMetric,
			Help: "Closed Edge single-request lifecycle observations.",
		}, labels),
		duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
			Name:    singleRequestDurationMetric,
			Help:    "Closed Edge single-request lifecycle durations.",
			Buckets: prometheus.DefBuckets,
		}, labels),
	}
	if reg == nil {
		return metrics
	}
	metrics.lifecycle = registerSingleRequestCounter(reg, metrics.lifecycle)
	metrics.duration = registerSingleRequestHistogram(reg, metrics.duration)
	return metrics
}

func registerSingleRequestCounter(reg prometheus.Registerer, collector *prometheus.CounterVec) *prometheus.CounterVec {
	if err := reg.Register(collector); err != nil {
		if existing, ok := err.(prometheus.AlreadyRegisteredError); ok {
			if counter, ok := existing.ExistingCollector.(*prometheus.CounterVec); ok {
				return counter
			}
		}
	}
	return collector
}

func registerSingleRequestHistogram(reg prometheus.Registerer, collector *prometheus.HistogramVec) *prometheus.HistogramVec {
	if err := reg.Register(collector); err != nil {
		if existing, ok := err.(prometheus.AlreadyRegisteredError); ok {
			if histogram, ok := existing.ExistingCollector.(*prometheus.HistogramVec); ok {
				return histogram
			}
		}
	}
	return collector
}

// SetSingleRequestObservationLogger installs the bounded production observer.
// It is called during Edge bootstrap before input servers are constructed.
func (s *Service) SetSingleRequestObservationLogger(logger *zap.Logger) {
	if s == nil {
		return
	}
	s.SetSingleRequestObserver(newDefaultSingleRequestObservability(logger))
}

// SetSingleRequestObservationLoggerForTesting installs an isolated collector
// registry for cross-package integration tests. Production bootstrap must use
// SetSingleRequestObservationLogger and the process-default collector set.
func (s *Service) SetSingleRequestObservationLoggerForTesting(reg prometheus.Registerer, logger *zap.Logger) {
	if s == nil {
		return
	}
	s.SetSingleRequestObserver(newSingleRequestObservability(reg, logger))
}

// SingleRequestObservationConfigured is a narrow bootstrap test seam. It
// exposes only whether a non-noop observer is present, never the observer or
// any request data.
func (s *Service) SingleRequestObservationConfigured() bool {
	if s == nil {
		return false
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.singleRequestObserver != nil
}

func (o *singleRequestObservability) Emit(dto singleRequestDTO) error {
	if o == nil || o.metrics == nil || !singleRequestDTOIsValid(dto) {
		return nil
	}
	labels := singleRequestMetricLabels(dto)
	o.metrics.lifecycle.WithLabelValues(labels...).Inc()
	if dto.DurationMS >= 0 {
		o.metrics.duration.WithLabelValues(labels...).Observe(float64(dto.DurationMS) / 1000)
	}
	o.mu.RLock()
	logger := o.logger
	o.mu.RUnlock()
	if logger == nil {
		return nil
	}
	logger.Info(singleRequestObservationLogKey,
		zap.String("correlation", singleRequestSanitizeString(dto.Correlation)),
		zap.String("event_class", string(dto.EventClass)),
		zap.String("stage", singleRequestMetricStage(dto.Stage)),
		zap.String("operation", singleRequestMetricOperation(dto.Operation)),
		zap.String("outcome", singleRequestMetricOutcome(dto.Outcome)),
		zap.String("error_class", singleRequestMetricErrorClass(dto.ErrorClass)),
		zap.Int64("duration_ms", dto.DurationMS),
		zap.Int("tool_count", dto.ToolCount),
		zap.Bool("has_result", dto.HasResult),
	)
	return nil
}

func singleRequestMetricLabels(dto singleRequestDTO) []string {
	return []string{
		string(dto.EventClass),
		singleRequestMetricStage(dto.Stage),
		singleRequestMetricOperation(dto.Operation),
		singleRequestMetricOutcome(dto.Outcome),
		singleRequestMetricErrorClass(dto.ErrorClass),
	}
}

func singleRequestMetricStage(value singleRequestStage) string {
	if singleRequestStageIsValid(value) {
		return string(value)
	}
	return "none"
}

func singleRequestMetricOperation(value singleRequestOperation) string {
	if singleRequestOperationIsValid(value) {
		return string(value)
	}
	return "none"
}

func singleRequestMetricOutcome(value singleRequestOutcome) string {
	if singleRequestOutcomeIsValid(value) {
		return string(value)
	}
	return "none"
}

func singleRequestMetricErrorClass(value singleRequestErrorClass) string {
	if singleRequestErrorClassIsValid(value) {
		return string(value)
	}
	return "none"
}
