// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license

package irc

import (
	"encoding/json"
	"fmt"
	"net/smtp"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unicode"

	"github.com/oragono/oragono/irc/caps"
	"github.com/oragono/oragono/irc/passwd"
	"github.com/oragono/oragono/irc/utils"
	"github.com/tidwall/buntdb"
)

const (
	keyAccountExists           = "account.exists %s"
	keyAccountVerified         = "account.verified %s"
	keyAccountCallback         = "account.callback %s"
	keyAccountVerificationCode = "account.verificationcode %s"
	keyAccountName             = "account.name %s" // stores the 'preferred name' of the account, not casemapped
	keyAccountRegTime          = "account.registered.time %s"
	keyAccountCredentials      = "account.credentials %s"
	keyAccountAdditionalNicks  = "account.additionalnicks %s"
	keyAccountEnforcement      = "account.customenforcement %s"
	keyAccountVHost            = "account.vhost %s"
	keyCertToAccount           = "account.creds.certfp %s"
	keyAccountChannels         = "account.channels %s"

	keyVHostQueueAcctToId = "vhostQueue %s"
	vhostRequestIdx       = "vhostQueue"
)

// everything about accounts is persistent; therefore, the database is the authoritative
// source of truth for all account information. anything on the heap is just a cache
type AccountManager struct {
	// XXX these are up here so they can be aligned to a 64-bit boundary, please forgive me
	// autoincrementing ID for vhost requests:
	vhostRequestID           uint64
	vhostRequestPendingCount uint64

	sync.RWMutex                      // tier 2
	serialCacheUpdateMutex sync.Mutex // tier 3
	vHostUpdateMutex       sync.Mutex // tier 3

	server *Server
	// track clients logged in to accounts
	accountToClients  map[string][]*Client
	nickToAccount     map[string]string
	skeletonToAccount map[string]string
	accountToMethod   map[string]NickReservationMethod
}

func (am *AccountManager) Initialize(server *Server) {
	am.accountToClients = make(map[string][]*Client)
	am.nickToAccount = make(map[string]string)
	am.skeletonToAccount = make(map[string]string)
	am.accountToMethod = make(map[string]NickReservationMethod)
	am.server = server

	am.buildNickToAccountIndex()
	am.initVHostRequestQueue()
}

func (am *AccountManager) buildNickToAccountIndex() {
	if !am.server.AccountConfig().NickReservation.Enabled {
		return
	}

	nickToAccount := make(map[string]string)
	skeletonToAccount := make(map[string]string)
	accountToMethod := make(map[string]NickReservationMethod)
	existsPrefix := fmt.Sprintf(keyAccountExists, "")

	am.serialCacheUpdateMutex.Lock()
	defer am.serialCacheUpdateMutex.Unlock()

	err := am.server.store.View(func(tx *buntdb.Tx) error {
		err := tx.AscendGreaterOrEqual("", existsPrefix, func(key, value string) bool {
			if !strings.HasPrefix(key, existsPrefix) {
				return false
			}

			account := strings.TrimPrefix(key, existsPrefix)
			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
				nickToAccount[account] = account
				accountName, err := tx.Get(fmt.Sprintf(keyAccountName, account))
				if err != nil {
					am.server.logger.Error("internal", "missing account name for", account)
				} else {
					skeleton, _ := Skeleton(accountName)
					skeletonToAccount[skeleton] = account
				}
			}
			if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
				additionalNicks := unmarshalReservedNicks(rawNicks)
				for _, nick := range additionalNicks {
					cfnick, _ := CasefoldName(nick)
					nickToAccount[cfnick] = account
					skeleton, _ := Skeleton(nick)
					skeletonToAccount[skeleton] = account
				}
			}

			if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
				method, err := nickReservationFromString(methodStr)
				if err == nil {
					accountToMethod[account] = method
				}
			}
			return true
		})
		return err
	})

	if err != nil {
		am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
	} else {
		am.Lock()
		am.nickToAccount = nickToAccount
		am.skeletonToAccount = skeletonToAccount
		am.accountToMethod = accountToMethod
		am.Unlock()
	}
}

