package middleware

import (
	"github.com/gin-gonic/gin"

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

// RequireBrowserSession authenticates an admitted first-party browser request from
// its session-control proof cookie alone.
//
// It is the cookie-only counterpart to RequireAuth. The two differ in what they
// will accept and in what they can hand back:
//
//   - the credential is the proof cookie, never the Authorization header. The lane
//     selector resolves that before anything is validated, so a caller cannot
//     present both and let whichever validates decide the lane;
//   - the request must already be admitted as first-party by session-control
//     admission. Authentication alone is not admission: without the intent header
//     and origin evidence, an ambient cookie would be usable from contexts the
//     browser attaches it to automatically;
//   - no bearer is ever produced. hostruntime.BrowserSessionValidation has no
//     bearer field, so this cannot regress into handing the page a JS-readable
//     credential, which is the entire point of the lane.
//
// A renewed durable session reissues the same proof cookie through its new expiry,
// which requires the handler to run inside ProtectSessionCredentialResponses. A
// renewal without that boundary fails the request rather than serving it without
// reissuing, so a session whose cookie should have been extended cannot expire
// unnoticed.
//
// The proof value itself is never regenerated here: rotation belongs to the session
// owner, on login and privilege change, not to an ordinary authenticated request.
//
// It takes no credential policy on purpose. This handler is the browser lane for a
// route that requires a browser principal, so it selects against
// CredentialBrowserOnly and there is no argument a caller could get wrong:
//
//   - a browser-or-programmatic route must not be handed this alone, because
//     choosing between the two lanes belongs to the per-route stack the credential
//     stack provider composes, not to one lane's handler;
//   - establishment has no principal to resolve, and termination must admit a
//     request whose proof grants nothing so a stale browser can still be cleaned
//     up. Both compose differently and neither can be expressed by tightening a
//     parameter here.
//
// Nothing mounts this yet.
func RequireBrowserSession(
	authSvc hostruntime.BrowserSessionAuthenticator,
	sessionWriter SessionContextWriter,
	failureResponder AuthFailureResponder,
	challengePolicies ...AuthChallengePolicy,
) gin.HandlerFunc {
	policies := append([]AuthChallengePolicy(nil), challengePolicies...)

	return func(c *gin.Context) {
		requestContext := c.Request.Context()

		// The zero-value options are the enforced final state: a browser-only route
		// rejects the Authorization header outright, so neither compatibility window
		// applies to this lane.
		selection := SelectRequestCredentialLane(c.Request.Header, dialect.CredentialBrowserOnly, CredentialSelectorOptions{})
		if selection.Lane != CredentialLaneBrowserSession {
			// Includes a presented Authorization header, a duplicated or malformed
			// cookie, and an absent credential. The selector already resolved
			// ambiguity, so nothing here needs to guess.
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}

		if !SessionControlRequestAdmitted(requestContext) {
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}

		proof, ok := usableSessionControlProof(c.Request.Header)
		if !ok {
			// Unreachable while the selector agrees with the shared parser; kept so
			// a divergence fails closed instead of authenticating on an empty proof.
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}

		validation, err := authSvc.ValidateBrowserSession(requestContext, proof)
		if err != nil || validation.Claims == nil {
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}

		authSvc.TouchSession(requestContext, validation.Claims.SessionID)
		if sessionWriter != nil {
			sessionWriter.WriteSession(c, validation.Claims.User, validation.Claims.SessionID)
		}
		if c.IsAborted() {
			return
		}
		if c.Writer.Written() {
			c.Next()
			return
		}

		if !validation.ProofCookieExpiresAt.IsZero() {
			// Reissue through the existing owner rather than building a cookie
			// here. IssueSessionControlResponse owns the cookie's attributes and
			// re-checks admission, so this cannot ship a proof cookie with weaker
			// flags than every other issue path.
			if err := IssueSessionControlResponse(requestContext, proof, validation.ProofCookieExpiresAt); err != nil {
				writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
				c.Abort()
				return
			}
		}

		c.Next()
	}
}
