package auth

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"regexp"
	"strings"
	"time"

	"code.secondbit.org/uuid.hg"
	"github.com/gorilla/mux"
)

const (
	// MinPassphraseLength is the minimum length, in bytes, of a passphrase, exclusive.
	MinPassphraseLength = 6
	// MaxPassphraseLength is the maximum length, in bytes, of a passphrase, exclusive.
	MaxPassphraseLength = 64
	// CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme
	CurPassphraseScheme = 1
	// MaxNameLength is the maximum length, in bytes, of a name, exclusive.
	MaxNameLength = 64
	// MaxEmailLength is the maximum length, in bytes, of an email address, exclusive.
	MaxEmailLength = 64
)

var (
	// ErrNoProfileStore is returned when a Context tries to act on a profileStore without setting one first.
	ErrNoProfileStore = errors.New("no profileStore was specified for the Context")
	// ErrProfileAlreadyExists is returned when a Profile is added to a profileStore, but another Profile with
	// the same ID already exists in the profileStore.
	ErrProfileAlreadyExists = errors.New("profile already exists in profileStore")
	// ErrProfileNotFound is returned when a Profile is requested but not found in the profileStore.
	ErrProfileNotFound = errors.New("profile not found in profileStore")
	// ErrLoginAlreadyExists is returned when a Login is added to a profileStore, but another Login with the same
	// Type and Value already exists in the profileStore.
	ErrLoginAlreadyExists = errors.New("login already exists in profileStore")
	// ErrLoginNotFound is returned when a Login is requested but not found in the profileStore.
	ErrLoginNotFound = errors.New("login not found in profileStore")
	// ErrLoginVerificationInvalid is returned when a Login is verified with the wrong verification code.
	ErrLoginVerificationInvalid = errors.New("login verification code incorrect")

	// ErrMissingPassphrase is returned when a ProfileChange is validated but does not contain a
	// Passphrase, and requires one.
	ErrMissingPassphrase = errors.New("missing passphrase")
	// ErrMissingPassphraseReset is returned when a ProfileChange is validated but does not contain
	// a PassphraseReset, and requires one.
	ErrMissingPassphraseReset = errors.New("missing passphrase reset")
	// ErrMissingPassphraseResetCreated is returned when a ProfileChange is validated but does not
	// contain a PassphraseResetCreated, and requires one.
	ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
	// ErrPassphraseTooShort is returned when a ProfileChange is validated and contains a Passphrase,
	// but the Passphrase is shorter than MinPassphraseLength.
	ErrPassphraseTooShort = errors.New("passphrase too short")
	// ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase,
	// but the Passphrase is longer than MaxPassphraseLength.
	ErrPassphraseTooLong = errors.New("passphrase too long")

	// ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected
	// of being compromised.
	ErrProfileCompromised = errors.New("profile compromised")
	// ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain
	// duration, to prevent brute force attacks.
	ErrProfileLocked = errors.New("profile locked")

	ScopeLoginAdmin = Scope{ID: "login_admin", Name: "Administer Logins", Description: "Read and write logins, bypassing ACL."}
)

// Profile represents a single user of the service,
// including their authentication information.
type Profile struct {
	ID                     uuid.ID   `json:"id,omitempty"`
	Name                   string    `json:"name,omitempty"`
	Passphrase             string    `json:"-"`
	Iterations             int       `json:"-"`
	Salt                   string    `json:"-"`
	PassphraseScheme       int       `json:"-"`
	Compromised            bool      `json:"-"`
	LockedUntil            time.Time `json:"-"`
	PassphraseReset        string    `json:"-"`
	PassphraseResetCreated time.Time `json:"-"`
	Created                time.Time `json:"created,omitempty"`
	LastSeen               time.Time `json:"last_seen,omitempty"`
}

// ApplyChange applies the properties of the passed ProfileChange
// to the Profile it is called on.
func (p *Profile) ApplyChange(change ProfileChange) {
	if change.Name != nil {
		p.Name = *change.Name
	}
	if change.Passphrase != nil {
		p.Passphrase = *change.Passphrase
	}
	if change.Iterations != nil {
		p.Iterations = *change.Iterations
	}
	if change.Salt != nil {
		p.Salt = *change.Salt
	}
	if change.PassphraseScheme != nil {
		p.PassphraseScheme = *change.PassphraseScheme
	}
	if change.Compromised != nil {
		p.Compromised = *change.Compromised
	}
	if change.LockedUntil != nil {
		p.LockedUntil = *change.LockedUntil
	}
	if change.PassphraseReset != nil {
		p.PassphraseReset = *change.PassphraseReset
	}
	if change.PassphraseResetCreated != nil {
		p.PassphraseResetCreated = *change.PassphraseResetCreated
	}
	if change.LastSeen != nil {
		p.LastSeen = *change.LastSeen
	}
}