func (am *AccountManager) initVHostRequestQueue() {
	if !am.server.AccountConfig().VHosts.Enabled {
		return
	}

	am.vHostUpdateMutex.Lock()
	defer am.vHostUpdateMutex.Unlock()

	// the db maps the account name to the autoincrementing integer ID of its request
	// create an numerically ordered index on ID, so we can list the oldest requests
	// finally, collect the integer id of the newest request and the total request count
	var total uint64
	var lastIDStr string
	err := am.server.store.Update(func(tx *buntdb.Tx) error {
		err := tx.CreateIndex(vhostRequestIdx, fmt.Sprintf(keyVHostQueueAcctToId, "*"), buntdb.IndexInt)
		if err != nil {
			return err
		}
		return tx.Descend(vhostRequestIdx, func(key, value string) bool {
			if lastIDStr == "" {
				lastIDStr = value
			}
			total++
			return true
		})
	})

	if err != nil {
		am.server.logger.Error("internal", "could not create vhost queue index", err.Error())
	}

	lastID, _ := strconv.ParseUint(lastIDStr, 10, 64)
	am.server.logger.Debug("services", fmt.Sprintf("vhost queue length is %d, autoincrementing id is %d", total, lastID))

	atomic.StoreUint64(&am.vhostRequestID, lastID)
	atomic.StoreUint64(&am.vhostRequestPendingCount, total)
}

func (am *AccountManager) NickToAccount(nick string) string {
	cfnick, err := CasefoldName(nick)
	if err != nil {
		return ""
	}

	am.RLock()
	defer am.RUnlock()
	return am.nickToAccount[cfnick]
}

// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership.
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
	config := am.server.Config()
	if !config.Accounts.NickReservation.Enabled {
		return "", NickReservationNone
	}

	am.RLock()
	defer am.RUnlock()

	// given an account, combine stored enforcement method with the config settings
	// to compute the actual enforcement method
	finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
		result = am.accountToMethod[account_]
		// if they don't have a custom setting, or customization is disabled, use the default
		if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
			result = config.Accounts.NickReservation.Method
		}
		if result == NickReservationOptional {
			// enforcement was explicitly enabled neither in the config or by the user
			result = NickReservationNone
		}
		return
	}

	nickAccount := am.nickToAccount[cfnick]
	skelAccount := am.skeletonToAccount[skeleton]
	if nickAccount == "" && skelAccount == "" {
		return "", NickReservationNone
	} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
		return nickAccount, finalEnforcementMethod(nickAccount)
	} else if skelAccount != "" && nickAccount == "" {
		return skelAccount, finalEnforcementMethod(skelAccount)
	} else {
		// nickAccount != skelAccount and both are nonempty:
		// two people have competing claims on (this casefolding of) this nick!
		nickMethod := finalEnforcementMethod(nickAccount)
		skelMethod := finalEnforcementMethod(skelAccount)
		switch {
		case skelMethod == NickReservationNone:
			return nickAccount, nickMethod
		case nickMethod == NickReservationNone:
			return skelAccount, skelMethod
		default:
			// nobody can use this nick
			return "!", NickReservationStrict
		}
	}
}

func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
	// TODO stub
	config := am.server.Config()
	if !config.Accounts.Bouncer.Enabled {
		return false
	}
	if config.Accounts.Bouncer.AllowedByDefault {
		return true
	}
	return session != nil && session.capabilities.Has(caps.Bouncer)
}

// Looks up the enforcement method stored in the database for an account
// (typically you want EnforcementStatus instead, which respects the config)
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
	am.RLock()
	defer am.RUnlock()
	return nickReservationToString(am.accountToMethod[account])
}

// Sets a custom enforcement method for an account and stores it in the database.
func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
	config := am.server.Config()
	if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
		return errFeatureDisabled
	}

	var serialized string
	if method == NickReservationOptional {
		serialized = "" // normally this is "default", but we're going to delete the key
	} else {
		serialized = nickReservationToString(method)
	}

	key := fmt.Sprintf(keyAccountEnforcement, account)

	am.Lock()
	defer am.Unlock()

	currentMethod := am.accountToMethod[account]
	if method != currentMethod {
		if method == NickReservationOptional {
			delete(am.accountToMethod, account)
		} else {
			am.accountToMethod[account] = method
		}

		return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
			if serialized != "" {
				_, _, err = tx.Set(key, nickReservationToString(method), nil)
			} else {
				_, err = tx.Delete(key)
			}
			return
		})
	}

	return nil
}

func (am *AccountManager) AccountToClients(account string) (result []*Client) {
	cfaccount, err := CasefoldName(account)
	if err != nil {
		return
	}

	am.RLock()
	defer am.RUnlock()
	return am.accountToClients[cfaccount]
}

