package middleware

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

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

const (
	headerAuthorization = "Authorization"
	headerCacheControl  = "Cache-Control"
	cacheControlNoStore = "no-store"

	// HeaderRefreshedToken is reserved by auth middleware for an optional raw
	// replacement bearer token after authentication succeeds.
	HeaderRefreshedToken = "X-Refreshed-Token"
)

type disableSessionRenewalContextKey struct{}

var (
	errInvalidSessionControlRequest    = errors.New("invalid session-control request")
	errInvalidSessionControlValidation = errors.New("invalid session-control validation result")
)

type sessionValidation struct {
	claims               *hostruntime.SessionClaims
	refreshedBearerToken string
	sessionControlCookie *http.Cookie
}

type AuthChallengeContext struct {
	Path         string
	MissingToken bool
}

type AuthChallenge struct {
	Status  int
	Code    string
	Message string
}

type AuthChallengePolicy interface {
	AuthChallenge(AuthChallengeContext) (AuthChallenge, bool)
}

type AuthChallengePolicyFunc func(AuthChallengeContext) (AuthChallenge, bool)

func (f AuthChallengePolicyFunc) AuthChallenge(ctx AuthChallengeContext) (AuthChallenge, bool) {
	if f == nil {
		return AuthChallenge{}, false
	}
	return f(ctx)
}

// SessionContextWriter stores the validated session in the host's request
// context shape. The api-host auth middleware owns token handling; the backend
// adapter owns legacy gin keys and backend auth.UserSession projection.
type SessionContextWriter interface {
	WriteSession(c *gin.Context, user hostruntime.UserSession, sessionID string)
}

type SessionContextWriterFunc func(c *gin.Context, user hostruntime.UserSession, sessionID string)

func (f SessionContextWriterFunc) WriteSession(c *gin.Context, user hostruntime.UserSession, sessionID string) {
	if f != nil {
		f(c, user, sessionID)
	}
}

type AuthFailure struct {
	Status  int
	Code    string
	Message string
}

type AuthFailureResponder interface {
	WriteAuthFailure(c *gin.Context, failure AuthFailure)
}

type AuthFailureResponderFunc func(c *gin.Context, failure AuthFailure)

func (f AuthFailureResponderFunc) WriteAuthFailure(c *gin.Context, failure AuthFailure) {
	if f != nil {
		f(c, failure)
	}
}

// RequireAuth validates bearer tokens and installs the host session.
func RequireAuth(authSvc hostruntime.SessionAuthenticator, sessionWriter SessionContextWriter, failureResponder AuthFailureResponder, challengePolicies ...AuthChallengePolicy) gin.HandlerFunc {
	policies := append([]AuthChallengePolicy(nil), challengePolicies...)
	return func(c *gin.Context) {
		authHeader := c.GetHeader(headerAuthorization)
		if authHeader == "" {
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, true, policies))
			c.Abort()
			return
		}

		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}

		requestContext := c.Request.Context()
		validation, err := validateSession(requestContext, authSvc, parts[1], refreshedTokenResponseCanRenew(requestContext) && !sessionRenewalDisabled(requestContext))
		if err != nil || validation.claims == nil {
			writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
			c.Abort()
			return
		}
		authSvc.TouchSession(c.Request.Context(), 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.sessionControlCookie != nil {
			if err := setSessionCredentialResponse(requestContext, validation.refreshedBearerToken, validation.sessionControlCookie); err != nil {
				writeAuthFailure(c, failureResponder, authFailure(c.Request.URL.Path, false, policies))
				c.Abort()
				return
			}
		} else {
			setRefreshedToken(requestContext, validation.refreshedBearerToken)
		}
		c.Next()
	}
}