// ApplyBulkChange applies the properties of the passed BulkProfileChange
// to the Profile it is called on.
func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
	if change.Compromised != nil {
		p.Compromised = *change.Compromised
	}
}

// ProfileChange represents a single atomic change to a Profile's mutable data.
type ProfileChange struct {
	Name                   *string
	Passphrase             *string
	Iterations             *int
	Salt                   *string
	PassphraseScheme       *int
	Compromised            *bool
	LockedUntil            *time.Time
	PassphraseReset        *string
	PassphraseResetCreated *time.Time
	LastSeen               *time.Time
}

func (c ProfileChange) Empty() bool {
	return (c.Name == nil && c.Passphrase == nil && c.Iterations == nil && c.Salt == nil && c.PassphraseScheme == nil && c.Compromised == nil && c.LockedUntil == nil && c.PassphraseReset == nil && c.PassphraseResetCreated == nil && c.LastSeen == nil)
}

// Validate checks the ProfileChange it is called on
// and asserts its internal validity, or lack thereof.
// A descriptive error will be returned in the case of
// an invalid change.
func (c ProfileChange) Validate() error {
	if c.Empty() {
		return ErrEmptyChange
	}
	if c.PassphraseScheme != nil && c.Passphrase == nil {
		return ErrMissingPassphrase
	}
	if c.PassphraseReset != nil && c.PassphraseResetCreated == nil {
		return ErrMissingPassphraseResetCreated
	}
	if c.PassphraseReset == nil && c.PassphraseResetCreated != nil {
		return ErrMissingPassphraseReset
	}
	if c.Salt != nil && c.Passphrase == nil {
		return ErrMissingPassphrase
	}
	if c.Iterations != nil && c.Passphrase == nil {
		return ErrMissingPassphrase
	}
	return nil
}

// BulkProfileChange represents a single atomic change to many Profiles' mutable data.
// It is a subset of a ProfileChange, as it doesn't make sense to mutate some of the
// ProfileChange values across many Profiles all at once.
type BulkProfileChange struct {
	Compromised *bool
}

func (b BulkProfileChange) Empty() bool {
	return b.Compromised == nil
}

// Validate checks the BulkProfileChange it is called on
// and asserts its internal validity, or lack thereof.
// A descriptive error will be returned in the case of an
// invalid change.
func (b BulkProfileChange) Validate() error {
	if b.Empty() {
		return ErrEmptyChange
	}
	return nil
}

// Login represents a single human-friendly identifier for
// a given Profile that can be used to log into that Profile.
// Each Profile may only have one Login for each Type.
type Login struct {
	Type         string    `json:"type,omitempty"`
	Value        string    `json:"value,omitempty"`
	ProfileID    uuid.ID   `json:"profile_id,omitempty"`
	Created      time.Time `json:"created,omitempty"`
	LastUsed     time.Time `json:"last_used,omitempty"`
	Verification string    `json:"-"`
	Verified     bool      `json:"verified"`
}

type LoginChange struct {
	Verification       *string `json:"verification,omitempty"`
	ResendVerification *bool   `json:"resend_verification,omitempty"`
}

type newProfileRequest struct {
	Email      string `json:"email"`
	Passphrase string `json:"passphrase"`
	Name       string `json:"name"`
}

func validateNewProfileRequest(req *newProfileRequest) []RequestError {
	errors := []RequestError{}
	req.Name = strings.TrimSpace(req.Name)
	req.Email = strings.TrimSpace(req.Email)
	if len(req.Passphrase) < MinPassphraseLength {
		errors = append(errors, RequestError{
			Slug:  RequestErrInsufficient,
			Field: "/passphrase",
		})
	}
	if len(req.Passphrase) > MaxPassphraseLength {
		errors = append(errors, RequestError{
			Slug:  RequestErrOverflow,
			Field: "/passphrase",
		})
	}
	if len(req.Name) > MaxNameLength {
		errors = append(errors, RequestError{
			Slug:  RequestErrOverflow,
			Field: "/name",
		})
	}
	if req.Email == "" {
		errors = append(errors, RequestError{
			Slug:  RequestErrMissing,
			Field: "/email",
		})
	}
	if len(req.Email) > MaxEmailLength {
		errors = append(errors, RequestError{
			Slug:  RequestErrOverflow,
			Field: "/email",
		})
	}
	re := regexp.MustCompile(".+@.+\\..+")
	if !re.Match([]byte(req.Email)) {
		errors = append(errors, RequestError{
			Slug:  RequestErrInvalidFormat,
			Field: "/email",
		})
	}
	return errors
}

