package middleware

import (
	"strings"

	"github.com/gin-gonic/gin"
	featureauth "github.com/nexuscrm/auth"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
)

// ContextKeyRateLimitSubject names the identity a programmatic request is rate-limited and audited
// as.
//
// It exists because "which Connected App is this" and "who do we throttle" stopped being the same
// question. A personal access token has no Connected App by design — section 8.1 permits that
// precisely when the PAT's own credential class and rate/audit subject are present — so a limiter
// keyed on the Connected App skipped PAT traffic entirely: `AppRateLimiter` treats an absent app as
// "direct login, no app rate limit". Long-lived, user-minted credentials were therefore the one
// programmatic class with no ceiling.
//
// The subject is qualified rather than bare, so a token id can never be mistaken for an app id in a
// counter, a log line or a limit bucket. Two subjects of different kinds that happened to share a
// raw id would otherwise share an allowance.
const ContextKeyRateLimitSubject = "rate_limit_subject"

// Rate-limit subject kinds. The prefix is part of the identity, not decoration.
const (
	rateLimitSubjectConnectedApp = "app:"
	rateLimitSubjectAPIToken     = "pat:"
)

// RateLimitSubjectForSession derives the subject a request should be limited as.
//
// A Connected App is the subject whenever there is one — that is the existing behaviour and it stays
// authoritative. A PAT is identified by its credential class, and its session id *is* the token id,
// which is what makes the token itself the throttled subject rather than its owner: two tokens minted
// by the same user are independently limited, and revoking one does not move the other's allowance.
//
// Anything else — an interactive session, a browser session — returns empty, which leaves the
// per-app limiter skipping exactly the traffic it skipped before.
func RateLimitSubjectForSession(user hostruntime.UserSession, sessionID string) string {
	if app := strings.TrimSpace(user.ConnectedAppID); app != "" {
		return rateLimitSubjectConnectedApp + app
	}
	if isAPITokenSession(user) {
		if token := strings.TrimSpace(sessionID); token != "" {
			return rateLimitSubjectAPIToken + token
		}
	}
	return ""
}

// isAPITokenSession reports whether the admitted credential was a personal access token.
//
// Both fields are checked because either alone is weaker than it looks: SessionClass is the explicit
// credential class the design requires a PAT to carry, and GrantType is what the admission has always
// set. Requiring one and accepting the other keeps a session that carries only half of the marking
// from being treated as a PAT.
func isAPITokenSession(user hostruntime.UserSession) bool {
	return strings.TrimSpace(user.SessionClass) == featureauth.ApiTokenGrantType ||
		strings.TrimSpace(user.GrantType) == featureauth.ApiTokenGrantType
}

// RateLimitSubject returns the subject this request is limited as, preferring the qualified subject
// and falling back to the Connected App id for anything that has not been through WriteSession.
func RateLimitSubject(c *gin.Context) string {
	if c == nil {
		return ""
	}
	if subject, exists := c.Get(ContextKeyRateLimitSubject); exists {
		if value, ok := subject.(string); ok && strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	// The Connected App path still writes only the bare id, and ScopeEnforcer in api-host owns that
	// call. Qualifying it here rather than there keeps this the one place that decides what a
	// subject looks like.
	if app, exists := c.Get(ContextKeyConnectedAppID); exists {
		if value, ok := app.(string); ok && strings.TrimSpace(value) != "" {
			return rateLimitSubjectConnectedApp + strings.TrimSpace(value)
		}
	}
	return ""
}