// OptionalAuth loads the current session into request context when a valid
// Bearer token is present, but never aborts the request when the token is
// missing or invalid.
func OptionalAuth(authSvc hostruntime.SessionAuthenticator, sessionWriter SessionContextWriter) gin.HandlerFunc {
	return func(c *gin.Context) {
		if authSvc == nil {
			c.Next()
			return
		}

		authHeader := c.GetHeader(headerAuthorization)
		if authHeader == "" {
			c.Next()
			return
		}

		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			c.Next()
			return
		}

		requestContext := c.Request.Context()
		validation, err := validateSession(requestContext, authSvc, parts[1], refreshedTokenResponseCanRenew(requestContext) && !sessionRenewalDisabled(requestContext))
		if err != nil || validation.claims == nil {
			c.Next()
			return
		}
		authSvc.TouchSession(c.Request.Context(), 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.sessionControlCookie != nil {
			if err := setSessionCredentialResponse(requestContext, validation.refreshedBearerToken, validation.sessionControlCookie); err != nil {
				c.Next()
				return
			}
		} else {
			setRefreshedToken(requestContext, validation.refreshedBearerToken)
		}
		c.Next()
	}
}

// WithoutSessionRenewal marks an endpoint whose response contract cannot carry
// a replacement bearer credential, such as a route that unconditionally
// switches protocols. Register it before RequireAuth or OptionalAuth. Renewal
// is never disabled solely from client-supplied request headers.
func WithoutSessionRenewal() gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := context.WithValue(c.Request.Context(), disableSessionRenewalContextKey{}, true)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

func sessionRenewalDisabled(ctx context.Context) bool {
	disabled, _ := ctx.Value(disableSessionRenewalContextKey{}).(bool)
	return disabled
}

func validateSession(ctx context.Context, authSvc hostruntime.SessionAuthenticator, bearerToken string, allowRenewal bool) (sessionValidation, error) {
	if sessionControlRequestAttempted(ctx) && !SessionControlRequestAdmitted(ctx) {
		return sessionValidation{}, errInvalidSessionControlRequest
	}
	if controlAuth, ok := authSvc.(hostruntime.SessionControlAuthenticator); allowRenewal && ok && SessionControlRequestAdmitted(ctx) {
		proof, _ := SessionControlProof(ctx)
		result, err := controlAuth.ValidateSessionWithControl(ctx, bearerToken, proof)
		if err != nil {
			return sessionValidation{}, err
		}
		hasReplacement := result.RefreshedBearerToken != ""
		hasExpiry := !result.ProofCookieExpiresAt.IsZero()
		if proof == "" && (hasReplacement || hasExpiry) || hasReplacement != hasExpiry ||
			hasReplacement && !validRefreshedToken(result.RefreshedBearerToken) {
			return sessionValidation{}, errInvalidSessionControlValidation
		}
		validation := sessionValidation{
			claims:               result.Claims,
			refreshedBearerToken: result.RefreshedBearerToken,
		}
		if hasExpiry {
			cookie, err := newSessionControlCookie(proof, result.ProofCookieExpiresAt, time.Now())
			if err != nil {
				return sessionValidation{}, errInvalidSessionControlValidation
			}
			validation.sessionControlCookie = cookie
		}
		return validation, nil
	}
	if renewalAuth, ok := authSvc.(hostruntime.SessionRenewingAuthenticator); allowRenewal && ok {
		claims, refreshed, err := renewalAuth.ValidateSessionWithRenewal(ctx, bearerToken)
		return sessionValidation{claims: claims, refreshedBearerToken: refreshed}, err
	}
	claims, err := authSvc.ValidateSession(ctx, bearerToken)
	return sessionValidation{claims: claims}, err
}

func authFailure(path string, missingToken bool, policies []AuthChallengePolicy) AuthFailure {
	ctx := AuthChallengeContext{Path: path, MissingToken: missingToken}
	for _, policy := range policies {
		if policy == nil {
			continue
		}
		if challenge, ok := policy.AuthChallenge(ctx); ok {
			status := challenge.Status
			if status == 0 {
				status = http.StatusUnauthorized
			}
			return AuthFailure{
				Status:  status,
				Code:    strings.TrimSpace(challenge.Code),
				Message: strings.TrimSpace(challenge.Message),
			}
		}
	}
	return AuthFailure{Status: http.StatusUnauthorized}
}

func writeAuthFailure(c *gin.Context, responder AuthFailureResponder, failure AuthFailure) {
	if failure.Status == 0 {
		failure.Status = http.StatusUnauthorized
	}
	if responder != nil {
		responder.WriteAuthFailure(c, failure)
		return
	}
	c.Status(failure.Status)
}