func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
	casefoldedAccount, err := CasefoldName(account)
	skeleton, skerr := Skeleton(account)
	if err != nil || skerr != nil || account == "" || account == "*" {
		return errAccountCreation
	}

	if restrictedNicknames[casefoldedAccount] || restrictedNicknames[skeleton] {
		return errAccountAlreadyRegistered
	}

	config := am.server.AccountConfig()

	// final "is registration allowed" check, probably redundant:
	if !(config.Registration.Enabled || callbackNamespace == "admin") {
		return errFeatureDisabled
	}

	// if nick reservation is enabled, you can only register your current nickname
	// as an account; this prevents "land-grab" situations where someone else
	// registers your nick out from under you and then NS GHOSTs you
	// n.b. client is nil during a SAREGISTER:
	if config.NickReservation.Enabled && client != nil && client.NickCasefolded() != casefoldedAccount {
		return errAccountMustHoldNick
	}

	// can't register a guest nickname
	renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
	if renamePrefix != "" && strings.HasPrefix(casefoldedAccount, renamePrefix) {
		return errAccountAlreadyRegistered
	}

	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)

	credStr, err := am.serializeCredentials(passphrase, certfp)
	if err != nil {
		return err
	}

	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)

	var setOptions *buntdb.SetOptions
	ttl := config.Registration.VerifyTimeout
	if ttl != 0 {
		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
	}

	err = func() error {
		am.serialCacheUpdateMutex.Lock()
		defer am.serialCacheUpdateMutex.Unlock()

		// can't register an account with the same name as a registered nick
		if am.NickToAccount(casefoldedAccount) != "" {
			return errAccountAlreadyRegistered
		}

		return am.server.store.Update(func(tx *buntdb.Tx) error {
			_, err := am.loadRawAccount(tx, casefoldedAccount)
			if err != errAccountDoesNotExist {
				return errAccountAlreadyRegistered
			}

			if certfp != "" {
				// make sure certfp doesn't already exist because that'd be silly
				_, err := tx.Get(certFPKey)
				if err != buntdb.ErrNotFound {
					return errCertfpAlreadyExists
				}
			}

			tx.Set(accountKey, "1", setOptions)
			tx.Set(accountNameKey, account, setOptions)
			tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
			tx.Set(credentialsKey, credStr, setOptions)
			tx.Set(callbackKey, callbackSpec, setOptions)
			if certfp != "" {
				tx.Set(certFPKey, casefoldedAccount, setOptions)
			}
			return nil
		})
	}()

	if err != nil {
		return err
	}

	code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
	if err != nil {
		am.Unregister(casefoldedAccount)
		return errCallbackFailed
	} else {
		return am.server.store.Update(func(tx *buntdb.Tx) error {
			_, _, err = tx.Set(verificationCodeKey, code, setOptions)
			return err
		})
	}
}

// validatePassphrase checks whether a passphrase is allowed by our rules
func validatePassphrase(passphrase string) error {
	// sanity check the length
	if len(passphrase) == 0 || len(passphrase) > 600 {
		return errAccountBadPassphrase
	}
	// for now, just enforce that spaces are not allowed
	for _, r := range passphrase {
		if unicode.IsSpace(r) {
			return errAccountBadPassphrase
		}
	}
	return nil
}

// helper to assemble the serialized JSON for an account's credentials
func (am *AccountManager) serializeCredentials(passphrase string, certfp string) (result string, err error) {
	var creds AccountCredentials
	creds.Version = 1
	// we need at least one of passphrase and certfp:
	if passphrase == "" && certfp == "" {
		return "", errAccountBadPassphrase
	}
	// but if we have one, it's fine if the other is missing, it just means no
	// credential of that type will be accepted.
	creds.Certificate = certfp
	if passphrase != "" {
		if validatePassphrase(passphrase) != nil {
			return "", errAccountBadPassphrase
		}
		bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
		creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
		if err != nil {
			am.server.logger.Error("internal", "could not hash password", err.Error())
			return "", errAccountCreation
		}
	}

	credText, err := json.Marshal(creds)
	if err != nil {
		am.server.logger.Error("internal", "could not marshal credentials", err.Error())
		return "", errAccountCreation
	}
	return string(credText), nil
}

// changes the password for an account
func (am *AccountManager) setPassword(account string, password string) (err error) {
	casefoldedAccount, err := CasefoldName(account)
	if err != nil {
		return err
	}
	act, err := am.LoadAccount(casefoldedAccount)
	if err != nil {
		return err
	}

	credStr, err := am.serializeCredentials(password, act.Credentials.Certificate)
	if err != nil {
		return err
	}

	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
	return am.server.store.Update(func(tx *buntdb.Tx) error {
		_, _, err := tx.Set(credentialsKey, credStr, nil)
		return err
	})
}

