package api

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

	"github.com/dustin/go-humanize/english"
	"github.com/gin-gonic/gin"

	"github.com/photoprism/photoprism/internal/auth/acl"
	"github.com/photoprism/photoprism/internal/entity"
	"github.com/photoprism/photoprism/internal/event"
	"github.com/photoprism/photoprism/internal/form"
	"github.com/photoprism/photoprism/internal/photoprism/get"
	"github.com/photoprism/photoprism/internal/server/limiter"
	"github.com/photoprism/photoprism/pkg/authn"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/http/header"
	"github.com/photoprism/photoprism/pkg/i18n"
	"github.com/photoprism/photoprism/pkg/log/status"
	"github.com/photoprism/photoprism/pkg/rnd"
)

// CreateUserPasscode sets up a new two-factor authentication passcode for a user.
//
//	@Summary	create a new 2FA passcode for a user
//	@Id			CreateUserPasscode
//	@Tags		Users
//	@Accept		json
//	@Produce	json
//	@Param		uid				path		string			true	"user uid"
//	@Param		request			body		form.Passcode	true	"passcode setup (password required)"
//	@Success	201				{object}	entity.Passcode
//	@Failure	400,401,403,429	{object}	i18n.Response
//	@Router		/api/v1/users/{uid}/passcode [post]
func CreateUserPasscode(router *gin.RouterGroup) {
	router.POST("/users/:uid/passcode", func(c *gin.Context) {
		// Check authentication and authorization.
		s, user, frm, authErr := checkUserPasscodeAuth(c, acl.ActionCreate)

		if authErr != nil {
			return
		}

		// Get client IP address for logs and rate limiting checks.
		clientIp := ClientIP(c)

		// Check request rate limit.
		r := limiter.Login.Request(clientIp)

		if r.Reject() {
			limiter.AbortJSON(c)
			return
		}

		// Check user password and abort if invalid.
		if code, msg, err := checkUserPasscodePassword(c, user, frm.Password); err != nil {
			event.AuditErr([]string{clientIp, "session %s", authn.Users, user.UserName, authn.ErrPasscodeGenerateFailed.Error(), strings.ToLower(clean.Error(err))}, s.RefID)
			Abort(c, code, msg)
			return
		}

		// Return the reserved request rate limit tokens after successful authentication.
		r.Success()

		// Get config.
		conf := get.Config()

		// Generate and save new passcode key.
		var passcode *entity.Passcode
		if key, err := rnd.AuthKey(conf.AppName(), user.UserName); err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeGenerateFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
			return
		} else if passcode, err = entity.NewPasscode(user.GetUID(), key.String(), rnd.RecoveryCode()); err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeCreateFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
			return
		} else if err = passcode.Save(); err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeSaveFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusConflict, i18n.ErrSaveFailed)
			return
		}

		event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Created}, s.RefID)

		header.SetLocation(c)
		c.JSON(http.StatusCreated, passcode)
	})
}

// ConfirmUserPasscode verifies a newly created passcode so that it can be activated.
//
//	@Summary	verify a new 2FA passcode
//	@Id			ConfirmUserPasscode
//	@Tags		Users
//	@Accept		json
//	@Produce	json
//	@Param		uid				path		string			true	"user uid"
//	@Param		request			body		form.Passcode	true	"verification code"
//	@Success	200				{object}	entity.Passcode
//	@Failure	400,401,403,429	{object}	i18n.Response
//	@Router		/api/v1/users/{uid}/passcode/confirm [post]
func ConfirmUserPasscode(router *gin.RouterGroup) {
	router.POST("/users/:uid/passcode/confirm", func(c *gin.Context) {
		// Check authentication and authorization.
		s, user, frm, authErr := checkUserPasscodeAuth(c, acl.ActionUpdate)

		if authErr != nil {
			return
		}

		// Check request rate limit.
		r := limiter.Login.RequestN(ClientIP(c), 3)

		if r.Reject() {
			limiter.AbortJSON(c)
			return
		}

		// Verify passcode.
		valid, passcode, err := user.VerifyPasscode(frm.Passcode())

		if err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeVerificationFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusForbidden, i18n.ErrInvalidPasscode)
			return
		} else if !valid {
			event.AuditWarn([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrInvalidPasscode.Error()}, s.RefID)
			Abort(c, http.StatusForbidden, i18n.ErrInvalidPasscode)
			return
		}

		// Return the reserved request rate limit tokens after successful authentication.
		r.Success()

		event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Verified}, s.RefID)

		// Clear session cache.
		s.ClearCache()

		c.JSON(http.StatusOK, passcode)
	})
}

