// Package metrics is Tapir's Prometheus instrumentation (ADR-030, issue #15). It
// owns the collectors and a small typed API the rest of the app calls — adapters
// never touch prometheus types directly. Two themes:
//
//   - HTTP/session: request count + latency by route (the matched pattern, so
//     cardinality stays bounded), and logins.
//   - AI (the priority): summarization latency by model/outcome/fallback, caption
//     fetch latency by outcome, chat latency by model, and LLM token usage.
//
// Handler() is served on a dedicated port (never the public app port) so a scrape
// is in-cluster only. slog timing lines are emitted at the call sites too.
package metrics

import (
	"net/http"
	"strconv"
	"time"

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

// latencyBuckets spans sub-second UI calls up to multi-minute model calls (a cold
// local model load is tens of seconds; the cloud fallback can be longer).
var latencyBuckets = []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 20, 30, 60, 120, 300}

var (
	httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "tapir_http_requests_total",
		Help: "HTTP requests by method, matched route pattern, and status code.",
	}, []string{"method", "route", "code"})

	httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "tapir_http_request_duration_seconds",
		Help:    "HTTP request latency by method and matched route pattern.",
		Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5},
	}, []string{"method", "route"})

	logins = promauto.NewCounter(prometheus.CounterOpts{
		Name: "tapir_logins_total",
		Help: "Successful OIDC logins (session established).",
	})

	summarizeDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "tapir_summarize_duration_seconds",
		Help:    "Per-endpoint summarization latency by model, outcome (success|parse_error|error), and whether it was a fallback.",
		Buckets: latencyBuckets,
	}, []string{"model", "outcome", "fallback"})

	captionFetchDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "tapir_caption_fetch_duration_seconds",
		Help:    "Caption fetch latency by outcome (captions|none|rate_limited).",
		Buckets: latencyBuckets,
	}, []string{"outcome"})

	chatDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "tapir_chat_duration_seconds",
		Help:    "Per-video Q&A answer latency by model.",
		Buckets: latencyBuckets,
	}, []string{"model"})

	llmTokens = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "tapir_llm_tokens_total",
		Help: "LLM tokens consumed by model and kind (prompt|completion).",
	}, []string{"model", "kind"})
)

// Handler serves the Prometheus exposition format. Mount on the dedicated metrics
// port, never the public app mux.
func Handler() http.Handler { return promhttp.Handler() }

// IncLogin records a successful login.
func IncLogin() { logins.Inc() }

// ObserveSummarize records one summarization endpoint attempt.
func ObserveSummarize(model, outcome string, fallback bool, d time.Duration) {
	summarizeDuration.WithLabelValues(model, outcome, strconv.FormatBool(fallback)).Observe(d.Seconds())
}

// ObserveCaptionFetch records one caption fetch by outcome.
func ObserveCaptionFetch(outcome string, d time.Duration) {
	captionFetchDuration.WithLabelValues(outcome).Observe(d.Seconds())
}

// ObserveChat records one Q&A answer latency.
func ObserveChat(model string, d time.Duration) {
	chatDuration.WithLabelValues(model).Observe(d.Seconds())
}

// RecordTokens records LLM token usage from a completion's usage block. Zero
// counts are skipped so a provider that omits usage adds nothing.
func RecordTokens(model string, prompt, completion int) {
	if prompt > 0 {
		llmTokens.WithLabelValues(model, "prompt").Add(float64(prompt))
	}
	if completion > 0 {
		llmTokens.WithLabelValues(model, "completion").Add(float64(completion))
	}
}

// HTTPMiddleware records request count + latency. It reads r.Pattern AFTER the
// inner handler routes (Go 1.22 sets it during ServeMux matching), so the label is
// the bounded registered pattern (e.g. "GET /v/{videoId}"), never the raw path
// with its high-cardinality ids. Unmatched requests bucket as "other".
func HTTPMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		sw := &statusWriter{ResponseWriter: w, code: http.StatusOK}
		next.ServeHTTP(sw, r)

		route := r.Pattern
		if route == "" {
			route = "other"
		}
		httpRequests.WithLabelValues(r.Method, route, strconv.Itoa(sw.code)).Inc()
		httpDuration.WithLabelValues(r.Method, route).Observe(time.Since(start).Seconds())
	})
}

// statusWriter captures the response status for the request-count label.
type statusWriter struct {
	http.ResponseWriter
	code        int
	wroteHeader bool
}

func (s *statusWriter) WriteHeader(code int) {
	if !s.wroteHeader {
		s.code = code
		s.wroteHeader = true
	}
	s.ResponseWriter.WriteHeader(code)
}

func (s *statusWriter) Write(b []byte) (int, error) {
	s.wroteHeader = true // an implicit 200
	return s.ResponseWriter.Write(b)
}
