package accountserver

import (
	"errors"

	umdb "git.autistici.org/id/usermetadb"
)

// GetUserRequest retrieves a specific User.
type GetUserRequest struct {
	UserRequestBase

	// Whether to return an inactive user.
	IncludeInactive bool `json:"include_inactive"`
}

// Serve the request.
func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if !r.IncludeInactive && rctx.User.Status != UserStatusActive {
		return nil, ErrUserNotFound
	}

	// Return the public User object contained within the RawUser.
	return &rctx.User.User, nil
}

// SearchUserRequest searches the database for users with names
// matching a given pattern. The actual pattern semantics are
// backend-specific (for LDAP, this is a prefix string search).
type SearchUserRequest struct {
	AdminRequestBase

	Pattern string `json:"pattern"`
	Limit   int    `json:"limit"`
}

// Validate the request.
func (r *SearchUserRequest) Validate(rctx *RequestContext) error {
	if r.Pattern == "" {
		return newValidationError(nil, "pattern", "empty pattern")
	}
	return nil
}

// SearchUserResponse is the response type for SearchUserRequest.
type SearchUserResponse struct {
	Usernames []string `json:"usernames"`
}

// Serve the request.
func (r *SearchUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
	usernames, err := rctx.TX.SearchUser(rctx.Context, r.Pattern, r.Limit)
	if err != nil {
		return nil, err
	}
	return &SearchUserResponse{Usernames: usernames}, nil
}

// ChangeUserPasswordRequest updates a user's password. It will also take
// care of re-encrypting the user encryption key, if present.
type ChangeUserPasswordRequest struct {
	PrivilegedRequestBase
	Password string `json:"password"`
}

// Sanitize the request.
func (r *ChangeUserPasswordRequest) Sanitize() {
	r.PrivilegedRequestBase.Sanitize()
	if r.Password != "" {
		r.Password = sanitizedValue
	}
}

// Validate the request.
func (r *ChangeUserPasswordRequest) Validate(rctx *RequestContext) error {
	if err := rctx.fieldValidators.password(rctx, r.Password); err != nil {
		return newValidationError(nil, "password", err.Error())
	}
	if r.Password == r.CurPassword {
		return newValidationError(nil, "password", "The new password can't be the same as the old one")
	}
	return r.PrivilegedRequestBase.Validate(rctx)
}

// Serve the request.
func (r *ChangeUserPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	err := rctx.User.setPrimaryPassword(rctx.Context, rctx.TX, r.CurPassword, r.Password, rctx.enableOpportunisticEncryption)
	if err != nil {
		return nil, err
	}

	rctx.audit.Log(rctx, nil, "password changed (user)")
	rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordChange, "password changed (user)")
	return nil, err
}

// AccountRecoveryRequest lets users reset their password by providing
// secondary credentials, which we authenticate ourselves. It is not
// authenticated with SSO.
//
// Two-factor authentication is disabled on successful recovery.
type AccountRecoveryRequest struct {
	Username         string `json:"username"`
	RecoveryPassword string `json:"recovery_password"`
	Password         string `json:"password"`
	RemoteAddr       string `json:"remote_addr"`
	// TODO: add full DeviceInfo?
}

// AccountRecoveryResponse is the response type for AccountRecoveryRequest.
type AccountRecoveryResponse struct {
	Hint string `json:"hint,omitempty"`
}

// Sanitize the request.
func (r *AccountRecoveryRequest) Sanitize() {
	if r.RecoveryPassword != "" {
		r.RecoveryPassword = sanitizedValue
	}
	if r.Password != "" {
		r.Password = sanitizedValue
	}
}

// Validate the request.
func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error {
	// Only validate the password if attempting recovery.
	if r.RecoveryPassword != "" {
		if err := rctx.fieldValidators.password(rctx, r.Password); err != nil {
			return newValidationError(nil, "password", err.Error())
		}
	}
	return nil
}

// PopulateContext extracts information from the request and stores it
// into the RequestContext.
func (r *AccountRecoveryRequest) PopulateContext(rctx *RequestContext) error {
	user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username)
	if err != nil {
		return err
	}
	rctx.User = user
	return nil
}

// Authorize the request.
func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error {
	// The user must be in the 'active' state.
	if rctx.User.Status != UserStatusActive {
		return errors.New("user is not active")
	}

	// Anyone can request the hint (rate-limit above this layer).
	if r.RecoveryPassword == "" {
		return nil
	}

	// TODO: call out to auth-server for rate limiting and other features.
	// Authenticate the secret recovery password.
	if err := rctx.authorizeAccountRecovery(rctx.Context, rctx.User.Name, r.RecoveryPassword, r.RemoteAddr); err != nil {
		return err
	}

	return nil
}