func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
	if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
		return "", nil
	} else if callbackNamespace == "mailto" {
		return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
	} else {
		return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
	}
}

func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
	config := am.server.AccountConfig().Registration.Callbacks.Mailto
	code = utils.GenerateSecretToken()

	subject := config.VerifyMessageSubject
	if subject == "" {
		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
	}
	messageStrings := []string{
		fmt.Sprintf("From: %s\r\n", config.Sender),
		fmt.Sprintf("To: %s\r\n", callbackValue),
		fmt.Sprintf("Subject: %s\r\n", subject),
		"\r\n", // end headers, begin message body
		fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
		fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
		"\r\n",
		client.t("To verify your account, issue one of these commands:") + "\r\n",
		fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
	}

	var message []byte
	for i := 0; i < len(messageStrings); i++ {
		message = append(message, []byte(messageStrings[i])...)
	}
	addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
	var auth smtp.Auth
	if config.Username != "" && config.Password != "" {
		auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
	}

	// TODO: this will never send the password in plaintext over a nonlocal link,
	// but it might send the email in plaintext, regardless of the value of
	// config.TLS.InsecureSkipVerify
	err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
	if err != nil {
		am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
	}
	return
}

func (am *AccountManager) Verify(client *Client, account string, code string) error {
	casefoldedAccount, err := CasefoldName(account)
	if err != nil || account == "" || account == "*" {
		return errAccountVerificationFailed
	}

	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)

	var raw rawClientAccount

	func() {
		am.serialCacheUpdateMutex.Lock()
		defer am.serialCacheUpdateMutex.Unlock()

		err = am.server.store.Update(func(tx *buntdb.Tx) error {
			raw, err = am.loadRawAccount(tx, casefoldedAccount)
			if err == errAccountDoesNotExist {
				return errAccountDoesNotExist
			} else if err != nil {
				return errAccountVerificationFailed
			} else if raw.Verified {
				return errAccountAlreadyVerified
			}

			// actually verify the code
			// a stored code of "" means a none callback / no code required
			success := false
			storedCode, err := tx.Get(verificationCodeKey)
			if err == nil {
				// this is probably unnecessary
				if storedCode == "" || utils.SecretTokensMatch(storedCode, code) {
					success = true
				}
			}
			if !success {
				return errAccountVerificationInvalidCode
			}

			// verify the account
			tx.Set(verifiedKey, "1", nil)
			// don't need the code anymore
			tx.Delete(verificationCodeKey)
			// re-set all other keys, removing the TTL
			tx.Set(accountKey, "1", nil)
			tx.Set(accountNameKey, raw.Name, nil)
			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
			tx.Set(callbackKey, raw.Callback, nil)
			tx.Set(credentialsKey, raw.Credentials, nil)

			var creds AccountCredentials
			// XXX we shouldn't do (de)serialization inside the txn,
			// but this is like 2 usec on my system
			json.Unmarshal([]byte(raw.Credentials), &creds)
			if creds.Certificate != "" {
				certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
				tx.Set(certFPKey, casefoldedAccount, nil)
			}

			return nil
		})

		if err == nil {
			skeleton, _ := Skeleton(raw.Name)
			am.Lock()
			am.nickToAccount[casefoldedAccount] = casefoldedAccount
			am.skeletonToAccount[skeleton] = casefoldedAccount
			am.Unlock()
		}
	}()

	if err != nil {
		return err
	}

	nick := "[server admin]"
	if client != nil {
		nick = client.Nick()
	}
	am.server.logger.Info("accounts", "client", nick, "registered account", casefoldedAccount)
	raw.Verified = true
	clientAccount, err := am.deserializeRawAccount(raw)
	if err != nil {
		return err
	}
	if client != nil {
		am.Login(client, clientAccount)
	}
	return nil
}

func marshalReservedNicks(nicks []string) string {
	return strings.Join(nicks, ",")
}

func unmarshalReservedNicks(nicks string) (result []string) {
	if nicks == "" {
		return
	}
	return strings.Split(nicks, ",")
}

