package middleware

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

var errInvalidAuthorizingSessionAdmission = errors.New("invalid authorizing session admission")

// AuthorizingSessionMiddlewareOptions supplies only strong admission and
// aggregate capabilities. Legacy SessionAuthenticator and PermissionProvider
// deliberately cannot satisfy this composition.
type AuthorizingSessionMiddlewareOptions struct {
	Admitter          hostruntime.AuthorizingSessionAdmitter
	SnapshotProvider  hostruntime.EffectivePermissionSnapshotProvider
	SnapshotCache     *PermissionStateCache
	SessionWriter     SessionContextWriter
	PermissionContext PermissionSessionContext
	RequestContext    hostruntime.PermissionRequestContext
	FailureResponder  AuthFailureResponder

	// BrowserSessionLane admits a first-party request that presents a usable
	// session-control proof and no Authorization header at all.
	//
	// It is off by default, and that default is the whole compatibility story: a host
	// that does not set it sees byte-identical admission behaviour, because the only
	// requests it changes are the ones that are rejected today for having no bearer.
	//
	// Enabling it does not weaken any existing lane. A malformed bearer is still a
	// rejected credential rather than an invitation to fall back to the cookie, and the
	// cookie path requires first-party admission of its own — an ambient cookie is not
	// evidence of intent, which is the difference between a session and a CSRF.
	BrowserSessionLane bool
}

// AuthorizingSessionMiddleware owns the closed HTTP admission path.
type AuthorizingSessionMiddleware struct {
	admitter          hostruntime.AuthorizingSessionAdmitter
	snapshotProvider  hostruntime.EffectivePermissionSnapshotProvider
	snapshotCache     *PermissionStateCache
	sessionWriter     SessionContextWriter
	permissionContext PermissionSessionContext
	requestContext    hostruntime.PermissionRequestContext
	failureResponder  AuthFailureResponder
	browserLane       bool
}

// NewAuthorizingSessionMiddleware rejects incomplete production composition at
// construction time, before any route can activate the strong path.
func NewAuthorizingSessionMiddleware(
	options AuthorizingSessionMiddlewareOptions,
) (*AuthorizingSessionMiddleware, error) {
	if options.Admitter == nil {
		return nil, errors.New("authorizing session middleware: strong admitter is required")
	}
	if options.SnapshotProvider == nil {
		return nil, errors.New("authorizing session middleware: aggregate snapshot provider is required")
	}
	if !options.SnapshotCache.configured() {
		return nil, errors.New("authorizing session middleware: Permission State cache is required")
	}
	if options.SessionWriter == nil {
		return nil, errors.New("authorizing session middleware: session writer is required")
	}
	if options.PermissionContext == nil {
		return nil, errors.New("authorizing session middleware: permission context is required")
	}
	return &AuthorizingSessionMiddleware{
		admitter:          options.Admitter,
		snapshotProvider:  options.SnapshotProvider,
		snapshotCache:     options.SnapshotCache,
		sessionWriter:     options.SessionWriter,
		permissionContext: options.PermissionContext,
		requestContext:    hostruntime.NormalizePermissionRequestContext(options.RequestContext),
		failureResponder:  options.FailureResponder,
		browserLane:       options.BrowserSessionLane,
	}, nil
}

// RequireAuth admits one bearer, resolves a floor-satisfying aggregate, and
// installs both before protected handlers run.
func (middleware *AuthorizingSessionMiddleware) RequireAuth(
	challengePolicies ...AuthChallengePolicy,
) gin.HandlerFunc {
	policies := append([]AuthChallengePolicy(nil), challengePolicies...)
	return middleware.handler(true, policies)
}

// OptionalAuth preserves anonymous requests only when no valid credential is
// supplied. Once a bearer reaches strong admission, an authority outage aborts
// instead of silently degrading the request to anonymous semantics.
func (middleware *AuthorizingSessionMiddleware) OptionalAuth() gin.HandlerFunc {
	return middleware.handler(false, nil)
}

