package auth

import (
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
)

// CredentialScopeObservation is one admitted credential's scope, reported for inventory.
//
// Design section 11 requires that before the empty-scope rule can change — from "empty means
// full" to a contextual rule — the existing unscoped programmatic credentials get inventory,
// operator-visible metrics and a runbook. They do not earn a permanent legacy bypass. This is
// the fact that inventory is built from.
type CredentialScopeObservation struct {
	// Class distinguishes what an operator has to act on differently: a browser session's
	// empty scope will be reinterpreted rather than denied, while a programmatic one is the
	// migration backlog.
	Class string
	// Scope is the credential's OAuth scope as admitted. Empty is the case being inventoried.
	Scope string
}

// Credential classes reported to the observer.
const (
	// CredentialScopeClassBrowser is a cookie-only first-party admission: the proof was the whole
	// credential and no bearer was presented. An empty scope here is the expected end state, so
	// this line is not the backlog.
	CredentialScopeClassBrowser = "browser"
	// CredentialScopeClassBrowserBearer is a browser session admitted from a *bearer* — the
	// transitional population, whether or not a proof cookie accompanied it.
	//
	// It has its own value because it is the number the readable-credential removal has to drive
	// to zero, and folding it into "browser" would hide it in the bucket an operator reads as the
	// expected end state. Classifying by what the session row carries did exactly that: a legacy
	// browser bearer lands on a session that has a verifier, so it looked like a cookie-only
	// admission when it is the opposite.
	CredentialScopeClassBrowserBearer = "browser_bearer"
	// CredentialScopeClassProgrammatic is a bearer or delegated admission with no browser
	// character at all.
	CredentialScopeClassProgrammatic = "programmatic"
	// CredentialScopeClassAPIToken is a personal access token: long-lived, user-minted, and
	// the most urgent part of the backlog.
	CredentialScopeClassAPIToken = "api_token"
)

// CredentialScopeObserver receives one observation per credential the authority grants.
//
// "Grants" is the boundary, not "request served". Callers can refuse afterwards on grounds this
// module cannot see, and following them does not converge — any consumer can add a check. A
// credential rejected by a transport for an epoch or consistency mismatch is already broken for
// reasons unrelated to scope, so excluding it would not change the migration backlog.
//
// It is a port rather than a metric because the scope fact is known here — this module owns
// admission — while the metric backend belongs to the host. Implementations must not block or
// fail the request: an inventory that can break admission is worse than no inventory.
type CredentialScopeObserver func(CredentialScopeObservation)

// SetCredentialScopeObserver installs the observer. A nil observer disables reporting, which is
// the default, so a deployment that has not wired metrics is unaffected.
func (s *AuthService) SetCredentialScopeObserver(observer CredentialScopeObserver) {
	if s == nil {
		return
	}
	s.credentialScopeObserver = observer
}

// observeCredentialScope reports one admission.
//
// Called from the single validating owner of durable session state, which every session
// admission now passes through, so coverage is a property of the call graph rather than of a
// list someone maintains. An earlier attempt instrumented each transport instead and had to
// classify five call sites by hand; three of those classifications were wrong, because a single
// file can hold both a client-presented admission and the server verifying its own work.
func (s *AuthService) observeCredentialScope(class string, scope string) {
	if s == nil || s.credentialScopeObserver == nil {
		return
	}
	s.credentialScopeObserver(CredentialScopeObservation{Class: class, Scope: scope})
}

// bearerAdmissionCredentialClass classifies a *bearer* admission.
//
// The class has to come from how the credential was presented, not from what the session row
// carries. Deriving it from the row put the legacy browser bearer — a bearer admission on a session
// that also has a proof cookie — into the cookie-only bucket, which is the population the migration
// exists to remove, hidden in the bucket that means "already done".
//
// A row with a verifier means the session is browser-shaped; combined with a bearer having been
// presented, that is the transitional state. Without a verifier it is an ordinary programmatic
// session.
func bearerAdmissionCredentialClass(session models.SystemSession) string {
	if session.ControlProofHash != nil && strings.TrimSpace(*session.ControlProofHash) != "" {
		return CredentialScopeClassBrowserBearer
	}
	return CredentialScopeClassProgrammatic
}