func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
	cfnick, err := CasefoldName(nick)
	skeleton, skerr := Skeleton(nick)
	// garbage nick, or garbage options, or disabled
	nrconfig := am.server.AccountConfig().NickReservation
	if err != nil || skerr != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
		return errAccountNickReservationFailed
	}

	// the cache is in sync with the DB while we hold serialCacheUpdateMutex
	am.serialCacheUpdateMutex.Lock()
	defer am.serialCacheUpdateMutex.Unlock()

	// find the affected account, which is usually the client's:
	account := client.Account()
	if saUnreserve {
		// unless this is a sadrop:
		account = am.NickToAccount(cfnick)
		if account == "" {
			// nothing to do
			return nil
		}
	}
	if account == "" {
		return errAccountNotLoggedIn
	}

	am.Lock()
	accountForNick := am.nickToAccount[cfnick]
	var accountForSkeleton string
	if reserve {
		accountForSkeleton = am.skeletonToAccount[skeleton]
	}
	am.Unlock()

	if reserve && (accountForNick != "" || accountForSkeleton != "") {
		return errNicknameReserved
	} else if !reserve && !saUnreserve && accountForNick != account {
		return errNicknameReserved
	} else if !reserve && cfnick == account {
		return errAccountCantDropPrimaryNick
	}

	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account)
	unverifiedAccountKey := fmt.Sprintf(keyAccountExists, cfnick)
	err = am.server.store.Update(func(tx *buntdb.Tx) error {
		if reserve {
			// unverified accounts don't show up in NickToAccount yet (which is intentional),
			// however you shouldn't be able to reserve a nick out from under them
			_, err := tx.Get(unverifiedAccountKey)
			if err == nil {
				return errNicknameReserved
			}
		}

		rawNicks, err := tx.Get(nicksKey)
		if err != nil && err != buntdb.ErrNotFound {
			return err
		}

		nicks := unmarshalReservedNicks(rawNicks)

		if reserve {
			if len(nicks) >= nrconfig.AdditionalNickLimit {
				return errAccountTooManyNicks
			}
			nicks = append(nicks, nick)
		} else {
			// compute (original reserved nicks) minus cfnick
			var newNicks []string
			for _, reservedNick := range nicks {
				cfreservednick, _ := CasefoldName(reservedNick)
				if cfreservednick != cfnick {
					newNicks = append(newNicks, reservedNick)
				} else {
					// found the original, unfolded version of the nick we're dropping;
					// recompute the true skeleton from it
					skeleton, _ = Skeleton(reservedNick)
				}
			}
			nicks = newNicks
		}

		marshaledNicks := marshalReservedNicks(nicks)
		_, _, err = tx.Set(nicksKey, string(marshaledNicks), nil)
		return err
	})

	if err == errAccountTooManyNicks || err == errNicknameReserved {
		return err
	} else if err != nil {
		return errAccountNickReservationFailed
	}

	// success
	am.Lock()
	defer am.Unlock()
	if reserve {
		am.nickToAccount[cfnick] = account
		am.skeletonToAccount[skeleton] = account
	} else {
		delete(am.nickToAccount, cfnick)
		delete(am.skeletonToAccount, skeleton)
	}
	return nil
}

func (am *AccountManager) checkPassphrase(accountName, passphrase string) (account ClientAccount, err error) {
	account, err = am.LoadAccount(accountName)
	if err != nil {
		return
	}

	if !account.Verified {
		err = errAccountUnverified
		return
	}

	switch account.Credentials.Version {
	case 0:
		err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
	case 1:
		if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
			err = errAccountInvalidCredentials
		}
	default:
		err = errAccountInvalidCredentials
	}
	return
}

func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
	account, err := am.checkPassphrase(accountName, passphrase)
	if err != nil {
		return err
	}

	am.Login(client, account)
	return nil
}

func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
	casefoldedAccount, err := CasefoldName(accountName)
	if err != nil {
		err = errAccountDoesNotExist
		return
	}

	var raw rawClientAccount
	am.server.store.View(func(tx *buntdb.Tx) error {
		raw, err = am.loadRawAccount(tx, casefoldedAccount)
		return nil
	})
	if err != nil {
		return
	}

	result, err = am.deserializeRawAccount(raw)
	return
}

func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
	result.Name = raw.Name
	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
	result.RegisteredAt = time.Unix(regTimeInt, 0)
	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
	if e != nil {
		am.server.logger.Error("internal", "could not unmarshal credentials", e.Error())
		err = errAccountDoesNotExist
		return
	}
	result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
	result.Verified = raw.Verified
	if raw.VHost != "" {
		e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
		if e != nil {
			am.server.logger.Warning("internal", "could not unmarshal vhost for account", result.Name, e.Error())
			// pretend they have no vhost and move on
		}
	}
	return
}