// Serve the request.
func (r *AccountRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) {
	resp := AccountRecoveryResponse{
		Hint: rctx.User.AccountRecoveryHint,
	}

	// Only attempt to authenticate if the recovery password is
	// set in the request, otherwise just return the hint.
	if r.RecoveryPassword == "" {
		return &resp, nil
	}

	if err := rctx.User.setPrimaryPassword(rctx.Context, rctx.TX, r.RecoveryPassword, r.Password, rctx.enableOpportunisticEncryption); err != nil {
		return nil, err
	}
	if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil {
		return nil, err
	}
	rctx.audit.Log(rctx, nil, "password changed (account recovery)")
	rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password changed (account recovery)")

	return nil, nil
}

// ResetPasswordRequest is an admin operation to forcefully reset the
// password for an account. A new password will be randomly generated
// by the accountserver. The user will lose access to all stored email
// (because the encryption keys will be reset) and to 2FA.
type ResetPasswordRequest struct {
	AdminUserRequestBase
}

// ResetPasswordResponse is the response type for ResetPasswordRequest.
type ResetPasswordResponse struct {
	Password string `json:"password"`
}

// Sanitize the response.
func (r *ResetPasswordResponse) Sanitize() {
	if r.Password != "" {
		r.Password = sanitizedValue
	}
}

// Serve the request.
func (r *ResetPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	password := randomPassword()

	if err := rctx.User.resetPassword(rctx.Context, rctx.TX, password); err != nil {
		return nil, err
	}
	if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil {
		return nil, err
	}

	rctx.audit.Log(rctx, nil, "password reset (admin)")
	rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password reset (admin)")
	return &ResetPasswordResponse{
		Password: password,
	}, nil
}

// SetAccountRecoveryHintRequest lets users set the password recovery hint
// and expected response (secondary password).
type SetAccountRecoveryHintRequest struct {
	PrivilegedRequestBase
	Hint     string `json:"recovery_hint"`
	Response string `json:"recovery_response"`
}

// Sanitize the request.
func (r *SetAccountRecoveryHintRequest) Sanitize() {
	r.PrivilegedRequestBase.Sanitize()
	if r.Response != "" {
		r.Response = sanitizedValue
	}
}

// Validate the request.
func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error {
	var err *ValidationError
	if r.Hint == "" {
		err = newValidationError(err, "recovery_hint", "mandatory field")
	}
	if verr := rctx.fieldValidators.password(rctx, r.Response); verr != nil {
		err = newValidationError(err, "recovery_response", verr.Error())
	}
	return err.orNil()
}

// Serve the request.
func (r *SetAccountRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) {
	return nil, rctx.User.setAccountRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response)
}

// CreateApplicationSpecificPasswordRequest lets users create their own ASPs.
type CreateApplicationSpecificPasswordRequest struct {
	PrivilegedRequestBase
	Service string `json:"service"`
	Notes   string `json:"notes"`
}

// CreateApplicationSpecificPasswordResponse is the response type for
// CreateApplicationSpecificPasswordRequest.
type CreateApplicationSpecificPasswordResponse struct {
	Password string `json:"password"`
}

// Sanitize the response.
func (r *CreateApplicationSpecificPasswordResponse) Sanitize() {
	if r.Password != "" {
		r.Password = sanitizedValue
	}
}

// Validate the request.
func (r *CreateApplicationSpecificPasswordRequest) Validate(_ *RequestContext) error {
	var err *ValidationError
	if r.Service == "" {
		err = newValidationError(err, "service", "mandatory field")
	}
	return err.orNil()
}

// Serve the request.
func (r *CreateApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if !rctx.User.Has2FA {
		return nil, errors.New("2FA is not enabled for this user")
	}

	// Create a new application-specific password metadata.
	asp := &AppSpecificPasswordInfo{
		ID:      randomAppSpecificPasswordID(),
		Service: r.Service,
		Comment: r.Notes,
	}
	pw := randomAppSpecificPassword()

	if err := rctx.User.addApplicationSpecificPassword(rctx.Context, rctx.TX, r.CurPassword, pw, asp); err != nil {
		return nil, err
	}

	return &CreateApplicationSpecificPasswordResponse{
		Password: pw,
	}, nil
}

