package guardian

// PAT stands for personal access token. These are used as tokens that
// don't expire, mainly for CLI tools. Personal access tokens are not
// obtained via standard oauth2 flow.

import (
	"errors"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"

	"code.justin.tv/systems/guardian/osin"
)

// PATClientID is the client id for personal access tokens
const PATClientID = "personal"

// PersonalAccessToken holds data for a personal access token
type PersonalAccessToken struct {
	Token string `json:"token,omitempty"`
	Name  string `json:"name"`
}

// getPATClient retrieves personal access token client
func (db *Storage) getPATClient() (client *Client, err error) {
	c, err := db.GetClient(PATClientID)
	if err != nil {
		return
	}

	client, ok := c.(*Client)
	if !ok && c != nil {
		err = errors.New("retrieved client is not of type *guardian.Client")
		return
	}
	return
}

// createPATClient creates and saves personal access token client
func (db *Storage) createPATClient() (client *Client, err error) {
	client, err = NewClient(PATClientID, "", "personal access token", "")
	if err != nil {
		return
	}
	client.ID = PATClientID

	err = db.SaveClient(client)
	if err != nil {
		return
	}
	return
}

// GetOrCreatePATClient retrieves the personal access token client. If no
// client exists in the db, creates one.
func (db *Storage) GetOrCreatePATClient() (client *Client, err error) {
	client, err = db.getPATClient()
	if err != nil {
		return
	}

	if client != nil {
		return
	}

	client, err = db.createPATClient()
	if err != nil {
		return
	}
	return
}

// GetPersonalAccessToken retrieves a personal access token
func (db *Storage) GetPersonalAccessToken(token string) (pat *PersonalAccessToken, err error) {
	data, err := db.LoadAccess(token)
	if err != nil {
		return
	}

	if data.Version == "invalid" {
		pat = nil
		return
	}

	pat = &PersonalAccessToken{
		Name: data.Name,
	}
	return
}

// CreatePersonalAccessToken creates a personal access token with provided name
func (db *Storage) CreatePersonalAccessToken(name string, user *User) (token *PersonalAccessToken, err error) {
	patClient, err := db.GetOrCreatePATClient()
	if err != nil {
		err = fmt.Errorf("error getting pat client: %s", err.Error())
		return
	}

	accessData := &osin.AccessData{
		Client:    patClient,
		CreatedAt: time.Now(),
		ExpiresIn: 0,
		Name:      name,
		Version:   patClient.Version,
		UserData:  user,
	}

	accessData.AccessToken, _, err = DefaultTokenGenerator.GenerateAccessToken(accessData, false)
	if err != nil {
		err = fmt.Errorf("error generating access token: %s", err.Error())
		return
	}

	err = db.SaveAccess(accessData)
	if err != nil {
		err = fmt.Errorf("error saving access token: %s", err.Error())
		return
	}

	token = &PersonalAccessToken{
		Token: accessData.AccessToken,
		Name:  accessData.Name,
	}
	return
}

// ListPersonalAccessTokens lists personal access tokens associated with provided user
func (db *Storage) ListPersonalAccessTokens(user string) (tokens []*PersonalAccessToken, err error) {
	params := &dynamodb.ScanInput{
		TableName:        aws.String(db.Config.AuthorizationsTable),
		ConsistentRead:   aws.Bool(true),
		FilterExpression: aws.String("user = :cn AND client_id = :client_id AND version != :version"),
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":cn":        {S: aws.String(user)},
			":client_id": {S: aws.String(PATClientID)},
			":version":   {S: aws.String("invalid")},
		},
	}
	results, err := db.DB.Scan(params)
	if err != nil {
		err = fmt.Errorf("guardian: error listing access tokens: %s", err.Error())
		return
	}

	tokens = make([]*PersonalAccessToken, aws.Int64Value(results.Count))
	for i, v := range results.Items {
		var name string
		if n, ok := v["name"]; ok {
			name = aws.StringValue(n.S)
		}
		tokens[i] = &PersonalAccessToken{Name: name}
	}
	return
}

// UpdatePersonalAccessToken updates a token's name
func (db *Storage) UpdatePersonalAccessToken(token, name string) (data *PersonalAccessToken, err error) {
	accessData, err := db.LoadAccess(token)
	if err != nil {
		return
	}

	accessData.Name = name

	err = db.SaveAccess(accessData)
	if err != nil {
		return
	}

	data = &PersonalAccessToken{
		Name: accessData.Name,
	}
	return
}

// DeletePersonalAccessToken invalidates a token by resetting version
func (db *Storage) DeletePersonalAccessToken(token string) (err error) {
	data, err := db.LoadAccess(token)
	if err != nil {
		return
	}

	data.Version = "invalid"
	err = db.SaveAccess(data)
	if err != nil {
		return
	}
	return
}