func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string) (result rawClientAccount, err error) {
	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)

	_, e := tx.Get(accountKey)
	if e == buntdb.ErrNotFound {
		err = errAccountDoesNotExist
		return
	}

	result.Name, _ = tx.Get(accountNameKey)
	result.RegisteredAt, _ = tx.Get(registeredTimeKey)
	result.Credentials, _ = tx.Get(credentialsKey)
	result.Callback, _ = tx.Get(callbackKey)
	result.AdditionalNicks, _ = tx.Get(nicksKey)
	result.VHost, _ = tx.Get(vhostKey)

	if _, e = tx.Get(verifiedKey); e == nil {
		result.Verified = true
	}

	return
}

func (am *AccountManager) Unregister(account string) error {
	config := am.server.Config()
	casefoldedAccount, err := CasefoldName(account)
	if err != nil {
		return errAccountDoesNotExist
	}

	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
	enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount)
	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)

	var clients []*Client

	var registeredChannels []string
	// on our way out, unregister all the account's channels and delete them from the db
	defer func() {
		for _, channelName := range registeredChannels {
			am.server.channels.SetUnregistered(channelName, casefoldedAccount)
		}
	}()

	var credText string
	var rawNicks string

	am.serialCacheUpdateMutex.Lock()
	defer am.serialCacheUpdateMutex.Unlock()

	var accountName string
	var channelsStr string
	am.server.store.Update(func(tx *buntdb.Tx) error {
		tx.Delete(accountKey)
		accountName, _ = tx.Get(accountNameKey)
		tx.Delete(accountNameKey)
		tx.Delete(verifiedKey)
		tx.Delete(registeredTimeKey)
		tx.Delete(callbackKey)
		tx.Delete(verificationCodeKey)
		tx.Delete(enforcementKey)
		rawNicks, _ = tx.Get(nicksKey)
		tx.Delete(nicksKey)
		credText, err = tx.Get(credentialsKey)
		tx.Delete(credentialsKey)
		tx.Delete(vhostKey)
		channelsStr, _ = tx.Get(channelsKey)
		tx.Delete(channelsKey)

		_, err := tx.Delete(vhostQueueKey)
		am.decrementVHostQueueCount(casefoldedAccount, err)
		return nil
	})

	if err == nil {
		var creds AccountCredentials
		if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
			certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
			am.server.store.Update(func(tx *buntdb.Tx) error {
				if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
					tx.Delete(certFPKey)
				}
				return nil
			})
		}
	}

	skeleton, _ := Skeleton(accountName)
	additionalNicks := unmarshalReservedNicks(rawNicks)
	registeredChannels = unmarshalRegisteredChannels(channelsStr)

	am.Lock()
	defer am.Unlock()

	clients = am.accountToClients[casefoldedAccount]
	delete(am.accountToClients, casefoldedAccount)
	delete(am.nickToAccount, casefoldedAccount)
	delete(am.skeletonToAccount, skeleton)
	for _, nick := range additionalNicks {
		delete(am.nickToAccount, nick)
		additionalSkel, _ := Skeleton(nick)
		delete(am.skeletonToAccount, additionalSkel)
	}
	for _, client := range clients {
		if config.Accounts.RequireSasl.Enabled {
			client.Quit(client.t("You are no longer authorized to be on this server"), nil)
			// destroy acquires a semaphore so we can't call it while holding a lock
			go client.destroy(false, nil)
		} else {
			am.logoutOfAccount(client)
		}
	}

	if err != nil {
		return errAccountDoesNotExist
	}

	return nil
}

func unmarshalRegisteredChannels(channelsStr string) (result []string) {
	if channelsStr != "" {
		result = strings.Split(channelsStr, ",")
	}
	return
}

func (am *AccountManager) ChannelsForAccount(account string) (channels []string) {
	cfaccount, err := CasefoldName(account)
	if err != nil {
		return
	}

	var channelStr string
	key := fmt.Sprintf(keyAccountChannels, cfaccount)
	am.server.store.View(func(tx *buntdb.Tx) error {
		channelStr, _ = tx.Get(key)
		return nil
	})
	return unmarshalRegisteredChannels(channelStr)
}

