package httpd

import (
	"errors"
	"fmt"
	"net/http"
	"runtime/debug"
	"strings"
	"time"

	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/jwtauth/v5"
	"github.com/lestrrat-go/jwx/jwt"
	"github.com/rs/xid"

	"github.com/drakkan/sftpgo/v2/common"
	"github.com/drakkan/sftpgo/v2/dataprovider"
	"github.com/drakkan/sftpgo/v2/logger"
	"github.com/drakkan/sftpgo/v2/sdk"
	"github.com/drakkan/sftpgo/v2/util"
)

var (
	forwardedProtoKey = &contextKey{"forwarded proto"}
	errInvalidToken   = errors.New("invalid JWT token")
)

type contextKey struct {
	name string
}

func (k *contextKey) String() string {
	return "context value " + k.name
}

func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
	token, _, err := jwtauth.FromContext(r.Context())

	var redirectPath string
	if audience == tokenAudienceWebAdmin {
		redirectPath = webLoginPath
	} else {
		redirectPath = webClientLoginPath
	}

	isAPIToken := (audience == tokenAudienceAPI || audience == tokenAudienceAPIUser)

	if err != nil || token == nil {
		logger.Debug(logSender, "", "error getting jwt token: %v", err)
		if isAPIToken {
			sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		} else {
			http.Redirect(w, r, redirectPath, http.StatusFound)
		}
		return errInvalidToken
	}

	err = jwt.Validate(token)
	if err != nil {
		logger.Debug(logSender, "", "error validating jwt token: %v", err)
		if isAPIToken {
			sendAPIResponse(w, r, err, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		} else {
			http.Redirect(w, r, redirectPath, http.StatusFound)
		}
		return errInvalidToken
	}
	if isTokenInvalidated(r) {
		logger.Debug(logSender, "", "the token has been invalidated")
		if isAPIToken {
			sendAPIResponse(w, r, nil, "Your token is no longer valid", http.StatusUnauthorized)
		} else {
			http.Redirect(w, r, redirectPath, http.StatusFound)
		}
		return errInvalidToken
	}
	// a user with a partial token will be always redirected to the appropriate two factor auth page
	if err := checkPartialAuth(w, r, audience, token.Audience()); err != nil {
		return err
	}
	if !util.IsStringInSlice(audience, token.Audience()) {
		logger.Debug(logSender, "", "the token is not valid for audience %#v", audience)
		if isAPIToken {
			sendAPIResponse(w, r, nil, "Your token audience is not valid", http.StatusUnauthorized)
		} else {
			http.Redirect(w, r, redirectPath, http.StatusFound)
		}
		return errInvalidToken
	}
	return nil
}

func validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
	token, _, err := jwtauth.FromContext(r.Context())
	var notFoundFunc func(w http.ResponseWriter, r *http.Request, err error)
	if audience == tokenAudienceWebAdminPartial {
		notFoundFunc = renderNotFoundPage
	} else {
		notFoundFunc = renderClientNotFoundPage
	}
	if err != nil || token == nil || jwt.Validate(token) != nil {
		notFoundFunc(w, r, nil)
		return errInvalidToken
	}
	if isTokenInvalidated(r) {
		notFoundFunc(w, r, nil)
		return errInvalidToken
	}
	if !util.IsStringInSlice(audience, token.Audience()) {
		logger.Debug(logSender, "", "the token is not valid for audience %#v", audience)
		notFoundFunc(w, r, nil)
		return errInvalidToken
	}

	return nil
}

func jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if err := validateJWTPartialToken(w, r, audience); err != nil {
				return
			}

			// Token is authenticated, pass it through
			next.ServeHTTP(w, r)
		})
	}
}

func jwtAuthenticatorAPI(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := validateJWTToken(w, r, tokenAudienceAPI); err != nil {
			return
		}

		// Token is authenticated, pass it through
		next.ServeHTTP(w, r)
	})
}

func jwtAuthenticatorAPIUser(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := validateJWTToken(w, r, tokenAudienceAPIUser); err != nil {
			return
		}

		// Token is authenticated, pass it through
		next.ServeHTTP(w, r)
	})
}

