package main

// Prometheus /metrics for the controller (WI-11). A dedicated registry (not
// the client_golang default) keeps the surface deliberate:
//
//   panel_bus_dropped_events_total     events dropped on full subscriber buffers
//   panel_sse_subscribers              currently-open /api/events streams
//   panel_agent_connected{agent_id}    1 per agent with a live gRPC stream
//   panel_pgx_pool_*                   pgxpool.Stat() gauges/counters
//   panel_schedule_fires_total{status} scheduler fire outcomes (ok/error/…)
//   panel_http_request_duration_seconds{method,code} cheap middleware histogram
//
// plus the standard Go runtime + process collectors.
//
// Auth matches the /admin/v1/ posture: loopback callers scrape without
// credentials; anything else needs X-Panel-Admin-Token.

import (
	"net/http"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/collectors"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/dbledeez/panel/controller/internal/db"
)

type promMetrics struct {
	reg           *prometheus.Registry
	httpDur       *prometheus.HistogramVec
	scheduleFires *prometheus.CounterVec
	sseClients    atomic.Int64
}

// newPromMetrics builds the registry. bus/db/registry callbacks are read at
// scrape time (no background sampling). Any of the h fields may be nil in
// tests; the *Func collectors guard for that.
func newPromMetrics(h *httpServer) *promMetrics {
	p := &promMetrics{reg: prometheus.NewRegistry()}
	p.reg.MustRegister(
		collectors.NewGoCollector(),
		collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
	)

	p.reg.MustRegister(prometheus.NewCounterFunc(prometheus.CounterOpts{
		Name: "panel_bus_dropped_events_total",
		Help: "Events dropped because a subscriber's buffer was full (process lifetime).",
	}, func() float64 {
		if h.bus == nil {
			return 0
		}
		return float64(h.bus.DroppedTotal())
	}))

	p.reg.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
		Name: "panel_sse_subscribers",
		Help: "Currently-open /api/events SSE streams.",
	}, func() float64 { return float64(p.sseClients.Load()) }))

	p.reg.MustRegister(&agentConnCollector{registry: h.registry})
	p.reg.MustRegister(&pgxPoolCollector{db: h.db})

	p.scheduleFires = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "panel_schedule_fires_total",
		Help: "Scheduler fire outcomes by recorded status (ok/error/cancelled/deferred).",
	}, []string{"status"})
	p.reg.MustRegister(p.scheduleFires)

	p.httpDur = prometheus.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "panel_http_request_duration_seconds",
		Help:    "HTTP request duration by method and status code (SSE stream excluded).",
		Buckets: []float64{.005, .025, .1, .25, .5, 1, 2.5, 5, 10, 30},
	}, []string{"method", "code"})
	p.reg.MustRegister(p.httpDur)
	return p
}

// handler serves the scrape endpoint off the dedicated registry.
func (p *promMetrics) handler() http.Handler {
	return promhttp.HandlerFor(p.reg, promhttp.HandlerOpts{})
}

// recordScheduleFire is the scheduler's outcome hook (see scheduleEngine.record).
func (p *promMetrics) recordScheduleFire(status string) {
	if p == nil {
		return
	}
	p.scheduleFires.WithLabelValues(status).Inc()
}

// statusRecorder captures the response code for the duration histogram.
type statusRecorder struct {
	http.ResponseWriter
	code int
}

func (s *statusRecorder) WriteHeader(code int) {
	s.code = code
	s.ResponseWriter.WriteHeader(code)
}

// Flush keeps SSE and other streaming handlers working through the wrapper.
func (s *statusRecorder) Flush() {
	if f, ok := s.ResponseWriter.(http.Flusher); ok {
		f.Flush()
	}
}

// middleware observes request duration. /api/events is excluded — a healthy
// SSE stream stays open for hours and would only pollute the histogram.
func (p *promMetrics) middleware(next http.Handler) http.Handler {
	if p == nil {
		return next
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/api/events") {
			next.ServeHTTP(w, r)
			return
		}
		rec := &statusRecorder{ResponseWriter: w, code: http.StatusOK}
		start := time.Now()
		next.ServeHTTP(rec, r)
		p.httpDur.WithLabelValues(r.Method, strconv.Itoa(rec.code)).
			Observe(time.Since(start).Seconds())
	})
}

