package middleware

import (
	"database/sql"
	"errors"

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

// DB connection-pool saturation telemetry — the smoking-gun SLI for issue #3295.
//
// Class-A through class-D of the shared-pool failure mode all trace to a single
// 100-connection *sql.DB being shared by foreground writes/reads/search and the
// async outbox worker with no admission control. The load-bearing evidence is
// whether that pool actually saturated (InUse near MaxOpenConnections) and
// whether foreground requests blocked waiting for a connection (a rising
// WaitCount / WaitDuration). sql.DB exposes exactly this via Stats(), but it is
// not surfaced anywhere today.
//
// Unlike the outbox backlog gauges (Slice 1), which needed a bounded background
// sampler because they issue a real SQL query, db.Stats() is a cheap in-memory
// read of counters the driver already maintains. The idiomatic, zero-overhead,
// always-fresh way to expose it is a scrape-time prometheus.Collector that reads
// Stats() inside Collect(): no goroutine, no DB round trip, never stale, and
// nothing to refresh on an interval.
//
// This is pure observation: the collector only reads Stats() and emits metrics.
// It changes no pool setting, no request handling, nothing.

// dbPoolStatsCollector is a prometheus.Collector that exposes sql.DB.Stats()
// at scrape time. It holds the *sql.DB handle and reads its in-memory stats
// inside Collect; it never queries the database.
type dbPoolStatsCollector struct {
	db       *sql.DB
	poolName string

	// Gauges — point-in-time pool occupancy.
	openConnectionsDesc *prometheus.Desc
	inUseDesc           *prometheus.Desc
	idleDesc            *prometheus.Desc
	maxOpenDesc         *prometheus.Desc

	// Counters — cumulative since process start. These MUST be exposed as
	// counters (monotonic), not gauges: WaitCount/WaitDuration and the various
	// *Closed totals only ever increase, and consumers compute rates over them.
	waitCountDesc         *prometheus.Desc
	waitDurationDesc      *prometheus.Desc
	maxIdleClosedDesc     *prometheus.Desc
	maxIdleTimeClosedDesc *prometheus.Desc
	maxLifetimeClosedDesc *prometheus.Desc
}

// newDBPoolStatsCollector builds a collector over the given *sql.DB. The handle
// may be nil; the collector then emits nothing (Collect is a no-op), so callers
// need not special-case a nil DB before constructing it.
func newDBPoolStatsCollector(db *sql.DB, poolName string) *dbPoolStatsCollector {
	if poolName == "" {
		poolName = "shared"
	}
	labels := prometheus.Labels{"pool": poolName}
	return &dbPoolStatsCollector{
		db:       db,
		poolName: poolName,
		openConnectionsDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_open_connections",
			"Number of established connections both in use and idle.",
			nil, labels,
		),
		inUseDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_in_use",
			"Number of connections currently in use. Approaching max_open_connections is the saturation signal.",
			nil, labels,
		),
		idleDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_idle",
			"Number of idle connections in the pool.",
			nil, labels,
		),
		maxOpenDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_max_open_connections",
			"Maximum number of open connections to the database (the pool ceiling).",
			nil, labels,
		),
		waitCountDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_wait_count_total",
			"Cumulative number of connection waits — foreground requests that blocked because the pool was exhausted.",
			nil, labels,
		),
		waitDurationDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_wait_duration_seconds_total",
			"Cumulative time blocked waiting for a connection, in seconds.",
			nil, labels,
		),
		maxIdleClosedDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_max_idle_closed_total",
			"Cumulative number of connections closed due to SetMaxIdleConns.",
			nil, labels,
		),
		maxIdleTimeClosedDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_max_idle_time_closed_total",
			"Cumulative number of connections closed due to SetConnMaxIdleTime.",
			nil, labels,
		),
		maxLifetimeClosedDesc: prometheus.NewDesc(
			"nexuscrm_db_pool_max_lifetime_closed_total",
			"Cumulative number of connections closed due to SetConnMaxLifetime.",
			nil, labels,
		),
	}
}

// Describe sends every metric descriptor. Implemented even when db is nil so the
// registry knows the collector's stable metric set regardless of DB state.
func (c *dbPoolStatsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.openConnectionsDesc
	ch <- c.inUseDesc
	ch <- c.idleDesc
	ch <- c.maxOpenDesc
	ch <- c.waitCountDesc
	ch <- c.waitDurationDesc
	ch <- c.maxIdleClosedDesc
	ch <- c.maxIdleTimeClosedDesc
	ch <- c.maxLifetimeClosedDesc
}

// Collect reads sql.DB.Stats() once at scrape time and emits the snapshot. If
// the handle is nil it emits nothing. Stats() is an in-memory read of counters
// the driver already keeps, so this adds no DB load and is always fresh.
func (c *dbPoolStatsCollector) Collect(ch chan<- prometheus.Metric) {
	if c.db == nil {
		return
	}
	s := c.db.Stats()

	// Gauges — point-in-time occupancy.
	ch <- prometheus.MustNewConstMetric(c.openConnectionsDesc, prometheus.GaugeValue, float64(s.OpenConnections))
	ch <- prometheus.MustNewConstMetric(c.inUseDesc, prometheus.GaugeValue, float64(s.InUse))
	ch <- prometheus.MustNewConstMetric(c.idleDesc, prometheus.GaugeValue, float64(s.Idle))
	ch <- prometheus.MustNewConstMetric(c.maxOpenDesc, prometheus.GaugeValue, float64(s.MaxOpenConnections))

	// Counters — cumulative since process start. WaitDuration is a time.Duration;
	// convert to seconds (the Prometheus base unit) for *_seconds_total.
	ch <- prometheus.MustNewConstMetric(c.waitCountDesc, prometheus.CounterValue, float64(s.WaitCount))
	ch <- prometheus.MustNewConstMetric(c.waitDurationDesc, prometheus.CounterValue, s.WaitDuration.Seconds())
	ch <- prometheus.MustNewConstMetric(c.maxIdleClosedDesc, prometheus.CounterValue, float64(s.MaxIdleClosed))
	ch <- prometheus.MustNewConstMetric(c.maxIdleTimeClosedDesc, prometheus.CounterValue, float64(s.MaxIdleTimeClosed))
	ch <- prometheus.MustNewConstMetric(c.maxLifetimeClosedDesc, prometheus.CounterValue, float64(s.MaxLifetimeClosed))
}

// RegisterDBPoolStatsCollector registers a scrape-time collector that exposes
// the shared pool's sql.DB.Stats() on the default Prometheus registry — the same
// registry promhttp.Handler() serves at /metrics, so the collector is actually
// scraped.
//
// It is a no-op when db is nil. Registration is idempotent: a duplicate
// registration (e.g. a second runtime in-process) is tolerated rather than
// panicking, mirroring promauto's default-registry usage in Slice 1.
func RegisterDBPoolStatsCollector(db *sql.DB) {
	RegisterNamedDBPoolStatsCollector("shared", db)
}

func RegisterNamedDBPoolStatsCollector(poolName string, db *sql.DB) {
	if db == nil {
		return
	}
	collector := newDBPoolStatsCollector(db, poolName)
	if err := prometheus.Register(collector); err != nil {
		// AlreadyRegisteredError means an equivalent collector is already
		// installed; that is fine and the metrics are still exposed. Any other
		// error is a genuine misconfiguration worth surfacing.
		var already prometheus.AlreadyRegisteredError
		if errors.As(err, &already) {
			return
		}
		panic(err)
	}
}
