package auth

import (
	"errors"
	"time"

	"code.secondbit.org/uuid"
)

const (
	defaultTokenExpiration        = 3600  // one hour
	defaultRefreshTokenExpiration = 86400 // one day
)

var (
	// ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first.
	ErrNoTokenStore = errors.New("no tokenStore was specified for the Context")
	// ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore.
	ErrTokenNotFound = errors.New("token not found in tokenStore")
	// ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with
	// the same AccessToken property already exists in the tokenStore.
	ErrTokenAlreadyExists = errors.New("token already exists in tokenStore")
)

// Token represents an access and/or refresh token that the Client can use to access user data
// or obtain a new access token.
type Token struct {
	AccessToken      string
	RefreshToken     string
	Created          time.Time
	CreatedFrom      string
	ExpiresIn        int32
	RefreshExpiresIn int32
	TokenType        string
	Scope            string
	ProfileID        uuid.ID
	Revoked          bool
}

type tokenStore interface {
	// BUG(paddy): need to be able to revoke tokens and refresh tokens
	getToken(token string, refresh bool) (Token, error)
	saveToken(token Token) error
	removeToken(token string) error
	revokeToken(token string, refresh bool) error
	getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
}

func (m *memstore) getToken(token string, refresh bool) (Token, error) {
	if refresh {
		t, err := m.lookupTokenByRefresh(token)
		if err != nil {
			return Token{}, err
		}
		token = t
	}
	m.tokenLock.RLock()
	defer m.tokenLock.RUnlock()
	result, ok := m.tokens[token]
	if !ok {
		return Token{}, ErrTokenNotFound
	}
	return result, nil
}

func (m *memstore) saveToken(token Token) error {
	m.tokenLock.Lock()
	defer m.tokenLock.Unlock()
	_, ok := m.tokens[token.AccessToken]
	if ok {
		return ErrTokenAlreadyExists
	}
	m.tokens[token.AccessToken] = token
	if token.RefreshToken != "" {
		m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
	}
	if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
		m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
	} else {
		m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
	}
	return nil
}

func (m *memstore) removeToken(token string) error {
	m.tokenLock.Lock()
	defer m.tokenLock.Unlock()
	t, ok := m.tokens[token]
	if !ok {
		return ErrTokenNotFound
	}
	delete(m.tokens, token)
	if t.RefreshToken != "" {
		delete(m.refreshTokenLookup, t.RefreshToken)
	}
	pos := -1
	for p, item := range m.profileTokenLookup[t.ProfileID.String()] {
		if item == token {
			pos = p
			break
		}
	}
	if pos >= 0 {
		m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...)
	}
	return nil
}

func (m *memstore) revokeToken(token string, refresh bool) error {
	if refresh {
		t, err := m.lookupTokenByRefresh(token)
		if err != nil {
			return err
		}
		token = t
	}
	m.tokenLock.Lock()
	defer m.tokenLock.Unlock()
	t, ok := m.tokens[token]
	if !ok {
		return ErrTokenNotFound
	}
	t.Revoked = true
	m.tokens[token] = t
	return nil
}

func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
	ids, err := m.lookupTokensByProfileID(profileID.String())
	if err != nil {
		return []Token{}, err
	}
	if len(ids) > num+offset {
		ids = ids[offset : num+offset]
	} else if len(ids) > offset {
		ids = ids[offset:]
	} else {
		return []Token{}, nil
	}
	tokens := []Token{}
	for _, id := range ids {
		token, err := m.getToken(id, false)
		if err != nil {
			return []Token{}, err
		}
		tokens = append(tokens, token)
	}
	return tokens, nil
}
