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")

	// 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")
)

// 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"`
	Deleted                bool      `json:"deleted,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
	}
	if change.Deleted != nil {
		p.Deleted = *change.Deleted
	}
}

// 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
	Deleted                *bool
}

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 && c.Deleted == 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"`
}

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

	addLogin(login Login) error
	removeLogin(value string, profile uuid.ID) error
	removeLoginsByProfile(profile uuid.ID) error
	recordLoginUse(value string, when time.Time) 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
	}
	if p.Deleted {
		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
	}
	if profile.Deleted {
		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) 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) 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) 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)
	}
	// BUG(paddy): need to terminate all sessions associated with the Profile
	// BUG(paddy): need to invalidate all tokens associated with the Profile
	// BUG(paddy): need to delete all the grants associated with the Profile
}

// 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")
	// BUG(paddy): We need to implement a handler that will return information about a profile or set of profiles.
	r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH")
	r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE")
	// 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.
}

// 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,
	}
	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)
	// TODO(paddy): should we kick off the email validation flow?
}

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
	}
	if !profile.Deleted && req.Deleted != nil && *req.Deleted {
		go cleanUpAfterProfileDeletion(profile.ID, context)
	}
	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
	}
	var change ProfileChange
	deleted := true
	change.Deleted = &deleted
	err = context.UpdateProfile(id, change)
	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(change)
	encode(w, r, http.StatusOK, response{Profiles: []Profile{profile}})
	go cleanUpAfterProfileDeletion(profile.ID, context)
}
