package store

import (
	"fmt"

	"golang.org/x/crypto/bcrypt"
)

type AccountInterface interface {
	CreateAccount(*Account) error
	FindAccount(string) (Account, error)
	FindAccounts(...string) ([]Account, error)
	FindAccountWithPassword(string, string) (Account, error)
}

// Account is stored in the database as domains/<domain>/accounts/<id>/config
type Account struct {
	// The username is typically user@example.com - the user is required to
	// enter it *exactly* to log in.
	Username string `storm:"id"`

	// Whether the user is an admin or not. Typically, only postmaster@example.com
	// is an admin, but others may be set up too if desired.
	Admin bool

	// Wildcard users are last-resort destinations for incoming mail. A domain
	// should only have one of them.
	Wildcard bool `storm:"index"`

	// As generated by HashPassword
	PasswordHash string

	// Where to put messages by default. FK: mailbox.id
	DefaultMailbox uint64
}

// HashPassword turns a plaintext password into a crypt()ed string, using bcrypt
func HashPassword(password string) (string, error) {
	b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

	return string(b), err
}

func CheckPassword(hashed, plain string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)) == nil
}

func (c *concrete) CreateAccount(account *Account) error {
	return c.storm.Save(account)
}

func (c *concrete) FindAccount(username string) (Account, error) {
	var account Account

	return account, c.storm.One("Username", username, &account)
}

func (c *concrete) FindAccounts(usernames ...string) ([]Account, error) {
	accounts := make([]Account, len(usernames))

	// FIXME: I don't know how to storm, it turns out
	for i, username := range usernames {
		account, err := c.FindAccount(username)
		if err != nil {
			return nil, err
		}
		accounts[i] = account
	}

	return accounts, nil
}

func (c *concrete) FindAccountWithPassword(username, password string) (Account, error) {
	account, err := c.FindAccount(username)

	if err != nil {
		// Always do a bcrypt check to avoid timing attacks
		_ = CheckPassword("", "")
		return Account{}, err
	}

	if !CheckPassword(account.PasswordHash, password) {
		return Account{}, fmt.Errorf("bad password")
	}

	return account, nil
}
