// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

// Metric names — PRD fixpack F15. Only emissions live in Lane A;
// runbook/alerts move to PR-1c per the PR-split decision.
var (
	quarantinedFieldsGauge = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "nexuscrm_autonumber_quarantined_fields",
			Help: "Autonumber fields currently quarantined due to metadata parse/validate failure. See PRD §5.6.",
		},
		[]string{"field_id"},
	)

	fallbackInFlightGauge = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "nexuscrm_autonumber_fallback_in_flight",
			Help: "Concurrent autonumber fallback reservers holding the per-counter semaphore.",
		},
		[]string{"field_id", "scope_key"},
	)

	retryAttemptsHist = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "nexuscrm_autonumber_retry_attempts",
			Help:    "Optimistic-UPDATE retry count per reservation (0 = first-try success).",
			Buckets: []float64{0, 1, 2, 3, 5, 10},
		},
		[]string{"field_id"},
	)

	reservationDurationHist = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "nexuscrm_autonumber_reservation_duration_ms",
			Help:    "Wall-time for a complete reserveOne / reserveBatch call (ms).",
			Buckets: []float64{1, 5, 10, 50, 100, 500, 1000, 5000},
		},
		[]string{"field_id"},
	)
)

// reservationMetrics is the thin wrapper the reserver uses so tests
// can plumb a nil-metrics emitter through without conditional logic.
type reservationMetrics struct{}

// newReservationMetrics returns the singleton (metrics are
// process-global via promauto). A non-nil return is always safe.
func newReservationMetrics() *reservationMetrics { return &reservationMetrics{} }

// observeRetryAttempt records how many retries the optimistic loop
// consumed before committing. Called at the top of each iteration.
func (m *reservationMetrics) observeRetryAttempt(fieldID string, attempt int) {
	if m == nil {
		return
	}
	retryAttemptsHist.WithLabelValues(fieldID).Observe(float64(attempt))
}

// observeReservationDuration records the total wall-time of a single
// reservation call (retries + fallback included).
func (m *reservationMetrics) observeReservationDuration(fieldID string, d time.Duration) {
	if m == nil {
		return
	}
	reservationDurationHist.WithLabelValues(fieldID).Observe(float64(d.Milliseconds()))
}

// incFallbackInFlight / decFallbackInFlight manage the in-flight
// gauge pair for the `FOR UPDATE` lane.
func (m *reservationMetrics) incFallbackInFlight(fieldID, scopeKey string) {
	if m == nil {
		return
	}
	fallbackInFlightGauge.WithLabelValues(fieldID, scopeKey).Inc()
}
func (m *reservationMetrics) decFallbackInFlight(fieldID, scopeKey string) {
	if m == nil {
		return
	}
	fallbackInFlightGauge.WithLabelValues(fieldID, scopeKey).Dec()
}

// emitQuarantine sets the quarantined-fields gauge to 1 for the given
// field. Cleared via emitUnquarantine on repair.
func emitQuarantine(fieldID string) {
	quarantinedFieldsGauge.WithLabelValues(fieldID).Set(1)
}
func emitUnquarantine(fieldID string) {
	quarantinedFieldsGauge.DeleteLabelValues(fieldID)
}
