package middleware

import (
	"fmt"
	"net/http"

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

// NewBrowserCredentialStackProvider composes the admission stack for the cookie-only
// browser lane, and refuses to compose anything else.
//
// It is deliberately not the host's general credential stack provider. The browser
// lane authenticates a proof cookie and installs a session; it does **not** establish
// the effective-permission floor that AuthorizingSessionMiddleware installs for the
// bearer lane, and which every route whose handler consults permissions depends on.
// A provider that answered for a programmatic or browser-or-programmatic policy would
// therefore be able to mount a data route with a session but no floor — a silent
// authorization downgrade, and one that would look like a correctly registered route.
//
// So the unsupported policies are a boot failure rather than a fallback. The browser
// lane can only be composed for routes that were declared browser-only, and a route
// that needs a permission floor cannot reach this stack by any spelling. That is the
// whole reason this constructor is separate: the restriction is structural, not a rule
// someone has to remember when adding the next route.
//
// The browser policies do not share one stack. CredentialBrowserOnly resolves a
// principal from the cookie. Establishment has no principal to resolve — it is the route
// that creates one — so its stack enforces the lane and first-party admission and stops
// there. Termination must admit a request whose proof grants nothing, so a stale browser
// can still be cleaned up, and is added by the PR that mounts it.
func NewBrowserCredentialStackProvider(
	authSvc hostruntime.BrowserSessionAuthenticator,
	// browserAuthorization admits a browser-only route through the same
	// effective-permission floor the bearer lane uses. It must be an instance whose
	// BrowserSessionLane is on, and it must not be the instance applied to ordinary
	// routes: that one guards handlers which return readable credentials, and the whole
	// point of the browser lane is that the page holds none.
	browserAuthorization *AuthorizingSessionMiddleware,
	failureProjector func(
		c *gin.Context,
		status int,
		code string,
		message string,
	) (int, any),
	challengePolicies ...AuthChallengePolicy,
) (dialecttransport.CredentialStackProvider, error) {
	if browserAuthorization == nil {
		return nil, fmt.Errorf("browser credential stack: a browser-capable authorizing middleware is required")
	}
	if authSvc == nil {
		// Rejected at construction rather than at the first request: a nil
		// authenticator would deny every browser request, which reads like an expired
		// cookie and would be diagnosed as such for a long time.
		return nil, fmt.Errorf("browser credential stack: a browser session authenticator is required")
	}

	policies := append([]AuthChallengePolicy(nil), challengePolicies...)

	return func(policy dialect.CredentialPolicy) []gin.HandlerFunc {
		switch policy {
		case dialect.CredentialBrowserOnly:
			// Two handlers, and both are load-bearing.
			//
			// The first refuses anything that is not a first-party cookie request, which is
			// what the declared policy means. It is needed because the authorizing
			// middleware behind it accepts a bearer too — that is correct for a
			// browser-or-programmatic route and wrong for this one, and narrowing it there
			// would make one middleware answer to two policies.
			//
			// The second is the ordinary authorization path with its cookie-only lane
			// enabled, so the request arrives with the same Subject and Permission State
			// floor a bearer request would. RequireBrowserSession authenticates the proof
			// but installs no floor, so a route mounted on it alone would skip the
			// authorization every other authenticated route is subject to. That gap was
			// recorded when this family was first mounted; this closes it.
			return []gin.HandlerFunc{
				requireBrowserOnlyLane(
					backendAuthFailureResponder{projector: failureProjector},
				),
				browserAuthorization.RequireAuth(policies...),
			}
		case dialect.CredentialBrowserEstablishment:
			return []gin.HandlerFunc{requireBrowserEstablishment(
				backendAuthFailureResponder{projector: failureProjector},
			)}
		case dialect.CredentialBrowserTermination:
			return []gin.HandlerFunc{requireBrowserTermination(
				backendAuthFailureResponder{projector: failureProjector},
			)}
		default:
			// Reached only at mount time, so this cannot fail a live request: it fails
			// the boot that tried to register the route.
			panic(fmt.Sprintf(
				"browser credential stack: asked to compose credential policy %s. "+
					"This provider composes only the cookie-only browser lane, which installs "+
					"no effective-permission floor, so a route under any other policy must be "+
					"registered against the authorizing session stack instead",
				policy,
			))
		}
	}, nil
}

// requireBrowserEstablishment admits a first-party login attempt.
//
// There is no principal to resolve here, so this is admission only, and both checks are
// about who is allowed to *ask*:
//
//   - the lane comes from SelectRequestCredentialLane under
//     CredentialBrowserEstablishment, which refuses an Authorization header and ignores
//     rather than rejects a stale cookie, since establishment replaces any prior
//     session. Delegating means this cannot drift from the contract the selector owns;
//   - the request must be admitted as first-party. Establishment is the one route in the
//     family with no credential to check, so admission is the only thing between it and
//     any origin that can reach the host.
//
// api-host deliberately provides no establishment middleware — it says the browser
// policies compose differently and leaves composition to the host — so this is the host
// doing that, not a second implementation of something L0 already owns.
//
// It takes no challenge policies, and that is deliberate rather than a gap. A challenge
// tells a client how to authenticate; there is nothing to tell a caller who is already
// submitting a password, and a WWW-Authenticate on the login route would invite the
// browser's own basic-auth dialog. The resulting AuthFailure is byte-identical to what
// api-host produces when no policy matches, so this refuses exactly the way every other
// lane refuses by default.
func requireBrowserEstablishment(failureResponder backendAuthFailureResponder) gin.HandlerFunc {
	return func(c *gin.Context) {
		deny := func() {
			failureResponder.WriteAuthFailure(c, apihostmiddleware.AuthFailure{
				Status: http.StatusUnauthorized,
			})
			c.Abort()
		}

		if apihostmiddleware.SelectRequestCredentialLane(
			c.Request.Header,
			dialect.CredentialBrowserEstablishment,
			apihostmiddleware.CredentialSelectorOptions{},
		).Rejected() {
			deny()
			return
		}
		if !apihostmiddleware.SessionControlRequestAdmitted(c.Request.Context()) {
			deny()
			return
		}
		c.Next()
	}
}

// requireBrowserTermination admits a logout attempt.
//
// Its admission requirement is conditional, and the condition is the harm rather than the
// credential. A forced logout is dangerous because it forces *revocation*, and revocation
// only happens when the presented proof is usable — so a usable proof demands full
// first-party admission, exactly like any other state change.
//
// A request with nothing to revoke is a different case. The cookie is HttpOnly, so a
// browser holding a corrupt or expired one cannot remove it itself and this route is its
// only way out; design section 10 requires that to keep working. Requiring full admission
// there would strand it permanently, so the intent header alone is enough — there is no
// harm to force when there is nothing to revoke.
//
// Splitting on SessionControlRequestAdmitted directly would not express this: that
// predicate conflates "first-party request" with "the presented cookie is usable", which
// is why an earlier version of this rule stranded the corrupt-cookie case. The lane the
// selector resolves is what distinguishes them.
func requireBrowserTermination(failureResponder backendAuthFailureResponder) gin.HandlerFunc {
	return func(c *gin.Context) {
		deny := func() {
			failureResponder.WriteAuthFailure(c, apihostmiddleware.AuthFailure{
				Status: http.StatusUnauthorized,
			})
			c.Abort()
		}

		selection := apihostmiddleware.SelectRequestCredentialLane(
			c.Request.Header,
			dialect.CredentialBrowserTermination,
			apihostmiddleware.CredentialSelectorOptions{},
		)
		if selection.Rejected() {
			// Includes a presented Authorization header: a caller holding a bearer is
			// not the browser this route ends a session for.
			deny()
			return
		}

		requestContext := c.Request.Context()
		if selection.Lane == apihostmiddleware.CredentialLaneBrowserSession {
			if !apihostmiddleware.SessionControlRequestAdmitted(requestContext) {
				deny()
				return
			}
		} else if !apihostmiddleware.SessionControlRequestAttempted(requestContext) {
			// Still not open to anything that merely reaches the host: the intent header
			// is a header no cross-site form post can set.
			deny()
			return
		}
		c.Next()
	}
}

// requireBrowserOnlyLane refuses anything that is not an admitted first-party cookie
// request.
//
// It is the lane half of a browser-only route's admission; the authorizing middleware
// behind it owns the principal and the permission floor. Splitting them this way is what
// lets the floor come from the one middleware that knows how to establish it, without that
// middleware having to know which policy each route declared.
func requireBrowserOnlyLane(failureResponder backendAuthFailureResponder) gin.HandlerFunc {
	return func(c *gin.Context) {
		deny := func() {
			failureResponder.WriteAuthFailure(c, apihostmiddleware.AuthFailure{
				Status: http.StatusUnauthorized,
			})
			c.Abort()
		}

		// CredentialBrowserOnly rejects an Authorization header outright and resolves a
		// usable proof to the browser lane. Anything else — a duplicated or malformed
		// cookie, no credential at all — is not this route's caller.
		if apihostmiddleware.SelectRequestCredentialLane(
			c.Request.Header,
			dialect.CredentialBrowserOnly,
			apihostmiddleware.CredentialSelectorOptions{},
		).Lane != apihostmiddleware.CredentialLaneBrowserSession {
			deny()
			return
		}
		if !apihostmiddleware.SessionControlRequestAdmitted(c.Request.Context()) {
			deny()
			return
		}
		c.Next()
	}
}