func (middleware *AuthorizingSessionMiddleware) handler(
	required bool,
	challengePolicies []AuthChallengePolicy,
) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Presence comes from the header map, not from the value. c.GetHeader collapses
		// an absent header and a present-but-empty one ("Authorization:") into the same
		// empty string, so deriving presence from the value would have let the emptiest
		// possible malformed credential reach the cookie lane — the exact rescue the
		// lane-separation rule below forbids.
		authorizationValues := c.Request.Header.Values(headerAuthorization)
		token, _, valid := authorizingBearerToken(c.GetHeader(headerAuthorization))
		present := len(authorizationValues) > 0
		bearerUsable := present && valid

		// A malformed bearer is a rejected credential, not an invitation to try the
		// cookie. Only the complete absence of an Authorization header can reach the
		// browser lane, so a caller cannot present a broken bearer and have an ambient
		// cookie silently rescue it.
		if !bearerUsable && !(middleware.browserLaneEnabled() && !present) {
			if required {
				writeAuthFailure(
					c,
					middleware.responder(),
					authFailure(c.Request.URL.Path, !present, challengePolicies),
				)
				c.Abort()
				return
			}
			c.Next()
			return
		}
		if middleware == nil ||
			middleware.admitter == nil ||
			middleware.snapshotProvider == nil ||
			middleware.snapshotCache == nil {
			middleware.writeAuthorityUnavailable(c)
			return
		}

		requestContext := middleware.requestContext.Install(c.Request.Context())
		c.Request = c.Request.WithContext(requestContext)

		var request hostruntime.AuthorizingSessionRequest
		if bearerUsable {
			request = authorizingSessionRequest(requestContext, token)
		} else {
			browserRequest, ok := browserAuthorizingSessionRequest(requestContext, c.Request.Header)
			if !ok {
				// No usable proof, or a proof presented without first-party evidence.
				// Both are the same answer as an absent credential: this is the branch
				// the request would have taken before the lane existed.
				if required {
					writeAuthFailure(
						c,
						middleware.responder(),
						authFailure(c.Request.URL.Path, true, challengePolicies),
					)
					c.Abort()
					return
				}
				c.Next()
				return
			}
			request = browserRequest
		}

		if sessionControlRequestAttempted(requestContext) &&
			!SessionControlRequestAdmitted(requestContext) {
			writeAuthFailure(
				c,
				middleware.responder(),
				authFailure(c.Request.URL.Path, false, challengePolicies),
			)
			c.Abort()
			return
		}

		admission, err := middleware.admitter.AdmitAuthorizingSession(
			requestContext,
			request,
		)
		if err != nil {
			if errors.Is(err, hostruntime.ErrAuthorizingCredentialRejected) {
				if required {
					writeAuthFailure(
						c,
						middleware.responder(),
						authFailure(c.Request.URL.Path, false, challengePolicies),
					)
					c.Abort()
					return
				}
				c.Next()
				return
			}
			middleware.writeAuthorityUnavailable(c)
			return
		}
		validation, err := validateAuthorizingSessionAdmission(request, admission)
		if err != nil {
			middleware.writeAuthorityUnavailable(c)
			return
		}

		snapshot, err := middleware.snapshotCache.Resolve(
			requestContext,
			middleware.snapshotProvider,
			admission.Subject,
			admission.PermissionFloor,
		)
		if err != nil {
			middleware.writeAuthorityUnavailable(c)
			return
		}
		requestContext, err = hostruntime.ContextWithAuthorizingPermissionState(
			requestContext,
			hostruntime.AuthorizingPermissionState{
				Subject:  admission.Subject,
				Floor:    admission.PermissionFloor,
				Snapshot: snapshot,
			},
		)
		if err != nil {
			middleware.writeAuthorityUnavailable(c)
			return
		}
		c.Request = c.Request.WithContext(requestContext)

		systemPermissions := permissionStatePermissionMap(snapshot.SystemPermissions())
		customPermissions := permissionStatePermissionMap(snapshot.CustomPermissions())
		user := validation.claims.User
		user.SystemPermissions = systemPermissions
		middleware.admitter.TouchSession(requestContext, validation.claims.SessionID)
		middleware.sessionWriter.WriteSession(c, user, validation.claims.SessionID)
		if c.IsAborted() {
			return
		}
		middleware.permissionContext.WriteCurrentSession(c, user)
		middleware.permissionContext.WritePermissions(c, systemPermissions, customPermissions)
		if c.IsAborted() {
			return
		}
		if c.Writer.Written() {
			c.Next()
			return
		}
		if validation.sessionControlCookie != nil {
			// Which owner writes the cookie depends on whether a bearer accompanies it,
			// and that is decided by the fact rather than by the mode so the two cannot
			// drift. Control pairs a cookie with a replacement bearer and its validator
			// requires both or neither; the browser lane forbids a bearer outright, so a
			// bearerless cookie is exactly its case — and setSessionCredentialResponse
			// rejects an empty bearer, which would have turned every browser renewal
			// into a 503 instead of reissuing the cookie.
			var err error
			if validation.refreshedBearerToken == "" {
				err = setSessionControlResponseCookie(requestContext, validation.sessionControlCookie)
			} else {
				err = setSessionCredentialResponse(
					requestContext,
					validation.refreshedBearerToken,
					validation.sessionControlCookie,
				)
			}
			if err != nil {
				middleware.writeAuthorityUnavailable(c)
				return
			}
		} else {
			setRefreshedToken(requestContext, validation.refreshedBearerToken)
		}
		c.Next()
	}
}

func authorizingBearerToken(header string) (token string, present bool, valid bool) {
	if header == "" {
		return "", false, false
	}
	parts := strings.SplitN(header, " ", 2)
	if len(parts) != 2 || parts[0] != "Bearer" || strings.TrimSpace(parts[1]) == "" {
		return "", true, false
	}
	return parts[1], true, true
}

func (middleware *AuthorizingSessionMiddleware) browserLaneEnabled() bool {
	return middleware != nil && middleware.browserLane
}

