package auth0

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

	"github.com/tidbcloud/pkgv2/auth/util"
)

var RawTokenKey util.ContextKey = "rawToken"

type HTTPMiddlewareBuilder struct {
	intoGetClaim    IntoGetClaim
	tv              *RawTokenValidator
	validators      []AuthValidatorID
	tokenExtractors []TokenExtractor
}

func NewHTTPMiddlewareBuilder(tv *RawTokenValidator, intoGetClaim IntoGetClaim) *HTTPMiddlewareBuilder {
	return &HTTPMiddlewareBuilder{
		intoGetClaim:    intoGetClaim,
		tv:              tv,
		validators:      make([]AuthValidatorID, 0),
		tokenExtractors: make([]TokenExtractor, 0),
	}
}

func (builder *HTTPMiddlewareBuilder) AcceptToken(e TokenExtractor) *HTTPMiddlewareBuilder {
	builder.tokenExtractors = append(builder.tokenExtractors, e)
	return builder
}

func (builder *HTTPMiddlewareBuilder) AcceptValidator(id AuthValidatorID) *HTTPMiddlewareBuilder {
	builder.validators = append(builder.validators, id)
	return builder
}

func (builder *HTTPMiddlewareBuilder) doAuth(r *http.Request) error {
	rawToken, err := TokenFromFirst(builder.tokenExtractors...)(r)
	if err != nil {
		return err
	}
	validatorID, claims, err := builder.tv.ValidateForMulti(builder.validators, rawToken)
	if err != nil {
		return err
	}
	v, err := builder.intoGetClaim.ToGetClaim()(r.Context(), claims)
	if err != nil {
		return err
	}

	newCtx := context.WithValue(r.Context(), AuthTokenClaimKey, v)
	newCtx = context.WithValue(newCtx, AuthValidatorIDKey, validatorID)
	newCtx = context.WithValue(newCtx, RawTokenKey, rawToken)
	newCtx = context.WithValue(newCtx, util.AuthProcessedByKey, AuthByID)

	newRequest := r.WithContext(newCtx)
	*r = *newRequest

	return nil
}

// MWTryAuth tries to authenticate the HTTP request according to predefined locations
// (e.g. Authorization header, cookie, etc).
// As a middleware, after a success auth, `AuthProcessedByKey` will be set.
// The middleware will not throw errors when auth failed, to allow other authentication methods to work.
// See `MWRequireAuth` for auth failure handling.
func (builder *HTTPMiddlewareBuilder) MWTryAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Don't authenticate an OPTIONS request
		if r.Method == http.MethodOptions {
			next.ServeHTTP(w, r)
			return
		}

		if r.Context().Value(util.AuthProcessedByKey) != nil {
			// Processed by other auth middlewares, skip.
			next.ServeHTTP(w, r)
			return
		}

		newCtx := context.WithValue(r.Context(), AuthHttpMethod, r.Method)
		newCtx = context.WithValue(newCtx, AuthHttpURI, r.URL.Path)
		r = r.WithContext(newCtx)

		if err := builder.doAuth(r); err != nil {
			// Cannot auth, skip.
			next.ServeHTTP(w, r)
			return
		}

		rawToken := r.Context().Value(RawTokenKey).(string)
		expire := time.Now().Add(20 * time.Minute) // Expires in 20 minutes
		cookie := http.Cookie{
			HttpOnly: true,
			Secure:   true,
			Name:     "jwt",
			Value:    rawToken,
			Path:     "/",
			Expires:  expire,
			MaxAge:   86400,
		}
		http.SetCookie(w, &cookie)

		next.ServeHTTP(w, r)
	})
}
