package runtimeproduction

import (
	"context"
	"errors"
	"sync/atomic"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/rs/zerolog/log"
)

const servingControllerEvent = "runtime_security_lease_renewal"

type servingControllerMetrics struct {
	attempts            *prometheus.CounterVec
	duration            *prometheus.HistogramVec
	reconciles          *prometheus.CounterVec
	lastSuccessUnix     atomic.Int64
	consecutiveFailures atomic.Int64
	lastDurationNanos   atomic.Int64
	lastSuccess         *prometheus.Desc
	consecutive         *prometheus.Desc
	lastDuration        *prometheus.Desc
}

func newServingControllerMetrics() *servingControllerMetrics {
	return &servingControllerMetrics{
		attempts: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: "nexuscrm_runtime_security_lease_renewal_attempts_total",
			Help: "Runtime serving-security lease renewal attempts by bounded outcome.",
		}, []string{"outcome"}),
		duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
			Name:    "nexuscrm_runtime_security_lease_renewal_duration_seconds",
			Help:    "Runtime serving-security lease renewal latency by bounded outcome.",
			Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5},
		}, []string{"outcome"}),
		reconciles: prometheus.NewCounterVec(prometheus.CounterOpts{
			Name: "nexuscrm_runtime_security_epoch_reconcile_total",
			Help: "Runtime serving-state security epoch reconciliations by bounded outcome.",
		}, []string{"outcome"}),
		lastSuccess: prometheus.NewDesc(
			"nexuscrm_runtime_security_lease_renewal_last_success_unixtime",
			"Unix time of the last successful serving-security lease renewal.", nil, nil,
		),
		consecutive: prometheus.NewDesc(
			"nexuscrm_runtime_security_lease_renewal_consecutive_failures",
			"Consecutive serving-security lease renewal failures.", nil, nil,
		),
		lastDuration: prometheus.NewDesc(
			"nexuscrm_runtime_security_lease_renewal_last_duration_seconds",
			"Duration of the latest serving-security lease renewal attempt.", nil, nil,
		),
	}
}

func (metrics *servingControllerMetrics) Describe(ch chan<- *prometheus.Desc) {
	metrics.attempts.Describe(ch)
	metrics.duration.Describe(ch)
	metrics.reconciles.Describe(ch)
	ch <- metrics.lastSuccess
	ch <- metrics.consecutive
	ch <- metrics.lastDuration
}

func (metrics *servingControllerMetrics) Collect(ch chan<- prometheus.Metric) {
	metrics.attempts.Collect(ch)
	metrics.duration.Collect(ch)
	metrics.reconciles.Collect(ch)
	ch <- prometheus.MustNewConstMetric(metrics.lastSuccess, prometheus.GaugeValue, float64(metrics.lastSuccessUnix.Load()))
	ch <- prometheus.MustNewConstMetric(metrics.consecutive, prometheus.GaugeValue, float64(metrics.consecutiveFailures.Load()))
	ch <- prometheus.MustNewConstMetric(metrics.lastDuration, prometheus.GaugeValue, time.Duration(metrics.lastDurationNanos.Load()).Seconds())
}

func (metrics *servingControllerMetrics) observeRenewal(outcome string, elapsed time.Duration, now time.Time) {
	if metrics == nil {
		return
	}
	metrics.attempts.WithLabelValues(outcome).Inc()
	metrics.duration.WithLabelValues(outcome).Observe(elapsed.Seconds())
	metrics.lastDurationNanos.Store(int64(elapsed))
	if outcome == "success" {
		metrics.lastSuccessUnix.Store(now.Unix())
		metrics.consecutiveFailures.Store(0)
		return
	}
	metrics.consecutiveFailures.Add(1)
}

func (metrics *servingControllerMetrics) observeReconcile(outcome string) {
	if metrics != nil {
		metrics.reconciles.WithLabelValues(outcome).Inc()
	}
}

// runServingController gives the cheap security heartbeat its own execution
// loop. A slow Catalog reconciliation may delay an epoch transition, but can no
// longer delay renewal of the authoritative lease that keeps the replica
// fail-closed and observable.
func runServingController(
	ctx context.Context,
	interval time.Duration,
	attemptCap time.Duration,
	reconciledEpoch uint64,
	refresh func(context.Context) error,
	preparedEpoch func() (uint64, bool),
	reconcile func(context.Context, uint64) error,
	metrics *servingControllerMetrics,
) error {
	if interval <= 0 || attemptCap <= 0 || attemptCap >= interval || reconciledEpoch == 0 || refresh == nil || preparedEpoch == nil || reconcile == nil {
		return errors.New("runtime production: serving controller dependencies are incomplete")
	}

	epochs := make(chan uint64, 1)
	renewalDone := make(chan struct{})
	publishLatestEpoch := func(epoch uint64) {
		select {
		case epochs <- epoch:
			return
		default:
		}
		select {
		case <-epochs:
		default:
		}
		select {
		case epochs <- epoch:
		default:
		}
	}
	renew := func() {
		started := time.Now()
		attemptCtx, cancel := context.WithTimeout(ctx, attemptCap)
		err := refresh(attemptCtx)
		cancel()
		elapsed := time.Since(started)
		if err != nil {
			metrics.observeRenewal("error", elapsed, time.Now())
			if ctx.Err() == nil {
				log.Warn().Err(err).Str("event", servingControllerEvent).
					Str("execution_owner", "independent_db_pool").Str("outcome", "error").
					Int64("elapsed_ms", elapsed.Milliseconds()).
					Msg("Runtime serving-security lease renewal failed")
			}
			return
		}
		epoch, ready := preparedEpoch()
		if !ready {
			metrics.observeRenewal("not_ready", elapsed, time.Now())
			log.Warn().Str("event", servingControllerEvent).
				Str("execution_owner", "independent_db_pool").Str("outcome", "not_ready").
				Int64("elapsed_ms", elapsed.Milliseconds()).
				Msg("Runtime serving-security lease renewal returned no prepared epoch")
			return
		}
		metrics.observeRenewal("success", elapsed, time.Now())
		log.Debug().Str("event", servingControllerEvent).
			Str("execution_owner", "independent_db_pool").Str("outcome", "success").
			Uint64("security_epoch", epoch).Int64("elapsed_ms", elapsed.Milliseconds()).
			Msg("Runtime serving-security lease renewed")
		publishLatestEpoch(epoch)
	}

	go func() {
		defer close(renewalDone)
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		renew()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				renew()
			}
		}
	}()

	for {
		select {
		case <-ctx.Done():
			<-renewalDone
			return nil
		case epoch := <-epochs:
			if epoch == reconciledEpoch {
				continue
			}
			if err := reconcile(ctx, epoch); err != nil {
				metrics.observeReconcile("error")
				if ctx.Err() == nil {
					log.Warn().Err(err).Str("event", "runtime_security_epoch_reconcile").
						Str("outcome", "error").Uint64("security_epoch", epoch).
						Msg("Runtime serving-state security epoch reconcile failed")
				}
				continue
			}
			metrics.observeReconcile("success")
			reconciledEpoch = epoch
		}
	}
}
