package middleware

import (
	"fmt"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
	"github.com/pingcap-inc/nexuscrm-api-host/dialecttransport"
)

// NewDialectCredentialStackProvider composes the admission stack for the versioned data API.
//
// It exists so dialect routes go through the same explicit credential-policy contract the app-blind
// host routes already use, instead of inheriting one unlabelled stack for a whole version group. That
// uniformity is a closure condition of the browser/programmatic split: as long as one route family
// declares its credential class and another simply inherits whatever middleware the group was given,
// there is no single place that says what a route accepts.
//
// It is deliberately behaviour-preserving. Every policy it answers resolves to exactly the stack these
// routes already run, so adopting the contract changes what is *declared*, not what is enforced.
// Making the cookie lane reach any of them is a separate step, gated on the UI ceasing to call the raw
// data endpoints — design section 11 rejects allowing the cookie on every versioned data route, and
// section 12 puts that migration in L1.
//
// It is a different instance from the browser-session family's provider, and that is the correct
// reading of one contract rather than a violation of it: a raw data route and a cookie-only session
// route need genuinely different stacks, and each provider refusing the other's policies is what keeps
// a route from acquiring a stack nobody chose for it.
func NewDialectCredentialStackProvider(
	dataAPIStack []gin.HandlerFunc,
) (dialecttransport.CredentialStackProvider, error) {
	if len(dataAPIStack) == 0 {
		// An empty stack would mount every data route with no admission at all. api-host panics on
		// that at registration, but failing here names the cause instead of surfacing it as a
		// mysterious boot panic from inside the binder.
		return nil, fmt.Errorf("dialect credential stack: the data API stack is required")
	}
	// Copied so a later append by the caller cannot reach inside the composed stack. The slice is
	// built by appending to a shared base, so this is not hypothetical.
	stack := append([]gin.HandlerFunc(nil), dataAPIStack...)

	return func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
		switch policy {
		case dialect.CredentialProgrammaticOnly:
			return append([]gin.HandlerFunc(nil), stack...)
		default:
			// Everything else fails the boot, and BrowserOrProgrammatic is deliberately in that set.
			//
			// An earlier version accepted it and answered with this same stack, which was worse than
			// refusing: that stack admits through the browser-lane-disabled authorization and requires
			// ApiEnabled, so a route declaring the browser lane would have had every browser request
			// silently rejected while appearing to be configured for them. A policy this provider
			// cannot honour has to be a refusal, not an approximation.
			//
			// Establishment, termination and browser-only belong to the session family's own provider
			// and have no meaning on the data API. Panicking at mount time makes a misdeclared route a
			// boot failure rather than a route serving traffic under a stack chosen by accident.
			panic(fmt.Sprintf(
				"dialect credential stack: asked to compose credential policy %s. "+
					"The versioned data API composes only programmatic-only admission; a route that "+
					"needs the browser lane cannot be served by this stack, and the browser-session "+
					"family has its own provider",
				policy,
			))
		}
	}, nil
}

// DialectDataAPICredentialPolicy is the only policy this stack can serve.
//
// It is exported so a test can name it, not so a caller can choose it: no route resolver is installed,
// and api-host's own default is this same value. Which of a product's routes are browser-capable is
// part of that product's route grammar and therefore an L1 declaration — but a declaration channel is
// only useful once there is a stack that can honour more than one answer, so it arrives with that
// stack rather than ahead of it.
func DialectDataAPICredentialPolicy() dialect.CredentialPolicy {
	return dialect.CredentialProgrammaticOnly
}