func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
	if client.certfp == "" {
		return errAccountInvalidCredentials
	}

	var account string
	var rawAccount rawClientAccount
	certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)

	err := am.server.store.Update(func(tx *buntdb.Tx) error {
		var err error
		account, _ = tx.Get(certFPKey)
		if account == "" {
			return errAccountInvalidCredentials
		}
		rawAccount, err = am.loadRawAccount(tx, account)
		if err != nil || !rawAccount.Verified {
			return errAccountUnverified
		}
		return nil
	})

	if err != nil {
		return err
	}

	// ok, we found an account corresponding to their certificate
	clientAccount, err := am.deserializeRawAccount(rawAccount)
	if err != nil {
		return err
	}
	am.Login(client, clientAccount)
	return nil
}

// represents someone's status in hostserv
type VHostInfo struct {
	ApprovedVHost   string
	Enabled         bool
	RequestedVHost  string
	RejectedVHost   string
	RejectionReason string
	LastRequestTime time.Time
}

// pair type, <VHostInfo, accountName>
type PendingVHostRequest struct {
	VHostInfo
	Account string
}

// callback type implementing the actual business logic of vhost operations
type vhostMunger func(input VHostInfo) (output VHostInfo, err error)

func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) {
	munger := func(input VHostInfo) (output VHostInfo, err error) {
		output = input
		output.Enabled = true
		output.ApprovedVHost = vhost
		return
	}

	return am.performVHostChange(account, munger)
}

func (am *AccountManager) VHostRequest(account string, vhost string) (result VHostInfo, err error) {
	munger := func(input VHostInfo) (output VHostInfo, err error) {
		output = input
		output.RequestedVHost = vhost
		output.RejectedVHost = ""
		output.RejectionReason = ""
		output.LastRequestTime = time.Now().UTC()
		return
	}

	return am.performVHostChange(account, munger)
}

func (am *AccountManager) VHostApprove(account string) (result VHostInfo, err error) {
	munger := func(input VHostInfo) (output VHostInfo, err error) {
		output = input
		output.Enabled = true
		output.ApprovedVHost = input.RequestedVHost
		output.RequestedVHost = ""
		output.RejectionReason = ""
		return
	}

	return am.performVHostChange(account, munger)
}

func (am *AccountManager) VHostReject(account string, reason string) (result VHostInfo, err error) {
	munger := func(input VHostInfo) (output VHostInfo, err error) {
		output = input
		output.RejectedVHost = output.RequestedVHost
		output.RequestedVHost = ""
		output.RejectionReason = reason
		return
	}

	return am.performVHostChange(account, munger)
}

func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
	munger := func(input VHostInfo) (output VHostInfo, err error) {
		output = input
		output.Enabled = enabled
		return
	}

	return am.performVHostChange(client.Account(), munger)
}

func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
	account, err = CasefoldName(account)
	if err != nil || account == "" {
		err = errAccountDoesNotExist
		return
	}

	am.vHostUpdateMutex.Lock()
	defer am.vHostUpdateMutex.Unlock()

	clientAccount, err := am.LoadAccount(account)
	if err != nil {
		err = errAccountDoesNotExist
		return
	} else if !clientAccount.Verified {
		err = errAccountUnverified
		return
	}

	result, err = munger(clientAccount.VHost)
	if err != nil {
		return
	}

	vhtext, err := json.Marshal(result)
	if err != nil {
		err = errAccountUpdateFailed
		return
	}
	vhstr := string(vhtext)

	key := fmt.Sprintf(keyAccountVHost, account)
	queueKey := fmt.Sprintf(keyVHostQueueAcctToId, account)
	err = am.server.store.Update(func(tx *buntdb.Tx) error {
		if _, _, err := tx.Set(key, vhstr, nil); err != nil {
			return err
		}

		// update request queue
		if clientAccount.VHost.RequestedVHost == "" && result.RequestedVHost != "" {
			id := atomic.AddUint64(&am.vhostRequestID, 1)
			if _, _, err = tx.Set(queueKey, strconv.FormatUint(id, 10), nil); err != nil {
				return err
			}
			atomic.AddUint64(&am.vhostRequestPendingCount, 1)
		} else if clientAccount.VHost.RequestedVHost != "" && result.RequestedVHost == "" {
			_, err = tx.Delete(queueKey)
			am.decrementVHostQueueCount(account, err)
		}

		return nil
	})

	if err != nil {
		err = errAccountUpdateFailed
		return
	}

	am.applyVhostToClients(account, result)
	return result, nil
}

// XXX annoying helper method for keeping the queue count in sync with the DB
// `err` is the buntdb error returned from deleting the queue key
func (am *AccountManager) decrementVHostQueueCount(account string, err error) {
	if err == nil {
		// successfully deleted a queue entry, do a 2's complement decrement:
		atomic.AddUint64(&am.vhostRequestPendingCount, ^uint64(0))
	} else if err != buntdb.ErrNotFound {
		am.server.logger.Error("internal", "buntdb dequeue error", account, err.Error())
	}
}

