package dialecttransport

import (
	"fmt"

	"github.com/gin-gonic/gin"

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

// CredentialStackProvider resolves the middleware stack that enforces one
// declared credential policy. It is the single stack-selection owner for every
// admission seam in this host: dialect manifest routes ask for it per route, and
// an app-blind host route asks for it through RegisterAuthenticatedRoute.
//
// The provider receives a policy, never a path, so route grammar stays with the
// declaring owner and no product prefix reaches this module.
//
// A provider must return a non-empty stack for every policy it is asked about.
// Returning an empty stack would mount an authenticated route with no admission
// at all, so callers treat it as a boot failure rather than serving the route.
type CredentialStackProvider func(policy dialect.CredentialPolicy) []gin.HandlerFunc

// RouteCredentialPolicyResolver declares the credential policy of one manifest
// route. The declaring owner supplies it, so a product keeps ownership of which
// of its routes are browser-capable and this module never derives a policy from a
// path.
//
// It is a resolver rather than a field on dialect.Route because dialect.Route is
// released public API: adding a field to it would break any consumer that builds
// one with an unkeyed composite literal. The policy still has exactly one
// declaring owner per route; only the carrier differs.
type RouteCredentialPolicyResolver func(dialectName string, route dialect.Route) dialect.CredentialPolicy

// WithRouteCredentialPolicyResolver supplies the per-route policy declarations the
// stack provider then resolves to a middleware stack.
//
// It must be paired with WithCredentialStackProvider. A resolver without a
// provider would compute policies that nothing enforces, which is exactly the
// silent downgrade this contract exists to prevent, so Mount fails closed on it.
func WithRouteCredentialPolicyResolver(resolver RouteCredentialPolicyResolver) MountOption {
	return func(opts *mountOptions) {
		opts.credentialPolicy = resolver
	}
}

// WithCredentialStackProvider selects a per-route stack for each manifest route
// from its declared policy, instead of applying one base stack to every route in
// a dialect version.
//
// Without this option Mount behaves exactly as before: the base stack is applied
// to the whole version group. That is what keeps this contract additive — a host
// that has not adopted the provider sees no change in composition or ordering.
// With it, the base stack is not applied to the group; the provider owns the whole
// admission stack for every route.
func WithCredentialStackProvider(provider CredentialStackProvider) MountOption {
	return func(opts *mountOptions) {
		opts.credentialStack = provider
	}
}

// resolveCredentialStack returns the stack for one route, failing closed rather
// than mounting an authenticated route the provider declined to describe.
func resolveCredentialStack(
	provider CredentialStackProvider,
	policy dialect.CredentialPolicy,
	subject string,
) []gin.HandlerFunc {
	stack := provider(policy)
	if len(stack) == 0 {
		panic(fmt.Sprintf(
			"dialecttransport: credential stack provider returned no middleware for %s under policy %s (an authenticated route must not mount without admission)",
			subject, policy,
		))
	}
	for _, handler := range stack {
		if handler == nil {
			panic(fmt.Sprintf(
				"dialecttransport: credential stack provider returned a nil middleware for %s under policy %s",
				subject, policy,
			))
		}
	}
	return stack
}

// RegisterAuthenticatedRoute mounts one app-blind host route under an explicitly
// declared credential policy, using the same provider the dialect binder uses.
//
// The policy is a required argument rather than a struct field, so a new host
// route cannot inherit an admission by leaving something unset: the author has to
// name the credential class the route accepts.
//
// This is the seam a generic browser-session route family uses. It carries no
// product path knowledge; the caller owns the path and this module owns which
// stack a declared policy resolves to.
func RegisterAuthenticatedRoute(
	router gin.IRouter,
	provider CredentialStackProvider,
	policy dialect.CredentialPolicy,
	method string,
	routePath string,
	handlers ...gin.HandlerFunc,
) {
	if router == nil {
		panic("dialecttransport: nil router for authenticated route registration")
	}
	if provider == nil {
		panic("dialecttransport: nil credential stack provider for authenticated route registration")
	}
	if !validHTTPMethod(method) {
		panic(fmt.Sprintf("dialecttransport: authenticated route %q declares invalid HTTP method %q", routePath, method))
	}
	if !policy.Valid() {
		panic(fmt.Sprintf(
			"dialecttransport: authenticated route %s %s declares unknown credential policy %s",
			method, routePath, policy,
		))
	}
	if len(handlers) == 0 {
		panic(fmt.Sprintf("dialecttransport: authenticated route %s %s declares no handler", method, routePath))
	}
	for _, handler := range handlers {
		if handler == nil {
			panic(fmt.Sprintf("dialecttransport: authenticated route %s %s declares a nil handler", method, routePath))
		}
	}

	subject := fmt.Sprintf("host route %s %s", method, routePath)
	stack := resolveCredentialStack(provider, policy, subject)
	router.Handle(method, routePath, append(append([]gin.HandlerFunc(nil), stack...), handlers...)...)
}