// browserAuthorizingSessionRequest builds a cookie-only admission request.
//
// It requires first-party admission itself rather than relying on the shared
// attempted/admitted check below. That check only bites once something declared intent, so
// a cookie presented with no intent header would slip past it — and an ambient cookie
// with no declared intent is precisely a cross-site request, which is the whole reason
// admission exists.
func browserAuthorizingSessionRequest(
	ctx context.Context,
	header http.Header,
) (hostruntime.AuthorizingSessionRequest, bool) {
	proof, ok := usableSessionControlProof(header)
	if !ok {
		return hostruntime.AuthorizingSessionRequest{}, false
	}
	if !SessionControlRequestAdmitted(ctx) {
		return hostruntime.AuthorizingSessionRequest{}, false
	}
	return hostruntime.AuthorizingSessionRequest{
		ControlProof: proof,
		Mode:         hostruntime.AuthorizingSessionBrowser,
	}, true
}

func authorizingSessionRequest(
	ctx context.Context,
	token string,
) hostruntime.AuthorizingSessionRequest {
	request := hostruntime.AuthorizingSessionRequest{
		BearerToken: token,
		Mode:        hostruntime.AuthorizingSessionStrict,
	}
	if !refreshedTokenResponseCanRenew(ctx) || sessionRenewalDisabled(ctx) {
		return request
	}
	if SessionControlRequestAdmitted(ctx) {
		request.Mode = hostruntime.AuthorizingSessionControl
		request.ControlProof, _ = SessionControlProof(ctx)
		return request
	}
	request.Mode = hostruntime.AuthorizingSessionRenewal
	return request
}

func validateAuthorizingSessionAdmission(
	request hostruntime.AuthorizingSessionRequest,
	admission hostruntime.AuthorizingSessionAdmission,
) (sessionValidation, error) {
	if !request.Mode.Valid() ||
		admission.Claims == nil ||
		strings.TrimSpace(admission.Claims.SessionID) == "" ||
		admission.Subject.Validate() != nil ||
		admission.PermissionFloor.Validate() != nil ||
		admission.Claims.User.ID != admission.Subject.UserID() ||
		admission.Claims.User.ProfileID != admission.Subject.ProfileID() {
		return sessionValidation{}, errInvalidAuthorizingSessionAdmission
	}
	hasReplacement := admission.RefreshedBearerToken != ""
	hasExpiry := !admission.ProofCookieExpiresAt.IsZero()
	validation := sessionValidation{
		claims:               admission.Claims,
		refreshedBearerToken: admission.RefreshedBearerToken,
	}
	switch request.Mode {
	case hostruntime.AuthorizingSessionStrict:
		if request.ControlProof != "" || hasReplacement || hasExpiry {
			return sessionValidation{}, errInvalidAuthorizingSessionAdmission
		}
	case hostruntime.AuthorizingSessionRenewal:
		if request.ControlProof != "" || hasExpiry ||
			hasReplacement && !validRefreshedToken(admission.RefreshedBearerToken) {
			return sessionValidation{}, errInvalidAuthorizingSessionAdmission
		}
	case hostruntime.AuthorizingSessionControl:
		if hasReplacement != hasExpiry ||
			request.ControlProof == "" && (hasReplacement || hasExpiry) ||
			hasReplacement && !validRefreshedToken(admission.RefreshedBearerToken) {
			return sessionValidation{}, errInvalidAuthorizingSessionAdmission
		}
		if hasExpiry {
			cookie, err := newSessionControlCookie(
				request.ControlProof,
				admission.ProofCookieExpiresAt,
				time.Now(),
			)
			if err != nil {
				return sessionValidation{}, errInvalidAuthorizingSessionAdmission
			}
			validation.sessionControlCookie = cookie
		}
	case hostruntime.AuthorizingSessionBrowser:
		// The browser lane may reissue its own cookie through a renewed expiry, and it
		// may never produce a bearer. hasReplacement being fatal here is the structural
		// guarantee that this lane cannot regress into handing the page a readable
		// credential: an admitter that returned one fails the request rather than
		// delivering it.
		if request.BearerToken != "" || request.ControlProof == "" || hasReplacement {
			return sessionValidation{}, errInvalidAuthorizingSessionAdmission
		}
		if hasExpiry {
			cookie, err := newSessionControlCookie(
				request.ControlProof,
				admission.ProofCookieExpiresAt,
				time.Now(),
			)
			if err != nil {
				return sessionValidation{}, errInvalidAuthorizingSessionAdmission
			}
			validation.sessionControlCookie = cookie
		}
	default:
		return sessionValidation{}, errInvalidAuthorizingSessionAdmission
	}
	return validation, nil
}

func permissionStatePermissionMap(permissions []string) map[string]bool {
	result := make(map[string]bool, len(permissions))
	for _, permission := range permissions {
		result[permission] = true
	}
	return result
}

func (middleware *AuthorizingSessionMiddleware) writeAuthorityUnavailable(c *gin.Context) {
	writeAuthFailure(c, middleware.responder(), AuthFailure{
		Status:  http.StatusServiceUnavailable,
		Code:    string(permissionstatev1.FailureAuthorityUnavailable),
		Message: "authorization authority unavailable",
	})
	c.Abort()
}

func (middleware *AuthorizingSessionMiddleware) responder() AuthFailureResponder {
	if middleware == nil {
		return nil
	}
	return middleware.failureResponder
}