type profileStore interface {
	getProfileByID(id uuid.ID) (Profile, error)
	getProfileByLogin(value string) (Profile, error)
	saveProfile(profile Profile) error
	updateProfile(id uuid.ID, change ProfileChange) error
	updateProfiles(ids []uuid.ID, change BulkProfileChange) error
	deleteProfile(id uuid.ID) error

	addLogin(login Login) error
	getLogin(value string) (Login, error)
	removeLogin(value string, profile uuid.ID) error
	removeLoginsByProfile(profile uuid.ID) error
	recordLoginUse(value string, when time.Time) error
	verifyLogin(value, verification string) error
	listLogins(profile uuid.ID, num, offset int) ([]Login, error)
}

func (m *memstore) getProfileByID(id uuid.ID) (Profile, error) {
	m.profileLock.RLock()
	defer m.profileLock.RUnlock()
	p, ok := m.profiles[id.String()]
	if !ok {
		return Profile{}, ErrProfileNotFound
	}
	return p, nil
}

func (m *memstore) getProfileByLogin(value string) (Profile, error) {
	m.loginLock.RLock()
	defer m.loginLock.RUnlock()
	login, ok := m.logins[value]
	if !ok {
		return Profile{}, ErrLoginNotFound
	}
	m.profileLock.RLock()
	defer m.profileLock.RUnlock()
	profile, ok := m.profiles[login.ProfileID.String()]
	if !ok {
		return Profile{}, ErrProfileNotFound
	}
	return profile, nil
}

func (m *memstore) saveProfile(profile Profile) error {
	m.profileLock.Lock()
	defer m.profileLock.Unlock()
	_, ok := m.profiles[profile.ID.String()]
	if ok {
		return ErrProfileAlreadyExists
	}
	m.profiles[profile.ID.String()] = profile
	return nil
}

func (m *memstore) updateProfile(id uuid.ID, change ProfileChange) error {
	m.profileLock.Lock()
	defer m.profileLock.Unlock()
	p, ok := m.profiles[id.String()]
	if !ok {
		return ErrProfileNotFound
	}
	p.ApplyChange(change)
	m.profiles[id.String()] = p
	return nil
}

func (m *memstore) updateProfiles(ids []uuid.ID, change BulkProfileChange) error {
	m.profileLock.Lock()
	defer m.profileLock.Unlock()
	for id, profile := range m.profiles {
		for _, i := range ids {
			if id == i.String() {
				profile.ApplyBulkChange(change)
				m.profiles[id] = profile
				break
			}
		}
	}
	return nil
}

func (m *memstore) deleteProfile(id uuid.ID) error {
	m.profileLock.Lock()
	defer m.profileLock.Unlock()
	if _, ok := m.profiles[id.String()]; !ok {
		return ErrProfileNotFound
	}
	delete(m.profiles, id.String())
	return nil
}

func (m *memstore) addLogin(login Login) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	_, ok := m.logins[login.Value]
	if ok {
		return ErrLoginAlreadyExists
	}
	m.logins[login.Value] = login
	m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Value)
	return nil
}

func (m *memstore) getLogin(value string) (Login, error) {
	m.loginLock.RLock()
	defer m.loginLock.RUnlock()
	l, ok := m.logins[value]
	if !ok {
		return Login{}, ErrLoginNotFound
	}
	return l, nil
}

func (m *memstore) removeLogin(value string, profile uuid.ID) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	l, ok := m.logins[value]
	if !ok {
		return ErrLoginNotFound
	}
	if !l.ProfileID.Equal(profile) {
		return ErrLoginNotFound
	}
	delete(m.logins, value)
	pos := -1
	for p, id := range m.profileLoginLookup[profile.String()] {
		if id == value {
			pos = p
			break
		}
	}
	if pos >= 0 {
		m.profileLoginLookup[profile.String()] = append(m.profileLoginLookup[profile.String()][:pos], m.profileLoginLookup[profile.String()][pos+1:]...)
	}
	return nil
}