// DeleteApplicationSpecificPasswordRequest deletes an application-specific
// password, identified by its unique ID.
type DeleteApplicationSpecificPasswordRequest struct {
	UserRequestBase
	AspID string `json:"asp_id"`
}

// Serve the request.
func (r *DeleteApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
	return nil, rctx.User.deleteApplicationSpecificPassword(rctx.Context, rctx.TX, r.AspID)
}

// EnableOTPRequest enables OTP-based two-factor authentication for a
// user. The caller can generate the TOTP secret itself if needed (useful for
// UX that confirms that the user is able to login first), or it can let the
// server generate a new secret by passing an empty totp_secret.
type EnableOTPRequest struct {
	UserRequestBase
	TOTPSecret string `json:"totp_secret"`
}

// Sanitize the request.
func (r *EnableOTPRequest) Sanitize() {
	r.UserRequestBase.Sanitize()
	if r.TOTPSecret != "" {
		r.TOTPSecret = sanitizedValue
	}
}

// Validate the request.
func (r *EnableOTPRequest) Validate(_ *RequestContext) error {
	var err *ValidationError
	// Only check if the client-side secret is set, skip otherwise.
	if r.TOTPSecret == "" && len(r.TOTPSecret) != 16 {
		err = newValidationError(err, "totp_secret", "bad value")
	}
	return err.orNil()
}

// EnableOTPResponse is the response type for AccountService.EnableOTP().
type EnableOTPResponse struct {
	TOTPSecret string `json:"totp_secret"`
}

// Sanitize the response.
func (r *EnableOTPResponse) Sanitize() {
	if r.TOTPSecret != "" {
		r.TOTPSecret = sanitizedValue
	}
}

// Serve the request.
func (r *EnableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
	// Replace or initialize the TOTP secret.
	secret := r.TOTPSecret
	if secret == "" {
		var err error
		secret, err = generateTOTPSecret()
		if err != nil {
			return nil, err
		}
	}

	if err := rctx.User.setTOTPSecret(rctx.Context, rctx.TX, secret); err != nil {
		return nil, err
	}

	rctx.audit.Log(rctx, nil, "totp enabled")

	return &EnableOTPResponse{
		TOTPSecret: secret,
	}, nil
}

// DisableOTPRequest disables two-factor authentication for a user.
type DisableOTPRequest struct {
	UserRequestBase
}

// Serve the request.
func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if err := rctx.User.disableOTP(rctx.Context, rctx.TX); err != nil {
		return nil, err
	}
	rctx.audit.Log(rctx, nil, "totp disabled")
	return nil, nil
}

// UpdateUserRequest allows the caller to update a (very limited) selected set
// of fields on a User object. It is a catch-all function for very simple
// changes that don't justify their own specialized method.
type UpdateUserRequest struct {
	UserRequestBase
	Lang             string             `json:"lang,omitempty"`
	U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"`
}

const maxU2FRegistrations = 20

// Validate the request.
func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
	if len(r.U2FRegistrations) > maxU2FRegistrations {
		return newValidationError(nil, "u2f_registrations", "too many U2F registrations")
	}

	// TODO: better validation of the language code!
	if len(r.Lang) > 2 {
		return newValidationError(nil, "lang", "invalid language code")
	}

	return nil
}

// Serve the request.
func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if r.Lang != "" {
		rctx.User.Lang = r.Lang
	}

	// TODO: check if setU2FRegistration calls tx.UpdateUser, this is a bug otherwise.
	return nil, rctx.User.setU2FRegistrations(rctx.Context, rctx.TX, r.U2FRegistrations)
}

// AdminUpdateUserRequest is the privileged version of UpdateUser and
// allows to update many more attributes. It is a catch-all function
// for very simple changes that don't justify their own specialized
// method.
type AdminUpdateUserRequest struct {
	AdminUserRequestBase
	Lang   string `json:"lang,omitempty"`
	Status string `json:"status"`
}

// Validate the request.
func (r *AdminUpdateUserRequest) Validate(rctx *RequestContext) error {
	switch r.Status {
	case "", ResourceStatusActive, ResourceStatusInactive:
	default:
		return newValidationError(nil, "status", "invalid or unknown status")
	}
	return nil
}

// Serve the request.
func (r *AdminUpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
	if r.Status != "" {
		rctx.User.Status = r.Status
	}

	return nil, rctx.TX.UpdateUser(rctx.Context, &rctx.User.User)
}