// ActivateUserPasscode activates 2FA after a passcode has been created and verified.
//
//	@Summary	activate 2FA with a verified passcode
//	@Id			ActivateUserPasscode
//	@Tags		Users
//	@Produce	json
//	@Param		uid				path		string	true	"user uid"
//	@Success	200				{object}	entity.Passcode
//	@Failure	401,403,404,429	{object}	i18n.Response
//	@Router		/api/v1/users/{uid}/passcode/activate [post]
func ActivateUserPasscode(router *gin.RouterGroup) {
	router.POST("/users/:uid/passcode/activate", func(c *gin.Context) {
		// Check authentication and authorization.
		s, user, _, authErr := checkUserPasscodeAuth(c, acl.ActionUpdate)

		if authErr != nil {
			return
		}

		// Activate new passcode.
		passcode, err := user.ActivatePasscode()

		if err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeActivationFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusForbidden, i18n.ErrSaveFailed)
			return
		}

		// Log event.
		event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Activated}, s.RefID)

		// Invalidate any other user sessions to protect the account:
		// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
		event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, "invalidated %s"}, s.RefID,
			english.Plural(user.DeleteSessions([]string{s.ID}), authn.Session, authn.Sessions))

		// Clear session cache.
		s.ClearCache()

		c.JSON(http.StatusOK, passcode)
	})
}

// DeactivateUserPasscode removes a passcode key to disable two-factor authentication.
//
//	@Summary	deactivate 2FA and remove the passcode
//	@Id			DeactivateUserPasscode
//	@Tags		Users
//	@Accept		json
//	@Produce	json
//	@Param		uid					path		string			true	"user uid"
//	@Param		request				body		form.Passcode	true	"password for confirmation"
//	@Success	200					{object}	i18n.Response
//	@Failure	400,401,403,404,429	{object}	i18n.Response
//	@Router		/api/v1/users/{uid}/passcode/deactivate [post]
func DeactivateUserPasscode(router *gin.RouterGroup) {
	router.POST("/users/:uid/passcode/deactivate", func(c *gin.Context) {
		// Check authentication and authorization.
		s, user, frm, authErr := checkUserPasscodeAuth(c, acl.ActionDelete)

		if authErr != nil {
			return
		}

		// Get client IP address for logs and rate limiting checks.
		clientIp := ClientIP(c)

		// Check request rate limit.
		r := limiter.Login.Request(clientIp)

		if r.Reject() {
			limiter.AbortJSON(c)
			return
		}

		// Check user password and abort if invalid.
		if code, msg, err := checkUserPasscodePassword(c, user, frm.Password); err != nil {
			event.AuditErr([]string{clientIp, "session %s", authn.Users, user.UserName, authn.ErrPasscodeDeactivationFailed.Error(), strings.ToLower(clean.Error(err))}, s.RefID)
			Abort(c, code, msg)
			return
		}

		// Return the reserved request rate limit tokens after successful authentication.
		r.Success()

		// Delete passcode.
		if _, err := user.DeactivatePasscode(); err != nil {
			event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeDeactivationFailed.Error(), clean.Error(err)}, s.RefID)
			Abort(c, http.StatusNotFound, i18n.ErrNotFound)
			return
		}

		event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Deactivated}, s.RefID)

		// Clear session cache.
		s.ClearCache()

		c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSettingsSaved))
	})
}