func jwtAuthenticatorWebAdmin(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := validateJWTToken(w, r, tokenAudienceWebAdmin); err != nil {
			return
		}

		// Token is authenticated, pass it through
		next.ServeHTTP(w, r)
	})
}

func jwtAuthenticatorWebClient(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := validateJWTToken(w, r, tokenAudienceWebClient); err != nil {
			return
		}

		// Token is authenticated, pass it through
		next.ServeHTTP(w, r)
	})
}

func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			_, claims, err := jwtauth.FromContext(r.Context())
			if err != nil {
				if isWebRequest(r) {
					renderClientBadRequestPage(w, r, err)
				} else {
					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
				}
				return
			}
			tokenClaims := jwtTokenClaims{}
			tokenClaims.Decode(claims)
			// for web client perms are negated and not granted
			if tokenClaims.hasPerm(perm) {
				if isWebRequest(r) {
					renderClientForbiddenPage(w, r, "You don't have permission for this action")
				} else {
					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
				}
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func checkPerm(perm string) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			_, claims, err := jwtauth.FromContext(r.Context())
			if err != nil {
				if isWebRequest(r) {
					renderBadRequestPage(w, r, err)
				} else {
					sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
				}
				return
			}
			tokenClaims := jwtTokenClaims{}
			tokenClaims.Decode(claims)

			if !tokenClaims.hasPerm(perm) {
				if isWebRequest(r) {
					renderForbiddenPage(w, r, "You don't have permission for this action")
				} else {
					sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
				}
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func verifyCSRFHeader(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tokenString := r.Header.Get(csrfHeaderToken)
		token, err := jwtauth.VerifyToken(csrfTokenAuth, tokenString)
		if err != nil || token == nil {
			logger.Debug(logSender, "", "error validating CSRF header: %v", err)
			sendAPIResponse(w, r, err, "Invalid token", http.StatusForbidden)
			return
		}

		if !util.IsStringInSlice(tokenAudienceCSRF, token.Audience()) {
			logger.Debug(logSender, "", "error validating CSRF header audience")
			sendAPIResponse(w, r, errors.New("the token is not valid"), "", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func checkAPIKeyAuth(tokenAuth *jwtauth.JWTAuth, scope dataprovider.APIKeyScope) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			apiKey := r.Header.Get("X-SFTPGO-API-KEY")
			if apiKey == "" {
				next.ServeHTTP(w, r)
				return
			}
			keyParams := strings.SplitN(apiKey, ".", 3)
			if len(keyParams) < 2 {
				logger.Debug(logSender, "", "invalid api key %#v", apiKey)
				sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
				return
			}
			keyID := keyParams[0]
			key := keyParams[1]
			apiUser := ""
			if len(keyParams) > 2 {
				apiUser = keyParams[2]
			}

			k, err := dataprovider.APIKeyExists(keyID)
			if err != nil {
				logger.Debug(logSender, "invalid api key %#v: %v", apiKey, err)
				sendAPIResponse(w, r, errors.New("the provided api key is not valid"), "", http.StatusBadRequest)
				return
			}
			if err := k.Authenticate(key); err != nil {
				logger.Debug(logSender, "unable to authenticate api key %#v: %v", apiKey, err)
				sendAPIResponse(w, r, fmt.Errorf("the provided api key cannot be authenticated"), "", http.StatusUnauthorized)
				return
			}
			if scope == dataprovider.APIKeyScopeAdmin {
				if k.Admin != "" {
					apiUser = k.Admin
				}
				if err := authenticateAdminWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {
					logger.Debug(logSender, "", "unable to authenticate admin %#v associated with api key %#v: %v",
						apiUser, apiKey, err)
					sendAPIResponse(w, r, fmt.Errorf("the admin associated with the provided api key cannot be authenticated"),
						"", http.StatusUnauthorized)
					return
				}
			} else {
				if k.User != "" {
					apiUser = k.User
				}
				if err := authenticateUserWithAPIKey(apiUser, keyID, tokenAuth, r); err != nil {
					logger.Debug(logSender, "", "unable to authenticate user %#v associated with api key %#v: %v",
						apiUser, apiKey, err)
					code := http.StatusUnauthorized
					if errors.Is(err, common.ErrInternalFailure) {
						code = http.StatusInternalServerError
					}
					sendAPIResponse(w, r, errors.New("the user associated with the provided api key cannot be authenticated"),
						"", code)
					return
				}
			}
			dataprovider.UpdateAPIKeyLastUse(&k) //nolint:errcheck

			next.ServeHTTP(w, r)
		})
	}
}

func forbidAPIKeyAuthentication(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, err := getTokenClaims(r)
		if err != nil || claims.Username == "" {
			sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
			return
		}
		if claims.APIKeyID != "" {
			sendAPIResponse(w, r, nil, "API key authentication is not allowed", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func recoverer(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rvr := recover(); rvr != nil {
				if rvr == http.ErrAbortHandler {
					panic(rvr)
				}

				logEntry := middleware.GetLogEntry(r)
				if logEntry != nil {
					logEntry.Panic(rvr, debug.Stack())
				} else {
					middleware.PrintPrettyStack(rvr)
				}

				w.WriteHeader(http.StatusInternalServerError)
			}
		}()

		next.ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

func authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAuth, r *http.Request) error {
	if username == "" {
		return errors.New("the provided key is not associated with any admin and no username was provided")
	}
	admin, err := dataprovider.AdminExists(username)
	if err != nil {
		return err
	}
	if !admin.Filters.AllowAPIKeyAuth {
		return fmt.Errorf("API key authentication disabled for admin %#v", admin.Username)
	}
	if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
		return err
	}
	c := jwtTokenClaims{
		Username:    admin.Username,
		Permissions: admin.Permissions,
		Signature:   admin.GetSignature(),
		APIKeyID:    keyID,
	}

	resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPI)
	if err != nil {
		return err
	}
	r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
	dataprovider.UpdateAdminLastLogin(&admin)
	return nil
}

func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAuth, r *http.Request) error {
	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
	if username == "" {
		err := errors.New("the provided key is not associated with any user and no username was provided")
		updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
		return err
	}
	if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
		return err
	}
	user, err := dataprovider.UserExists(username)
	if err != nil {
		updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
		return err
	}
	if !user.Filters.AllowAPIKeyAuth {
		err := fmt.Errorf("API key authentication disabled for user %#v", user.Username)
		updateLoginMetrics(&user, ipAddr, err)
		return err
	}
	if err := user.CheckLoginConditions(); err != nil {
		updateLoginMetrics(&user, ipAddr, err)
		return err
	}
	connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
	if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
		updateLoginMetrics(&user, ipAddr, err)
		return err
	}
	lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
	diff := -time.Until(lastLogin)
	if diff < 0 || diff > 10*time.Minute {
		defer user.CloseFs() //nolint:errcheck
		err = user.CheckFsRoot(connectionID)
		if err != nil {
			updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
			return common.ErrInternalFailure
		}
	}
	c := jwtTokenClaims{
		Username:    user.Username,
		Permissions: user.Filters.WebClient,
		Signature:   user.GetSignature(),
		APIKeyID:    keyID,
	}

	resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPIUser)
	if err != nil {
		updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
		return err
	}
	r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
	dataprovider.UpdateLastLogin(&user)
	updateLoginMetrics(&user, ipAddr, nil)

	return nil
}

func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, tokenAudience []string) error {
	if audience == tokenAudienceWebAdmin && util.IsStringInSlice(tokenAudienceWebAdminPartial, tokenAudience) {
		http.Redirect(w, r, webAdminTwoFactorPath, http.StatusFound)
		return errInvalidToken
	}
	if audience == tokenAudienceWebClient && util.IsStringInSlice(tokenAudienceWebClientPartial, tokenAudience) {
		http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
		return errInvalidToken
	}
	return nil
}
