package auth

import (
	"errors"
	"time"

	"code.secondbit.org/uuid"
)

const (
	MinPassphraseLength = 6
	MaxPassphraseLength = 64
)

var (
	ErrProfileAlreadyExists = errors.New("profile already exists in ProfileStore")
	ErrProfileNotFound      = errors.New("profile not found in ProfileStore")
	ErrLoginAlreadyExists   = errors.New("login already exists in ProfileStore")
	ErrLoginNotFound        = errors.New("login not found in ProfileStore")

	ErrMissingPassphrase             = errors.New("missing passphrase")
	ErrMissingPassphraseReset        = errors.New("missing passphrase reset")
	ErrMissingPassphraseResetCreated = errors.New("missing passphrase reset created timestamp")
	ErrPassphraseTooShort            = errors.New("passphrase too short")
	ErrPassphraseTooLong             = errors.New("passphrase too long")
)

type Profile struct {
	ID                     uuid.ID
	Name                   string
	Passphrase             string
	Iterations             int64
	Salt                   string
	PassphraseScheme       int
	Compromised            bool
	LockedUntil            time.Time
	PassphraseReset        string
	PassphraseResetCreated time.Time
	Created                time.Time
	LastSeen               time.Time
}

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

func (p *Profile) ApplyBulkChange(change BulkProfileChange) {
	if change.Compromised != nil {
		p.Compromised = *change.Compromised
	}
}

type ProfileChange struct {
	Name                   *string
	Passphrase             *string
	Iterations             *int64
	Salt                   *string
	PassphraseScheme       *int
	Compromised            *bool
	LockedUntil            *time.Time
	PassphraseReset        *string
	PassphraseResetCreated *time.Time
	LastSeen               *time.Time
}

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
}

type BulkProfileChange struct {
	Compromised *bool
}

func (b BulkProfileChange) Validate() error {
	if b.Compromised == nil {
		return ErrEmptyChange
	}
	return nil
}

type Login struct {
	Type      string
	Value     string
	ProfileID uuid.ID
	Created   time.Time
	LastUsed  time.Time
}

type ProfileStore interface {
	GetProfileByID(id uuid.ID) (Profile, error)
	GetProfileByLogin(loginType, 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(loginType, value string, profile uuid.ID) error
	RecordLoginUse(loginType, 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(loginType, value string) (Profile, error) {
	m.loginLock.RLock()
	defer m.loginLock.RUnlock()
	login, ok := m.logins[loginType+":"+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.Type+":"+login.Value]
	if ok {
		return ErrLoginAlreadyExists
	}
	m.logins[login.Type+":"+login.Value] = login
	m.profileLoginLookup[login.ProfileID.String()] = append(m.profileLoginLookup[login.ProfileID.String()], login.Type+":"+login.Value)
	return nil
}

func (m *Memstore) RemoveLogin(loginType, value string, profile uuid.ID) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	l, ok := m.logins[loginType+":"+value]
	if !ok {
		return ErrLoginNotFound
	}
	if !l.ProfileID.Equal(profile) {
		return ErrLoginNotFound
	}
	delete(m.logins, loginType+":"+value)
	pos := -1
	for p, id := range m.profileLoginLookup[profile.String()] {
		if id == loginType+":"+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(loginType, value string, when time.Time) error {
	m.loginLock.Lock()
	defer m.loginLock.Unlock()
	l, ok := m.logins[loginType+":"+value]
	if !ok {
		return ErrLoginNotFound
	}
	l.LastUsed = when
	m.logins[loginType+":"+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
}
