package handlers

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"

	"github.com/authelia/authelia/v4/internal/middlewares"
	"github.com/authelia/authelia/v4/internal/model"
	"github.com/authelia/authelia/v4/internal/session"
	"github.com/authelia/authelia/v4/internal/templates"
	"github.com/authelia/authelia/v4/internal/utils"
)

// ResetPasswordDELETE handler for deleting password reset JWT's.
func ResetPasswordDELETE(ctx *middlewares.AutheliaCtx) {
	var (
		token        *jwt.Token
		verification *model.IdentityVerification
		claims       *model.IdentityVerificationClaim
		ok           bool
		err          error
	)

	body := &bodyRequestPasswordResetDELETE{}

	if err = ctx.ParseBody(body); err != nil {
		ctx.Error(fmt.Errorf("error occurred parsing reset password delete body: %w", err), messageOperationFailed)
		return
	}

	token, err = jwt.ParseWithClaims(body.Token, &model.IdentityVerificationClaim{},
		func(token *jwt.Token) (any, error) {
			return []byte(ctx.Configuration.IdentityValidation.ResetPassword.JWTSecret), nil
		},
		jwt.WithIssuedAt(),
		jwt.WithIssuer("Authelia"),
		jwt.WithStrictDecoding(),
		ctx.GetJWTWithTimeFuncOption(),
	)

	switch {
	case err == nil:
		break
	case errors.Is(err, jwt.ErrTokenMalformed):
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token as it appears to be malformed, this potentially can occur if you've not copied the full link")
		ctx.SetJSONError(messageOperationFailed)

		return
	case errors.Is(err, jwt.ErrTokenExpired):
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token validity period as it appears to be expired")
		ctx.SetJSONError(messageOperationFailed)

		return
	case errors.Is(err, jwt.ErrTokenNotValidYet):
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token validity period as it appears to only be valid in the future")
		ctx.SetJSONError(messageOperationFailed)

		return
	case errors.Is(err, jwt.ErrTokenSignatureInvalid):
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token signature")
		ctx.SetJSONError(messageOperationFailed)

		return
	default:
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	if claims, ok = token.Claims.(*model.IdentityVerificationClaim); !ok {
		ctx.Logger.WithError(fmt.Errorf("failed to map the %T claims to a *model.IdentityVerificationClaim", claims)).Error("Error occurred validating the identity verification token claims")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	if verification, err = claims.ToIdentityVerification(); err != nil {
		ctx.Logger.WithError(err).Error("Error occurred validating the identity verification token claims as they appear to be malformed")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	if verification.Action != ActionResetPassword {
		ctx.Logger.Errorf("Error occurred revoking the identity verification token, the token action '%s' does not match the endpoint action '%s' which is not allowed", claims.Action, ActionResetPassword)
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	var full *model.IdentityVerification

	if full, err = ctx.Providers.StorageProvider.LoadIdentityVerification(ctx, verification.JTI.String()); err != nil {
		ctx.Logger.WithError(err).Error("Error occurred looking up identity verification during the revocation phase")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	if full.RevokedAt.Valid {
		ctx.Logger.Error("Error occurred revoking identity verification token as it's already revoked")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	if err = ctx.Providers.StorageProvider.RevokeIdentityVerification(ctx, verification.JTI.String(), model.NewNullIP(ctx.RemoteIP())); err != nil {
		ctx.Logger.WithError(err).Error("Error occurred revoking identity verification when attempting to save the revocation status to the database")
		ctx.SetJSONError(messageOperationFailed)

		return
	}

	ctx.ReplyOK()
}

// ResetPasswordPOST handler for resetting passwords.
func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
	var (
		userSession session.UserSession
		err         error
	)
	if userSession, err = ctx.GetSession(); err != nil {
		ctx.Error(fmt.Errorf("error occurred retrieving session for user: %w", err), messageUnableToResetPassword)
		return
	}

	// Those checks unsure that the identity verification process has been initiated and completed successfully
	// otherwise PasswordReset would not be set to true. We can improve the security of this check by making the
	// request expire at some point because here it only expires when the cookie expires.
	if userSession.PasswordResetUsername == nil {
		ctx.Error(fmt.Errorf("no identity verification process has been initiated"), messageUnableToResetPassword)
		return
	}

	username := *userSession.PasswordResetUsername

	var requestBody resetPasswordStep2RequestBody

	if err = ctx.ParseBody(&requestBody); err != nil {
		ctx.Error(err, messageUnableToResetPassword)
		return
	}

	if err = ctx.Providers.PasswordPolicy.Check(requestBody.Password); err != nil {
		ctx.Error(err, messagePasswordWeak)
		return
	}

	if err = ctx.Providers.UserProvider.UpdatePassword(username, requestBody.Password); err != nil {
		switch {
		case utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityCodes),
			utils.IsStringInSliceContains(err.Error(), ldapPasswordComplexityErrors):
			ctx.Error(err, ldapPasswordComplexityCode)
		default:
			ctx.Error(err, messageUnableToResetPassword)
		}

		return
	}

	ctx.Logger.Debugf("Password of user %s has been reset", username)

	// Reset the request.
	userSession.PasswordResetUsername = nil

	if err = ctx.SaveSession(userSession); err != nil {
		ctx.Error(fmt.Errorf("unable to update password reset state: %w", err), messageOperationFailed)
		return
	}

	// Send Notification.
	userInfo, err := ctx.Providers.UserProvider.GetDetails(username)
	if err != nil {
		ctx.Logger.Error(err)
		ctx.ReplyOK()

		return
	}

	if len(userInfo.Emails) == 0 {
		ctx.Logger.Error(fmt.Errorf("user %s has no email address configured", username))
		ctx.ReplyOK()

		return
	}

	data := templates.EmailEventValues{
		Title:       "Password changed successfully",
		DisplayName: userInfo.DisplayName,
		RemoteIP:    ctx.RemoteIP().String(),
		Details: map[string]any{
			"Action": "Password Reset",
		},
		BodyPrefix: eventEmailActionPasswordModifyPrefix,
		BodyEvent:  eventEmailActionPasswordReset,
		BodySuffix: eventEmailActionPasswordModifySuffix,
	}

	addresses := userInfo.Addresses()

	ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
		username, addresses[0].String())

	if err = ctx.Providers.Notifier.Send(ctx, addresses[0], "Password changed successfully", ctx.Providers.Templates.GetEventEmailTemplate(), data); err != nil {
		ctx.Logger.Error(err)
		ctx.ReplyOK()

		return
	}
}

func identityRetrieverFromStorage(ctx *middlewares.AutheliaCtx) (*session.Identity, error) {
	var requestBody resetPasswordStep1RequestBody

	err := json.Unmarshal(ctx.PostBody(), &requestBody)
	if err != nil {
		return nil, err
	}

	details, err := ctx.Providers.UserProvider.GetDetails(requestBody.Username)
	if err != nil {
		return nil, err
	}

	if len(details.Emails) == 0 {
		return nil, fmt.Errorf("user %s has no email address configured", requestBody.Username)
	}

	return &session.Identity{
		Username:    requestBody.Username,
		DisplayName: details.DisplayName,
		Email:       details.Emails[0],
	}, nil
}

// ResetPasswordIdentityStart is the handler for initiating the identity validation for resetting a password.
// We need to ensure the attacker cannot perform user enumeration by always replying with 200 whatever what happens in backend.
var ResetPasswordIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
	MailTitle:               "Reset your password",
	MailButtonContent:       "Reset",
	MailButtonRevokeContent: "Revoke",
	TargetEndpoint:          "/reset-password/step2",
	RevokeEndpoint:          "/revoke/reset-password",
	ActionClaim:             ActionResetPassword,
	IdentityRetrieverFunc:   identityRetrieverFromStorage,
}, middlewares.TimingAttackDelay(10, 250, 85, time.Millisecond*500, false))

func resetPasswordIdentityVerificationFinish(ctx *middlewares.AutheliaCtx, username string) {
	var (
		userSession session.UserSession
		err         error
	)

	ctx.ReplyOK()

	if userSession, err = ctx.GetSession(); err != nil {
		ctx.Logger.WithError(err).Errorf("Unable to get session to clear password reset flag in session for user '%s'", userSession.Username)

		return
	}

	userSession.PasswordResetUsername = &username

	if err = ctx.SaveSession(userSession); err != nil {
		ctx.Logger.WithError(err).Errorf("Unable to clear password reset flag in session for user '%s'", userSession.Username)
	}
}

// ResetPasswordIdentityFinish the handler for finishing the identity validation.
var ResetPasswordIdentityFinish = middlewares.IdentityVerificationFinish(
	middlewares.IdentityVerificationFinishArgs{ActionClaim: ActionResetPassword}, resetPasswordIdentityVerificationFinish)
