// Package metrics emits per-request OpenTelemetry counters and histograms
// scoped by route group.
//
// The metric names are fixed by `backend/README.md` §15:
//
//   - http_requests_total{group, method, route, status}
//   - http_request_duration_seconds{group, method, route, status}
//
// One Middleware instance per route group keeps the `group` attribute stable
// across requests while allowing the gin router to share the same Meter.
package metrics

import (
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/metric"
)

// Group identifies the route family that emits the metric. The set is closed
// and matches the prefixes registered by router.New.
type Group string

const (
	// GroupRoot covers `/healthz`, `/readyz`, and unmatched routes.
	GroupRoot Group = "root"
	// GroupProbes covers the readiness/liveness probes when reported separately
	// from other root-level traffic.
	GroupProbes Group = "probes"
	// GroupPublic covers `/api/v1/public/*` endpoints.
	GroupPublic Group = "public"
	// GroupUser covers `/api/v1/user/*` endpoints.
	GroupUser Group = "user"
	// GroupAdmin covers `/api/v1/admin/*` endpoints.
	GroupAdmin Group = "admin"
	// GroupInternal covers `/api/v1/internal/*` endpoints.
	GroupInternal Group = "internal"
)

// Instruments holds the shared metric instruments used by every Group-scoped
// middleware. The instruments are constructed once per Meter; the
// per-middleware closure binds them to the right `group` attribute.
type Instruments struct {
	requestsTotal   metric.Int64Counter
	requestDuration metric.Float64Histogram
}

// NewInstruments builds the shared metric instruments from meter. A nil meter
// returns nil instruments and disables metric emission.
func NewInstruments(meter metric.Meter) (*Instruments, error) {
	if meter == nil {
		return nil, nil
	}

	requestsTotal, err := meter.Int64Counter(
		"http_requests_total",
		metric.WithDescription("Number of HTTP requests served by the backend, partitioned by route group, method, route, and response status."),
		metric.WithUnit("1"),
	)
	if err != nil {
		return nil, err
	}

	requestDuration, err := meter.Float64Histogram(
		"http_request_duration_seconds",
		metric.WithDescription("Duration of HTTP requests served by the backend, partitioned by route group, method, route, and response status."),
		metric.WithUnit("s"),
	)
	if err != nil {
		return nil, err
	}

	return &Instruments{
		requestsTotal:   requestsTotal,
		requestDuration: requestDuration,
	}, nil
}

// Middleware returns a gin middleware that records request counters and
// duration histograms with the `group` attribute fixed to group. A nil
// instruments value yields a no-op middleware so that metric emission is
// strictly opt-in.
func Middleware(instruments *Instruments, group Group) gin.HandlerFunc {
	if instruments == nil {
		return func(c *gin.Context) { c.Next() }
	}

	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		duration := time.Since(start)

		route := c.FullPath()
		if route == "" {
			route = "unmatched"
		}

		attrs := metric.WithAttributes(
			attribute.String("group", string(group)),
			attribute.String("method", c.Request.Method),
			attribute.String("route", route),
			attribute.String("status", strconv.Itoa(c.Writer.Status())),
		)

		instruments.requestsTotal.Add(c.Request.Context(), 1, attrs)
		instruments.requestDuration.Record(c.Request.Context(), duration.Seconds(), attrs)
	}
}
