package auth

import (
	"errors"
	"time"

	"code.secondbit.org/uuid.hg"
)

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