package service

import (
	"sync"

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

const (
	providerHealthEvidenceMetric    = "iop_edge_provider_health_evidence_total"
	providerHealthTransitionMetric  = "iop_edge_provider_health_transitions_total"
	providerHealthObservationLogKey = "edge_provider_health_observation"
)

// providerHealthObservation is an immutable, identity-free projection of a
// health-overlay decision. The queue constructs it while holding its lock and
// sends it to the observer only after the decision, release, and pump finish.
type providerHealthObservation struct {
	source         string
	evidenceHealth string
	decision       string
	fromHealth     string
	toHealth       string
	stateChanged   bool
}

type providerHealthObserver interface {
	Observe(providerHealthObservation)
}

type providerHealthMetrics struct {
	evidence    *prometheus.CounterVec
	transitions *prometheus.CounterVec
}

type providerHealthObservability struct {
	metrics *providerHealthMetrics
	mu      sync.RWMutex
	logger  *zap.Logger
}

var defaultProviderHealthMetrics struct {
	once    sync.Once
	metrics *providerHealthMetrics
}

func defaultHealthObserver() providerHealthObserver {
	return &providerHealthObservability{
		metrics: defaultProviderHealthCollectorSet(),
		logger:  zap.NewNop(),
	}
}

func defaultProviderHealthCollectorSet() *providerHealthMetrics {
	defaultProviderHealthMetrics.once.Do(func() {
		defaultProviderHealthMetrics.metrics = newProviderHealthMetrics(prometheus.DefaultRegisterer)
	})
	return defaultProviderHealthMetrics.metrics
}

// newProviderHealthObservability creates an isolated observer for tests when
// reg is a private registry. Production callers use defaultHealthObserver.
func newProviderHealthObservability(reg prometheus.Registerer, logger *zap.Logger) *providerHealthObservability {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &providerHealthObservability{metrics: newProviderHealthMetrics(reg), logger: logger}
}

func newProviderHealthMetrics(reg prometheus.Registerer) *providerHealthMetrics {
	metrics := &providerHealthMetrics{
		evidence: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: providerHealthEvidenceMetric,
			Help: "Authoritative Edge provider health-overlay evidence decisions.",
		}, []string{"source", "evidence_health", "decision"}),
		transitions: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: providerHealthTransitionMetric,
			Help: "Authoritative Edge provider health-overlay state transitions.",
		}, []string{"from_health", "to_health"}),
	}
	if reg == nil {
		return metrics
	}
	metrics.evidence = registerProviderHealthCounter(reg, metrics.evidence)
	metrics.transitions = registerProviderHealthCounter(reg, metrics.transitions)
	return metrics
}

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

func (o *providerHealthObservability) SetLogger(logger *zap.Logger) {
	if o == nil || logger == nil {
		return
	}
	o.mu.Lock()
	o.logger = logger
	o.mu.Unlock()
}

func (o *providerHealthObservability) Observe(observation providerHealthObservation) {
	if o == nil || o.metrics == nil {
		return
	}
	// Prometheus counters do not return errors. The projection is deliberately
	// bounded before it reaches either metrics or logs.
	source := normalizeProviderHealthSource(observation.source)
	evidenceHealth := normalizeProviderHealth(observation.evidenceHealth)
	decision := normalizeProviderHealthDecision(observation.decision)
	fromHealth := normalizeProviderHealth(observation.fromHealth)
	toHealth := normalizeProviderHealth(observation.toHealth)
	o.metrics.evidence.WithLabelValues(source, evidenceHealth, decision).Inc()
	if observation.stateChanged {
		o.metrics.transitions.WithLabelValues(fromHealth, toHealth).Inc()
	}
	o.mu.RLock()
	logger := o.logger
	o.mu.RUnlock()
	if logger == nil {
		return
	}
	logger.Info(providerHealthObservationLogKey,
		zap.String("source", source),
		zap.String("evidence_health", evidenceHealth),
		zap.String("decision", decision),
		zap.String("from_health", fromHealth),
		zap.String("to_health", toHealth),
		zap.Bool("state_changed", observation.stateChanged),
	)
}

func normalizeProviderHealthSource(source string) string {
	switch source {
	case "stall", "probe":
		return source
	default:
		return "unknown"
	}
}

func normalizeProviderHealth(value string) string {
	switch value {
	case "available", "unavailable":
		return value
	default:
		return "unknown"
	}
}

func normalizeProviderHealthDecision(decision string) string {
	switch decision {
	case "applied", "rejected_stale", "rejected_binding", "rejected_ambiguous", "inconclusive":
		return decision
	default:
		return "inconclusive"
	}
}

func (m *modelQueueManager) observeProviderHealth(observation *providerHealthObservation) {
	if observation == nil || m == nil || m.healthObserver == nil {
		return
	}
	// Observation is non-authoritative. A custom observer must not be able to
	// turn a released lease or pumped queue back into a failed terminal path.
	defer func() { _ = recover() }()
	m.healthObserver.Observe(*observation)
}