// checkUserPasscodeAuth checks authentication and authorization for the passcode endpoints.
func checkUserPasscodeAuth(c *gin.Context, action acl.Permission) (*entity.Session, *entity.User, *form.Passcode, error) {
	conf := get.Config()

	// Prevent caching of API response.
	c.Header(header.CacheControl, header.CacheControlNoStore)

	// You cannot change any passwords without authentication and settings enabled.
	if conf.Public() || conf.DisableSettings() {
		Abort(c, http.StatusForbidden, i18n.ErrPublic)
		return nil, nil, nil, authn.ErrPasscodeNotSupported
	}

	// Check limit for failed auth requests (max. 10 per minute).
	if limiter.Login.Reject(ClientIP(c)) {
		limiter.AbortJSON(c)
		return nil, nil, nil, authn.ErrRateLimitExceeded
	}

	// Get session.
	s := Auth(c, acl.ResourcePasscode, action)

	if s.Abort(c) {
		return s, nil, nil, authn.ErrUnauthorized
	}

	// Check if the current user has management privileges.
	uid := clean.UID(c.Param("uid"))

	// Get user from session.
	user := s.GetUser()

	// Regular users can only set up a passcode for their own account.
	if user.UserUID != uid || !user.CanLogIn() {
		AbortForbidden(c)
		return s, nil, nil, authn.ErrUnauthorized
	}

	// Check if the user's authentication provider supports 2FA passcodes.
	if !user.Provider().SupportsPasscodeAuthentication() {
		Abort(c, http.StatusForbidden, i18n.ErrUnsupported)
		return s, nil, nil, authn.ErrPasscodeNotSupported
	}

	frm := &form.Passcode{}

	// Validate request form values.
	if err := c.BindJSON(frm); err != nil {
		Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
		return s, nil, nil, authn.ErrInvalidRequest
	} else if authn.KeyTOTP.NotEqual(frm.Type) {
		Abort(c, http.StatusBadRequest, i18n.ErrUnsupportedType)
		return s, nil, nil, authn.ErrInvalidPasscodeType
	}

	return s, user, frm, nil
}

// checkUserPasscodePassword checks if the specified password is valid.
func checkUserPasscodePassword(c *gin.Context, user *entity.User, password string) (code int, msg i18n.Message, err error) {
	// Set result defaults.
	code = http.StatusForbidden
	msg = i18n.ErrInvalidPassword

	if user == nil {
		return code, msg, authn.ErrUserRequired
	} else if c == nil {
		return code, msg, authn.ErrContextRequired
	}

	username := user.Username()

	if username == "" {
		return code, msg, authn.ErrUsernameRequired
	}

	switch user.Provider() {
	// Check local account password.
	case authn.ProviderLocal:
		if user.InvalidPassword(password) {
			return code, msg, authn.ErrInvalidPassword
		}
	// Use generic authentication check.
	default:
		// Create user login form.
		f := form.Login{
			Username: username,
			Password: password,
		}

		// Check if user login credentials are valid.
		authUser, provider, method, authErr := entity.Auth(f, nil, c)

		switch {
		case method == authn.Method2FA && errors.Is(authErr, authn.ErrPasscodeRequired):
			return http.StatusOK, i18n.MsgVerified, nil
		case authErr != nil:
			// Abort if authentication has failed otherwise.
			return code, msg, authErr
		case authUser == nil:
			// Abort if account was not found.
			return code, msg, authn.ErrAccountNotFound
		case !authUser.Equal(user):
			// Abort if user accounts do not match.
			return code, msg, authn.ErrUserDoesNotMatch
		case !provider.SupportsPasscodeAuthentication() || method != authn.MethodDefault:
			// Abort if e.g. an app password was provided.
			return code, msg, authn.ErrInvalidCredentials
		default:
			return http.StatusOK, i18n.MsgVerified, nil
		}
	}

	return http.StatusOK, i18n.MsgVerified, nil
}