// agentConnCollector exports 1 for every agent with a live gRPC connection.
// Absence of a series means "not connected" — offline-but-known agents live
// in the DB and are deliberately not queried at scrape time (keep scrapes
// allocation-cheap and DB-free).
type agentConnCollector struct {
	registry *agentRegistry
}

var agentConnDesc = prometheus.NewDesc(
	"panel_agent_connected",
	"1 for each agent with a live gRPC stream to the controller.",
	[]string{"agent_id"}, nil,
)

func (c *agentConnCollector) Describe(ch chan<- *prometheus.Desc) { ch <- agentConnDesc }
func (c *agentConnCollector) Collect(ch chan<- prometheus.Metric) {
	if c.registry == nil {
		return
	}
	for _, a := range c.registry.snapshot() {
		ch <- prometheus.MustNewConstMetric(agentConnDesc, prometheus.GaugeValue, 1, a.id)
	}
}

// pgxPoolCollector snapshots pgxpool.Stat() at scrape time.
type pgxPoolCollector struct {
	db *db.DB
}

var pgxGauges = map[string]*prometheus.Desc{
	"panel_pgx_pool_total_conns":        prometheus.NewDesc("panel_pgx_pool_total_conns", "Total connections in the pgx pool.", nil, nil),
	"panel_pgx_pool_idle_conns":         prometheus.NewDesc("panel_pgx_pool_idle_conns", "Idle connections in the pgx pool.", nil, nil),
	"panel_pgx_pool_acquired_conns":     prometheus.NewDesc("panel_pgx_pool_acquired_conns", "Connections currently acquired from the pgx pool.", nil, nil),
	"panel_pgx_pool_constructing_conns": prometheus.NewDesc("panel_pgx_pool_constructing_conns", "Connections currently being constructed.", nil, nil),
	"panel_pgx_pool_max_conns":          prometheus.NewDesc("panel_pgx_pool_max_conns", "Configured pool maximum.", nil, nil),
}

var pgxCounters = map[string]*prometheus.Desc{
	"panel_pgx_pool_acquires_total":          prometheus.NewDesc("panel_pgx_pool_acquires_total", "Successful pool acquires.", nil, nil),
	"panel_pgx_pool_empty_acquires_total":    prometheus.NewDesc("panel_pgx_pool_empty_acquires_total", "Acquires that had to wait for an empty pool.", nil, nil),
	"panel_pgx_pool_canceled_acquires_total": prometheus.NewDesc("panel_pgx_pool_canceled_acquires_total", "Acquires canceled by context.", nil, nil),
	"panel_pgx_pool_new_conns_total":         prometheus.NewDesc("panel_pgx_pool_new_conns_total", "Connections opened by the pool.", nil, nil),
}

func (c *pgxPoolCollector) Describe(ch chan<- *prometheus.Desc) {
	for _, d := range pgxGauges {
		ch <- d
	}
	for _, d := range pgxCounters {
		ch <- d
	}
}

func (c *pgxPoolCollector) Collect(ch chan<- prometheus.Metric) {
	if c.db == nil || c.db.Pool() == nil {
		return
	}
	st := c.db.Pool().Stat()
	g := func(name string, v float64) {
		ch <- prometheus.MustNewConstMetric(pgxGauges[name], prometheus.GaugeValue, v)
	}
	cnt := func(name string, v float64) {
		ch <- prometheus.MustNewConstMetric(pgxCounters[name], prometheus.CounterValue, v)
	}
	g("panel_pgx_pool_total_conns", float64(st.TotalConns()))
	g("panel_pgx_pool_idle_conns", float64(st.IdleConns()))
	g("panel_pgx_pool_acquired_conns", float64(st.AcquiredConns()))
	g("panel_pgx_pool_constructing_conns", float64(st.ConstructingConns()))
	g("panel_pgx_pool_max_conns", float64(st.MaxConns()))
	cnt("panel_pgx_pool_acquires_total", float64(st.AcquireCount()))
	cnt("panel_pgx_pool_empty_acquires_total", float64(st.EmptyAcquireCount()))
	cnt("panel_pgx_pool_canceled_acquires_total", float64(st.CanceledAcquireCount()))
	cnt("panel_pgx_pool_new_conns_total", float64(st.NewConnsCount()))
}
