package auth

import (
	"encoding/json"
	"errors"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"time"

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

const (
	defaultTokenExpiration = 3600 // one hour
)

func init() {
	RegisterGrantType("refresh_token", GrantType{
		Validate:      refreshTokenValidate,
		Invalidate:    refreshTokenInvalidate,
		IssuesRefresh: true,
		ReturnToken:   RenderJSONToken,
		AuditString:   refreshTokenAuditString,
	})
}

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    `json:"access_token"`
	RefreshToken   string    `json:"refresh_token,omitempty"`
	Created        time.Time `json:"-"`
	CreatedFrom    string    `json:"created_from"`
	ExpiresIn      int32     `json:"expires_in"`
	TokenType      string    `json:"token_type"`
	Scopes         Scopes    `json:"-"`
	ProfileID      uuid.ID   `json:"profile_id"`
	ClientID       uuid.ID   `json:"client_id"`
	Revoked        bool      `json:"revoked,omitempty"`
	RefreshRevoked bool      `json:"refresh_revoked,omitempty"`
}

type tokenStore interface {
	getToken(token string, refresh bool) (Token, error)
	saveToken(token Token) error
	revokeToken(token string, refresh bool) error
	getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error)
	revokeTokensByProfileID(profileID uuid.ID) error
	revokeTokensByClientID(clientID uuid.ID) 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) 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
	}
	if refresh {
		t.RefreshRevoked = true
	} else {
		t.Revoked = true
	}
	m.tokens[token] = t
	return nil
}

func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error {
	ids, err := m.lookupTokensByProfileID(profileID.String())
	if err != nil {
		return err
	}
	if len(ids) < 1 {
		return ErrProfileNotFound
	}
	m.tokenLock.Lock()
	defer m.tokenLock.Unlock()
	for _, id := range ids {
		token := m.tokens[id]
		token.Revoked = true
		token.RefreshRevoked = true
		m.tokens[id] = token
	}
	return nil
}

func (m *memstore) revokeTokensByClientID(clientID uuid.ID) error {
	m.tokenLock.Lock()
	defer m.tokenLock.Unlock()
	for id, token := range m.tokens {
		if !token.ClientID.Equal(clientID) {
			continue
		}
		token.Revoked = true
		token.RefreshRevoked = true
		m.tokens[id] = token
	}
	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
}

func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) {
	enc := json.NewEncoder(w)
	refresh := r.PostFormValue("refresh_token")
	if refresh == "" {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_request")
		return
	}
	token, err := context.GetToken(refresh, true)
	if err != nil {
		if err == ErrTokenNotFound {
			w.WriteHeader(http.StatusBadRequest)
			renderJSONError(enc, "invalid_grant")
			return
		}
		log.Println("Error exchanging refresh token:", err)
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return
	}
	clientID, _, ok := getClientAuth(w, r, true)
	if !ok {
		return
	}
	if !token.ClientID.Equal(clientID) {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	if token.RefreshRevoked {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	return token.Scopes, token.ProfileID, true
}

func refreshTokenInvalidate(r *http.Request, context Context) error {
	refresh := r.PostFormValue("refresh_token")
	if refresh == "" {
		return ErrTokenNotFound
	}
	return context.RevokeToken(refresh, true)
}

func refreshTokenAuditString(r *http.Request) string {
	return "refresh_token:" + r.PostFormValue("refresh_token")
}

func RegisterTokenHandlers(r *mux.Router, context Context) {
	r.Handle("/tokens/{id}", wrap(context, GetTokenInfoHandler)).Methods("GET", "OPTIONS")
	r.Handle("/tokens/{id}", wrap(context, RevokeTokenHandler)).Methods("DELETE", "OPTIONS")
}

// GetTokenInfoHandler is an HTTP handler for retrieving information about a token.
func GetTokenInfoHandler(w http.ResponseWriter, r *http.Request, context Context) {
	errors := []requestError{}
	vars := mux.Vars(r)
	tokenID := vars["id"]
	if tokenID == "" {
		errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"})
		encode(w, r, http.StatusBadRequest, response{Errors: errors})
		return
	}
	token, err := context.GetToken(tokenID, false)
	if err != nil {
		if err == ErrTokenNotFound {
			errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"})
			encode(w, r, http.StatusNotFound, response{Errors: errors})
			return
		}
		encode(w, r, http.StatusInternalServerError, actOfGodResponse)
		return
	}
	token.RefreshToken = ""
	expired := int64(time.Now().Sub(token.Created) / time.Second)
	if expired > int64(token.ExpiresIn) {
		token.ExpiresIn = 0
	} else {
		token.ExpiresIn = token.ExpiresIn - int32(expired)
	}
	encode(w, r, http.StatusOK, response{Tokens: []Token{token}})
	return
}

// RevokeTokenHandler is an HTTP handler for revoking a Token prematurely.
func RevokeTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
	//errors := []requestError{}
	// TODO
}