func (m *memstore) removeLoginsByProfile(profile uuid.ID) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	logins, ok := m.profileLoginLookup[profile.String()]
	if !ok {
		return ErrProfileNotFound
	}
	delete(m.profileLoginLookup, profile.String())
	for _, login := range logins {
		delete(m.logins, login)
	}
	return nil
}

func (m *memstore) recordLoginUse(value string, when time.Time) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	l, ok := m.logins[value]
	if !ok {
		return ErrLoginNotFound
	}
	l.LastUsed = when
	m.logins[value] = l
	return nil
}

func (m *memstore) verifyLogin(value, verification string) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	l, ok := m.logins[value]
	if !ok {
		return ErrLoginNotFound
	}
	if l.Verification != verification {
		return ErrLoginVerificationInvalid
	}
	l.Verified = true
	m.logins[value] = l
	return nil
}

func (m *memstore) listLogins(profile uuid.ID, num, offset int) ([]Login, error) {
	m.loginLock.RLock()
	defer m.loginLock.RUnlock()
	ids, ok := m.profileLoginLookup[profile.String()]
	if !ok {
		return []Login{}, nil
	}
	if len(ids) > num+offset {
		ids = ids[offset : num+offset]
	} else if len(ids) > offset {
		ids = ids[offset:]
	} else {
		return []Login{}, nil
	}
	logins := []Login{}
	for _, id := range ids {
		login, ok := m.logins[id]
		if !ok {
			continue
		}
		logins = append(logins, login)
	}
	return logins, nil
}

func cleanUpAfterProfileDeletion(profile uuid.ID, context Context) {
	err := context.RemoveLoginsByProfile(profile)
	if err != nil {
		log.Printf("Error removing logins from profile %s: %+v\n", profile, err)
	}
	err = context.TerminateSessionsByProfile(profile)
	if err != nil {
		log.Printf("Error terminating sessions associated with profile %s: %+v\n", profile, err)
	}
	err = context.RevokeTokensByProfileID(profile)
	if err != nil {
		log.Printf("Error revoking tokens associated with profile %s: %+v\n", profile, err)
	}
	err = context.DeleteAuthorizationCodesByProfileID(profile)
	if err != nil {
		log.Printf("Error deleting authorization codes associated with profile %s: %+v\n", profile, err)
	}
	clients, err := context.ListClientsByOwner(profile, -1, 0)
	if err != nil {
		log.Printf("Error listing clients by profile %s: %+v\n", profile, err)
	}
	err = context.DeleteClientsByOwner(profile)
	if err != nil {
		log.Printf("Error deleting clients by profile %s: %+v\n", profile, err)
	}
	for _, client := range clients {
		cleanUpAfterClientDeletion(client.ID, context)
	}
}

// RegisterProfileHandlers adds handlers to the passed router to handle the profile endpoints, like registration and user retrieval.
func RegisterProfileHandlers(r *mux.Router, context Context) {
	r.Handle("/profiles", wrap(context, CreateProfileHandler)).Methods("POST", "OPTIONS")
	r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS")
	r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS")
	r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS")
	// BUG(paddy): We need to implement a handler that will add a login to a profile.
	// BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login?
	// BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
	r.Handle("/logins/{login}", wrap(context, GetLoginHandler)).Methods("GET", "OPTIONS")
	r.Handle("/logins/{login}", wrap(context, UpdateLoginHandler)).Methods("PUT", "PATCH", "OPTIONS")
}

