package auth

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

	"code.secondbit.org/uuid"

	"github.com/extemporalgenome/slug"
)

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
	// MaxUsernameLength is the maximum length, in bytes, of a username, exclusive.
	MaxUsernameLength = 16
	// 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, but not
// including their username or email.
type Profile struct {
	ID                     uuid.ID
	Name                   string
	Passphrase             string
	Iterations             int
	Salt                   string
	PassphraseScheme       int
	Compromised            bool
	LockedUntil            time.Time
	PassphraseReset        string
	PassphraseResetCreated time.Time
	Created                time.Time
	LastSeen               time.Time
}

// 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
}

// 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.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 {
		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
	}
	if c.Passphrase != nil && len(*c.Passphrase) < MinPassphraseLength {
		return ErrPassphraseTooShort
	}
	if c.Passphrase != nil && len(*c.Passphrase) > MaxPassphraseLength {
		return ErrPassphraseTooLong
	}
	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
}

// 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.Compromised == nil {
		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
	Value     string
	ProfileID uuid.ID
	Created   time.Time
	LastUsed  time.Time
}

type newProfileRequest struct {
	Username   string `json:"username"`
	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)
	req.Username = slug.SlugAscii(strings.TrimSpace(req.Username))
	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 len(req.Username) > MaxUsernameLength {
		errors = append(errors, requestError{
			Slug:  requestErrOverflow,
			Field: "/username",
		})
	}
	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:  requestErrInvalidValue,
			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
	removeLogin(value string, 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
	}
	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()
	_, ok := m.profiles[id.String()]
	if !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) 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) 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
}

// 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 {
		// TODO(paddy): write error
		return
	}
	var req newProfileRequest
	errors := []requestError{}
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&req)
	if err != nil {
		// TODO(paddy): write error
		return
	}
	errors = append(errors, validateNewProfileRequest(&req)...)
	if len(errors) > 0 {
		//TODO(paddy): return errors
		return
	}
	passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations)
	if err != nil {
		// TODO(paddy): write error
		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 {
		// TODO(paddy): write error
		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 {
		// TODO(paddy): write error
		return
	}
	logins = append(logins, login)
	if req.Username != "" {
		login.Type = "username"
		login.Value = req.Username
		err = context.AddLogin(login)
		if err != nil {
			// TODO(paddy): write error
			return
		}
		logins = append(logins, login)
	}
	// TODO(paddy): respond with login(s) and profile that were created
	// TODO(paddy): should we kick off the email validation flow?
}