func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostRequest, total int) {
	am.vHostUpdateMutex.Lock()
	defer am.vHostUpdateMutex.Unlock()

	total = int(atomic.LoadUint64(&am.vhostRequestPendingCount))

	prefix := fmt.Sprintf(keyVHostQueueAcctToId, "")
	accounts := make([]string, 0, limit)
	err := am.server.store.View(func(tx *buntdb.Tx) error {
		return tx.Ascend(vhostRequestIdx, func(key, value string) bool {
			accounts = append(accounts, strings.TrimPrefix(key, prefix))
			return len(accounts) < limit
		})
	})

	if err != nil {
		am.server.logger.Error("internal", "couldn't traverse vhost queue", err.Error())
		return
	}

	for _, account := range accounts {
		accountInfo, err := am.LoadAccount(account)
		if err == nil {
			requests = append(requests, PendingVHostRequest{
				Account:   account,
				VHostInfo: accountInfo.VHost,
			})
		} else {
			am.server.logger.Error("internal", "corrupt account", account, err.Error())
		}
	}
	return
}

func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
	// if hostserv is disabled in config, then don't grant vhosts
	// that were previously approved while it was enabled
	if !am.server.AccountConfig().VHosts.Enabled {
		return
	}

	vhost := ""
	if info.Enabled {
		vhost = info.ApprovedVHost
	}
	oldNickmask := client.NickMaskString()
	updated := client.SetVHost(vhost)
	if updated {
		// TODO: doing I/O here is kind of a kludge
		go client.sendChghost(oldNickmask, client.Hostname())
	}
}

func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
	am.RLock()
	clients := am.accountToClients[account]
	am.RUnlock()

	for _, client := range clients {
		am.applyVHostInfo(client, result)
	}
}

func (am *AccountManager) Login(client *Client, account ClientAccount) {
	changed := client.SetAccountName(account.Name)
	if !changed {
		return
	}

	client.nickTimer.Touch(nil)

	am.applyVHostInfo(client, account.VHost)

	casefoldedAccount := client.Account()
	am.Lock()
	defer am.Unlock()
	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
}

func (am *AccountManager) Logout(client *Client) {
	am.Lock()
	defer am.Unlock()

	casefoldedAccount := client.Account()
	if casefoldedAccount == "" {
		return
	}

	am.logoutOfAccount(client)

	clients := am.accountToClients[casefoldedAccount]
	if len(clients) <= 1 {
		delete(am.accountToClients, casefoldedAccount)
		return
	}
	remainingClients := make([]*Client, len(clients)-1)
	remainingPos := 0
	for currentPos := 0; currentPos < len(clients); currentPos++ {
		if clients[currentPos] != client {
			remainingClients[remainingPos] = clients[currentPos]
			remainingPos++
		}
	}
	am.accountToClients[casefoldedAccount] = remainingClients
}

var (
	// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
	// This can be moved to some other data structure/place if we need to load/unload mechs later.
	EnabledSaslMechanisms = map[string]func(*Server, *Client, string, []byte, *ResponseBuffer) bool{
		"PLAIN":    authPlainHandler,
		"EXTERNAL": authExternalHandler,
	}
)

// AccountCredentials stores the various methods for verifying accounts.
type AccountCredentials struct {
	Version        uint
	PassphraseSalt []byte // legacy field, not used by v1 and later
	PassphraseHash []byte
	Certificate    string // fingerprint
}

// ClientAccount represents a user account.
type ClientAccount struct {
	// Name of the account.
	Name string
	// RegisteredAt represents the time that the account was registered.
	RegisteredAt    time.Time
	Credentials     AccountCredentials
	Verified        bool
	AdditionalNicks []string
	VHost           VHostInfo
}

// convenience for passing around raw serialized account data
type rawClientAccount struct {
	Name            string
	RegisteredAt    string
	Credentials     string
	Callback        string
	Verified        bool
	AdditionalNicks string
	VHost           string
}

// logoutOfAccount logs the client out of their current account.
func (am *AccountManager) logoutOfAccount(client *Client) {
	if client.Account() == "" {
		// already logged out
		return
	}

	client.SetAccountName("")
	go client.nickTimer.Touch(nil)

	// dispatch account-notify
	// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
	go func() {
		for friend := range client.Friends(caps.AccountNotify) {
			friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*")
		}
	}()
}