package kv

import (
	"context"
	"fmt"

	"golang.org/x/crypto/bcrypt"

	"github.com/influxdata/influxdb/v2"
)

// MinPasswordLength is the shortest password we allow into the system.
const MinPasswordLength = 8

var (
	// EIncorrectPassword is returned when any password operation fails in which
	// we do not want to leak information.
	EIncorrectPassword = &influxdb.Error{
		Code: influxdb.EForbidden,
		Msg:  "your username or password is incorrect",
	}

	// EIncorrectUser is returned when any user is failed to be found which indicates
	// the userID provided is for a user that does not exist.
	EIncorrectUser = &influxdb.Error{
		Code: influxdb.EForbidden,
		Msg:  "your userID is incorrect",
	}

	// EShortPassword is used when a password is less than the minimum
	// acceptable password length.
	EShortPassword = &influxdb.Error{
		Code: influxdb.EInvalid,
		Msg:  "passwords must be at least 8 characters long",
	}
)

// UnavailablePasswordServiceError is used if we aren't able to add the
// password to the store, it means the store is not available at the moment
// (e.g. network).
func UnavailablePasswordServiceError(err error) *influxdb.Error {
	return &influxdb.Error{
		Code: influxdb.EUnavailable,
		Msg:  fmt.Sprintf("Unable to connect to password service. Please try again; Err: %v", err),
		Op:   "kv/setPassword",
	}
}

// CorruptUserIDError is used when the ID was encoded incorrectly previously.
// This is some sort of internal server error.
func CorruptUserIDError(userID string, err error) *influxdb.Error {
	return &influxdb.Error{
		Code: influxdb.EInternal,
		Msg:  fmt.Sprintf("User ID %s has been corrupted; Err: %v", userID, err),
		Op:   "kv/setPassword",
	}
}

// InternalPasswordHashError is used if the hasher is unable to generate
// a hash of the password.  This is some sort of internal server error.
func InternalPasswordHashError(err error) *influxdb.Error {
	return &influxdb.Error{
		Code: influxdb.EInternal,
		Msg:  fmt.Sprintf("Unable to generate password; Err: %v", err),
		Op:   "kv/setPassword",
	}
}

var (
	userpasswordBucket = []byte("userspasswordv1")
)

var _ influxdb.PasswordsService = (*Service)(nil)

func (s *Service) initializePasswords(ctx context.Context, tx Tx) error {
	_, err := tx.Bucket(userpasswordBucket)
	return err
}

// CompareAndSetPassword checks the password and if they match
// updates to the new password.
func (s *Service) CompareAndSetPassword(ctx context.Context, userID influxdb.ID, old string, new string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		if err := s.comparePassword(ctx, tx, userID, old); err != nil {
			return err
		}
		return s.setPassword(ctx, tx, userID, new)
	})
}

// SetPassword overrides the password of a known user.
func (s *Service) SetPassword(ctx context.Context, userID influxdb.ID, password string) error {
	return s.kv.Update(ctx, func(tx Tx) error {
		return s.setPassword(ctx, tx, userID, password)
	})
}

// ComparePassword checks if the password matches the password recorded.
// Passwords that do not match return errors.
func (s *Service) ComparePassword(ctx context.Context, userID influxdb.ID, password string) error {
	return s.kv.View(ctx, func(tx Tx) error {
		return s.comparePassword(ctx, tx, userID, password)
	})
}

func (s *Service) setPassword(ctx context.Context, tx Tx, userID influxdb.ID, password string) error {
	if len(password) < MinPasswordLength {
		return EShortPassword
	}

	encodedID, err := userID.Encode()
	if err != nil {
		return CorruptUserIDError(userID.String(), err)
	}

	if _, err := s.findUserByID(ctx, tx, userID); err != nil {
		return EIncorrectUser
	}

	b, err := tx.Bucket(userpasswordBucket)
	if err != nil {
		return UnavailablePasswordServiceError(err)
	}

	hasher := s.Hash
	if hasher == nil {
		hasher = &Bcrypt{}
	}

	hash, err := hasher.GenerateFromPassword([]byte(password), DefaultCost)
	if err != nil {
		return InternalPasswordHashError(err)
	}

	if err := b.Put(encodedID, hash); err != nil {
		return UnavailablePasswordServiceError(err)
	}
	return nil
}

func (s *Service) comparePassword(ctx context.Context, tx Tx, userID influxdb.ID, password string) error {
	encodedID, err := userID.Encode()
	if err != nil {
		return CorruptUserIDError(userID.String(), err)
	}

	if _, err := s.findUserByID(ctx, tx, userID); err != nil {
		return EIncorrectUser
	}

	b, err := tx.Bucket(userpasswordBucket)
	if err != nil {
		return UnavailablePasswordServiceError(err)
	}

	hash, err := b.Get(encodedID)
	if err != nil {
		// User exists but has no password has been set.
		return EIncorrectPassword
	}

	hasher := s.Hash
	if hasher == nil {
		hasher = &Bcrypt{}
	}

	if err := hasher.CompareHashAndPassword(hash, []byte(password)); err != nil {
		// User exists but the password was incorrect
		return EIncorrectPassword
	}
	return nil
}

// DefaultCost is the cost that will actually be set if a cost below MinCost
// is passed into GenerateFromPassword
var DefaultCost = bcrypt.DefaultCost

// Crypt represents a cryptographic hashing function.
type Crypt interface {
	// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
	// Returns nil on success, or an error on failure.
	CompareHashAndPassword(hashedPassword, password []byte) error
	// GenerateFromPassword returns the hash of the password at the given cost.
	// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
	GenerateFromPassword(password []byte, cost int) ([]byte, error)
}

var _ Crypt = (*Bcrypt)(nil)

// Bcrypt implements Crypt using golang.org/x/crypto/bcrypt
type Bcrypt struct{}

// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
// Returns nil on success, or an error on failure.
func (b *Bcrypt) CompareHashAndPassword(hashedPassword, password []byte) error {
	return bcrypt.CompareHashAndPassword(hashedPassword, password)
}

// GenerateFromPassword returns the hash of the password at the given cost.
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
func (b *Bcrypt) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
	if cost < bcrypt.MinCost {
		cost = DefaultCost
	}
	return bcrypt.GenerateFromPassword(password, cost)
}
