package middleware

import (
	"context"
	"errors"
	"net/http"

	"github.com/nexuscrm/kernel/pkg/apierr"
	kernelErrors "github.com/nexuscrm/kernel/pkg/errors"
	persistence "github.com/nexuscrm/persistence"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

// Request-path observational metrics (#3295, PR-0 Slice 3).
//
// These two metrics complete the core diagnostic for the shared-pool saturation
// failure mode alongside the outbox-backlog gauges (Slice 1) and the db-pool
// saturation collector (Slice 2). They are strictly observational: nothing here
// changes a response status, body, branch, or control flow. They only count and
// observe.
//
//   - httpErrorsByCause classifies every error the global error handler is about
//     to render into a small, fixed `cause` label set. It answers the question
//     that the raw 5xx counter cannot: how many 5xx are LOAD-INDUCED (a context
//     deadline expiring, lock contention) versus a genuine internal fault. The
//     handler renders exactly as before; this counter is read-only classification.
//
//   - httpRequestsInFlightByRoute is the per-route companion to the existing
//     global httpRequestsInFlight gauge. It is keyed on the route TEMPLATE
//     (c.FullPath()), never a raw path, so cardinality stays bounded to the
//     registered route set. It shows WHICH endpoint's concurrency is climbing
//     during a burst, which the single global gauge cannot.

// Fixed cause label values. Cardinality is bounded to exactly this set; the raw
// error string is never used as a label.
const (
	causeCtxDeadline          = "ctx_deadline"
	causeCtxCanceled          = "ctx_canceled"
	causeParentLaneContention = "parent_lane_contention"
	causeDBRowLockContention  = "db_row_lock_contention"
	causeInternal             = "internal"
	causeOther                = "other"
)

var (
	// httpErrorsByCause counts errors handled by the global error handler,
	// bucketed by a bounded classification of the underlying cause. This is the
	// failure-class-B signal: it separates load-induced 5xx (ctx_deadline,
	// parent_lane_contention, db_row_lock_contention) from genuine internal
	// faults (internal) and ordinary non-5xx app errors (other).
	httpErrorsByCause = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "nexuscrm_http_errors_by_cause_total",
			Help: "Total errors rendered by the global error handler, bucketed by a bounded cause classification (ctx_deadline/ctx_canceled/parent_lane_contention/db_row_lock_contention/internal/other).",
		},
		[]string{"cause"},
	)

	// httpRequestsInFlightByRoute tracks concurrent in-flight requests per route
	// TEMPLATE (c.FullPath()). Keyed on the template, not the raw path, so the
	// label set is bounded to the registered routes.
	httpRequestsInFlightByRoute = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "nexuscrm_http_requests_in_flight_by_route",
			Help: "Number of HTTP requests currently being processed, labeled by route template.",
		},
		[]string{"route"},
	)
)

// classifyErrorCause maps an error into one of the fixed cause buckets. It is a
// pure, read-only classifier: it inspects the error and returns a label, with no
// side effects and no influence on how the error is rendered.
//
// The ordering is significant. context cancellation/deadline are checked first
// because a deadline-exceeded or canceled context is the load-induced signal we
// most want to separate out, and it can be wrapped underneath an internal error.
// Parent-lane contention and database row-lock contention are checked next as
// separate persistence predicates. Only then do we classify the genuine
// internal/5xx faults, and finally fall through to the catch-all "other" bucket
// for non-5xx app errors and anything unrecognized.
//
// The internal bucket combines the two ways the global error handler itself
// recognizes a 5xx fault: the neutral InternalConditioner classification
// (kernelErrors.IsInternal — error_handler.go:72) AND an *apierr.Error whose
// Status is already >= 500 (error_handler.go:87). Mirroring both arms is what
// keeps `cause` honest: a request the handler renders as a 5xx fault is counted
// as `internal`, not silently dropped into `other`, which is the whole point of
// the load-induced-vs-genuine-fault split (failure class B). The classification
// is read-only — it never changes which arm the handler actually renders.
func classifyErrorCause(err error) string {
	if err == nil {
		return causeOther
	}
	switch {
	case errors.Is(err, context.DeadlineExceeded):
		return causeCtxDeadline
	case errors.Is(err, context.Canceled):
		return causeCtxCanceled
	case persistence.IsParentLaneContention(err):
		return causeParentLaneContention
	case persistence.IsDBRowLockContention(err):
		return causeDBRowLockContention
	case kernelErrors.IsInternal(err):
		return causeInternal
	case isAPIServerError(err):
		return causeInternal
	default:
		return causeOther
	}
}

// isAPIServerError reports whether err is (or wraps) an *apierr.Error whose
// status is already a 5xx. This mirrors the global error handler's own
// `errors.As(err, &apiErr)` + `Status >= 500` arm so an API-level internal error
// is classified as `internal` rather than `other`.
func isAPIServerError(err error) bool {
	var apiErr *apierr.Error
	return errors.As(err, &apiErr) && apiErr.Status >= http.StatusInternalServerError
}

// recordErrorCause classifies err and increments the cause counter. This is the
// sole observational hook the global error handler calls; it deliberately returns
// nothing and renders nothing, so it cannot affect the response.
func recordErrorCause(err error) {
	httpErrorsByCause.WithLabelValues(classifyErrorCause(err)).Inc()
}
