package middleware

import (
	"strings"

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

// Unscoped-credential admission counters are the inventory design section 11 requires before
// the empty-scope rule can change.
//
// Today an empty OAuth scope means *full* access. The future rule is contextual: empty scope
// on a browser session grants browser-route authority only, and empty scope on a programmatic
// Bearer or PAT is invalid for protected API routes. Section 11 refuses to flip that early —
// it would break UI consumers before their replacement exists — and requires instead that
// existing unscoped programmatic sessions get inventory, operator-visible metrics and a
// runbook first. They do not earn a permanent legacy bypass.
//
// So this measures *reliance* rather than existence. A count of unscoped rows in the database
// says how many credentials could break; a count of unscoped admissions says how many are
// actually being used, which is the number that has to reach zero before the flip is safe. A
// row that nothing has presented for a month is not a migration risk.
//
// The boundary is precise and worth naming, because "only successful admissions" turned out to be
// ambiguous about *whose* success. What is counted is a grant by the credential authority: the
// point at which authentication and authorization have both said yes. A transport can still refuse
// afterwards — an auth-epoch that disagrees with the presented claims, a subject that fails
// re-validation — and those are deliberately still counted, because a credential failing them is
// already broken for reasons that have nothing to do with scope, so it is not part of the
// empty-scope backlog either way. Chasing each consumer's checks does not converge: any caller can
// add one, so the only stable boundary is the authority's own answer.
//
// It is deliberately observation-only. Nothing here denies, and nothing here reads the
// database: the label values arrive from the auth owner's scope observer, so a scrape costs
// nothing and an admission costs one counter increment.
//
// The counters live here and the observation is made in nexuscrm-auth, because the scope fact
// is known where admission happens while the metric backend belongs to the host. An earlier
// attempt instrumented each transport adapter instead; it had to classify five call sites by
// hand and three of those classifications were wrong, since one file can hold both a
// client-presented admission and the server verifying its own work.
var (
	// unscopedCredentialAdmissions counts admitted requests whose credential carried no
	// OAuth scope, split by the credential class that presented it.
	//
	// The class is the whole point of the split. "browser" admissions are expected to stay
	// and will be reinterpreted rather than denied; "programmatic" admissions are the
	// migration backlog, and their count is the flip's readiness signal.
	unscopedCredentialAdmissions = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "nexuscrm_unscoped_credential_admissions_total",
			Help: "Credentials granted by the credential authority that carried no OAuth scope, by class (browser/programmatic/api_token). Counts authority grants, not requests served: a transport may still refuse afterwards on epoch or consistency grounds, and such a credential is already broken for reasons unrelated to scope.",
		},
		[]string{"class"},
	)

	// scopedCredentialAdmissions counts the same population that *does* carry a scope, so the
	// unscoped count can be read as a proportion.
	//
	// Without it the metric is unactionable: 500 unscoped admissions an hour means one thing
	// against 500 total and another against 5,000,000, and an operator watching a migration
	// needs to know which. It also makes a drop to zero legible — no unscoped admissions with
	// no admissions at all is an outage, not progress.
	scopedCredentialAdmissions = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "nexuscrm_scoped_credential_admissions_total",
			Help: "Credentials granted by the credential authority that carried an explicit OAuth scope, by class (browser/programmatic/api_token). The denominator for the unscoped count.",
		},
		[]string{"class"},
	)
)

// Credential classes reported by the counters. They are the classes an operator has to act on
// differently, not every distinction the system makes internally.
const (
	// CredentialClassBrowser is a first-party browser session. An empty scope here is expected
	// and will be reinterpreted as browser-route authority, not denied.
	CredentialClassBrowser = "browser"
	// CredentialClassAPIToken is a personal access token. An empty scope here is the narrowest
	// and most urgent part of the backlog: a PAT is long-lived and user-minted.
	CredentialClassAPIToken = "api_token"
	// CredentialClassProgrammatic is any other bearer-presented session.
	CredentialClassProgrammatic = "programmatic"
	// CredentialClassUnclassified is what section 11 calls "absent credential
	// classification". It gets its own visible value rather than being folded into
	// programmatic, where an operator would read it as understood.
	CredentialClassUnclassified = "unclassified"
)

// RecordCredentialScopeAdmission records one admitted request against the scope inventory.
//
// It takes the facts rather than a session type so the caller cannot pass something that
// disagrees with what was admitted, and so this stays callable from the one place that has
// both facts at once.
func RecordCredentialScopeAdmission(class string, scope string) {
	if class == "" {
		// An unclassified admission is exactly what section 11 calls "absent credential
		// classification", and counting it as programmatic would hide it inside a bucket an
		// operator reads as understood. It gets its own visible value instead.
		class = CredentialClassUnclassified
	}
	if strings.TrimSpace(scope) == "" {
		unscopedCredentialAdmissions.WithLabelValues(class).Inc()
		return
	}
	scopedCredentialAdmissions.WithLabelValues(class).Inc()
}

// CredentialScopeInventoryCollectors are the inventory counters, for a process whose metrics
// registry is private.
//
// The HTTP host scrapes the default registry, so it needs nothing; the Runtime process builds its
// own and serves only what it is handed, which means a metric it is not handed is invisible there
// rather than merely unexported. Review found exactly that gap.
func CredentialScopeInventoryCollectors() []prometheus.Collector {
	return []prometheus.Collector{unscopedCredentialAdmissions, scopedCredentialAdmissions}
}