// GetProfileHandler is an HTTP handler for retrieving a profile.
func GetProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
	errors := []RequestError{}
	authz := r.Header.Get("Authorization")
	if !strings.HasPrefix(authz, "Bearer ") {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
		return
	}
	authz = strings.TrimPrefix(authz, "Bearer ")
	vars := mux.Vars(r)
	if vars["id"] == "" {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	id, err := uuid.Parse(vars["id"])
	if err != nil {
		errors = append(errors, RequestError{Slug: RequestErrInvalidFormat, Param: "id"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	token, err := context.GetToken(authz, false)
	if err != nil || token.Revoked {
		if err == ErrTokenNotFound || token.Revoked {
			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
			return
		} else {
			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
			return
		}
	}
	if !id.Equal(token.ProfileID) {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusForbidden, Response{Errors: errors})
		return
	}
	profile, err := context.GetProfileByID(id)
	if err != nil {
		if err == ErrProfileNotFound {
			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "id"})
			encode(w, r, http.StatusNotFound, Response{Errors: errors})
			return
		}
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
	return
}

// CreateProfileHandler is an HTTP handler for registering new profiles.
func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
	scheme, ok := passphraseSchemes[CurPassphraseScheme]
	if !ok {
		log.Printf("Error selecting passphrase scheme #%d\n", CurPassphraseScheme)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	var req newProfileRequest
	errors := []RequestError{}
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&req)
	if err != nil {
		encode(w, r, http.StatusBadRequest, invalidFormatResponse)
		return
	}
	errors = append(errors, validateNewProfileRequest(&req)...)
	if len(errors) > 0 {
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
	if err != nil {
		log.Printf("Error creating encoded passphrase: %#+v\n", err)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	profile := Profile{
		ID:               uuid.NewID(),
		Name:             req.Name,
		Passphrase:       string(passphrase),
		Iterations:       context.config.iterations,
		Salt:             string(salt),
		PassphraseScheme: CurPassphraseScheme,
		Created:          time.Now(),
		LastSeen:         time.Now(),
	}
	err = context.SaveProfile(profile)
	if err != nil {
		if err == ErrProfileAlreadyExists {
			encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/id"}}})
			return
		}
		log.Printf("Error saving profile: %#+v\n", err)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	logins := []Login{}
	login := Login{
		Type:         "email",
		Value:        req.Email,
		Created:      profile.Created,
		LastUsed:     profile.Created,
		ProfileID:    profile.ID,
		Verification: uuid.NewID().String(),
	}
	err = context.AddLogin(login)
	if err != nil {
		if err == ErrLoginAlreadyExists {
			encode(w, r, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrConflict, Field: "/email"}}})
			return
		}
		log.Printf("Error adding login: %#+v\n", err)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	logins = append(logins, login)
	resp := Response{
		Logins:   logins,
		Profiles: []Profile{profile},
	}
	encode(w, r, http.StatusCreated, resp)
	go context.SendLoginVerification(login) // this should key off that model event, instead
	//BUG(paddy): Need to trigger a ModelEvent saying the profile was created
	// go context.SendModelEvent(profile, events.ActionCreated)
}

func UpdateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
	errors := []RequestError{}
	vars := mux.Vars(r)
	if vars["id"] == "" {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	id, err := uuid.Parse(vars["id"])
	if err != nil {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	username, password, ok := r.BasicAuth()
	if !ok {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
		return
	}
	profile, err := authenticate(username, password, context)
	if err != nil {
		if isAuthError(err) {
			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
		} else {
			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
		}
		return
	}
	if !profile.ID.Equal(id) {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusForbidden, Response{Errors: errors})
		return
	}
	var req ProfileChange
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&req)
	if err != nil {
		log.Printf("Error decoding request: %#+v\n", err)
		encode(w, r, http.StatusBadRequest, invalidFormatResponse)
		return
	}
	req.Iterations = nil
	req.Salt = nil
	req.PassphraseScheme = nil
	req.Compromised = nil // BUG(paddy): Need a way for admins to mark accounts as compromised
	req.LockedUntil = nil
	req.LastSeen = nil
	if req.Passphrase != nil {
		if len(*req.Passphrase) < MinPassphraseLength {
			errors = append(errors, RequestError{Slug: RequestErrInsufficient, Field: "/passphrase"})
			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
			return
		}
		if len(*req.Passphrase) > MaxPassphraseLength {
			errors = append(errors, RequestError{Slug: RequestErrOverflow, Field: "/passphrase"})
			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
			return
		}
		iterations := context.config.iterations
		scheme, ok := passphraseSchemes[CurPassphraseScheme]
		if !ok {
			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
			return
		}
		curScheme := CurPassphraseScheme
		req.PassphraseScheme = &curScheme
		passphrase, salt, err := scheme.create(*req.Passphrase, iterations)
		if err != nil {
			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
			return
		}
		req.Passphrase = &passphrase
		req.Salt = &salt
		req.Iterations = &iterations
	}
	if req.PassphraseReset != nil {
		now := time.Now()
		req.PassphraseResetCreated = &now
	}
	err = req.Validate()
	if err != nil {
		var status int
		var resp Response
		switch err {
		case ErrEmptyChange:
			resp.Profiles = []Profile{profile}
			status = http.StatusOK
		default:
			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
			resp.Errors = errors
			status = http.StatusInternalServerError
		}
		encode(w, r, status, resp)
		return
	}
	err = context.UpdateProfile(id, req)
	if err != nil {
		if err == ErrProfileNotFound {
			errors = append(errors, RequestError{Slug: RequestErrNotFound})
			encode(w, r, http.StatusNotFound, Response{Errors: errors})
			return
		}
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	profile.ApplyChange(req)
	encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
	return
}

func DeleteProfileHandler(w http.ResponseWriter, r *http.Request, context Context) {
	errors := []RequestError{}
	vars := mux.Vars(r)
	if vars["id"] == "" {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "id"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	id, err := uuid.Parse(vars["id"])
	if err != nil {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	username, password, ok := r.BasicAuth()
	if !ok {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
		return
	}
	profile, err := authenticate(username, password, context)
	if err != nil {
		if isAuthError(err) {
			errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
			encode(w, r, http.StatusUnauthorized, Response{Errors: errors})
		} else {
			errors = append(errors, RequestError{Slug: RequestErrActOfGod})
			encode(w, r, http.StatusInternalServerError, Response{Errors: errors})
		}
		return
	}
	if !profile.ID.Equal(id) {
		errors = append(errors, RequestError{Slug: RequestErrAccessDenied})
		encode(w, r, http.StatusForbidden, Response{Errors: errors})
		return
	}
	err = context.DeleteProfile(id)
	if err != nil {
		if err == ErrProfileNotFound {
			errors = append(errors, RequestError{Slug: RequestErrNotFound})
			encode(w, r, http.StatusNotFound, Response{Errors: errors})
			return
		}
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	encode(w, r, http.StatusOK, Response{Profiles: []Profile{profile}})
	go cleanUpAfterProfileDeletion(profile.ID, context)
}

func GetLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
	var errors []RequestError
	vars := mux.Vars(r)
	if vars["login"] == "" {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	login, err := context.GetLogin(vars["login"])
	if err != nil {
		if err == ErrLoginNotFound {
			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
			encode(w, r, http.StatusNotFound, Response{Errors: errors})
			return
		}
		log.Printf("Error retrieving login: %#+v\n", err)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
}

func UpdateLoginHandler(w http.ResponseWriter, r *http.Request, context Context) {
	var errors []RequestError
	vars := mux.Vars(r)
	if vars["login"] == "" {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Param: "login"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	var req LoginChange
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&req)
	if err != nil {
		log.Printf("Error decoding request: %#+v\n", err)
		encode(w, r, http.StatusBadRequest, invalidFormatResponse)
		return
	}
	login, err := context.GetLogin(vars["login"])
	if err != nil {
		if err == ErrLoginNotFound {
			errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
			encode(w, r, http.StatusNotFound, Response{Errors: errors})
			return
		}
		log.Printf("Error retrieving login: %#+v\n", err)
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	if req.Verification != nil {
		err = context.VerifyLogin(vars["login"], *req.Verification)
		if err != nil {
			if err == ErrLoginNotFound {
				errors = append(errors, RequestError{Slug: RequestErrNotFound, Param: "login"})
				encode(w, r, http.StatusNotFound, Response{Errors: errors})
				return
			} else if err == ErrLoginVerificationInvalid {
				errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/verification"})
				encode(w, r, http.StatusBadRequest, Response{Errors: errors})
				return
			}
			log.Printf("Error verifying login with verification '%s': %#+v\n", *req.Verification, err)
			encode(w, r, http.StatusInternalServerError, actOfGodResponse)
			return
		}
		login.Verified = true
	} else if req.ResendVerification != nil {
		if !*req.ResendVerification {
			errors = append(errors, RequestError{Slug: RequestErrInvalidValue, Field: "/resend_verification"})
			encode(w, r, http.StatusBadRequest, Response{Errors: errors})
			return
		}
		context.SendLoginVerification(login)
	} else {
		errors = append(errors, RequestError{Slug: RequestErrMissing, Field: "/"})
		encode(w, r, http.StatusBadRequest, Response{Errors: errors})
		return
	}
	encode(w, r, http.StatusOK, Response{Logins: []Login{login}})
}
