package guardian

import (
	"errors"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	"golang.org/x/oauth2"

	"code.justin.tv/systems/guardian/osin"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func accessDataToAttributeValues(data *osin.AccessData) (params map[string]*dynamodb.AttributeValue) {
	params = map[string]*dynamodb.AttributeValue{
		"token_hash": {S: aws.String(data.AccessTokenHash)},
		"client_id":  {S: aws.String(data.Client.GetID())},
		"valid_from": {N: aws.String(strconv.FormatInt(data.CreatedAt.Unix(), 10))},
		"expires_in": {N: aws.String(strconv.FormatInt(int64(data.ExpiresIn), 10))},
		"version":    {S: aws.String(data.Version)},
		"type":       {S: aws.String(AccessTokenType)},
	}

	if data.RedirectURI != "" {
		params["callback_url"] = &dynamodb.AttributeValue{S: aws.String(data.RedirectURI)}
	}

	if data.Scope != "" {
		params["scopes"] = &dynamodb.AttributeValue{SS: aws.StringSlice(strings.Split(data.Scope, ","))}
	}

	if user, ok := data.UserData.(*User); ok {
		params["user"] = &dynamodb.AttributeValue{S: aws.String(user.CN)}
	}

	if data.Name != "" {
		params["name"] = &dynamodb.AttributeValue{S: aws.String(data.Name)}
	}

	return
}

func attributeValuesToAccessData(params map[string]*dynamodb.AttributeValue, db *Storage) (data *osin.AccessData, err error) {
	client, err := db.GetClient(aws.StringValue(params["client_id"].S))
	if err != nil {
		return
	}

	expiresIn, err := strconv.ParseInt(aws.StringValue(params["expires_in"].N), 10, 64)
	if err != nil {
		return
	}

	createdAt, err := strconv.ParseInt(aws.StringValue(params["valid_from"].N), 10, 64)
	if err != nil {
		return
	}

	data = &osin.AccessData{
		Client:    client,
		ExpiresIn: int32(expiresIn),
		CreatedAt: time.Unix(createdAt, 0),
		Version:   aws.StringValue(params["version"].S),
	}

	if _, ok := params["scopes"]; ok {
		data.Scope = strings.Join(aws.StringValueSlice(params["scopes"].SS), ",")
	}

	if _, ok := params["callback_url"]; ok {
		data.RedirectURI = aws.StringValue(params["callback_url"].S)
	}

	if _, ok := params["type"]; !ok {
		err = errors.New("guardian: missing token type from access data in db")
		return
	}

	switch aws.StringValue(params["type"].S) {
	case AccessTokenType:
		data.AccessTokenHash = aws.StringValue(params["token_hash"].S)
	case RefreshTokenType:
		data.RefreshTokenHash = aws.StringValue(params["token_hash"].S)
	}

	if u, ok := params["user"]; ok {
		data.UserData, err = db.Identifier.GetUser(aws.StringValue(u.S))
		if err != nil {
			return
		}
	}

	if _, ok := params["name"]; ok {
		data.Name = aws.StringValue(params["name"].S)
	}

	return
}

// SaveAccess saves authorization data to dynamodb
func (db *Storage) SaveAccess(data *osin.AccessData) (err error) {
	if data.AccessToken != "" {
		data.AccessTokenHash, _, err = DefaultTokenGenerator.HashSecret(data.AccessToken, nil)
		if err != nil {
			err = fmt.Errorf("guardian: error hashing token: %s", err.Error())
			return
		}
	}

	if data.RefreshToken != "" {
		data.RefreshTokenHash, _, err = DefaultTokenGenerator.HashSecret(data.RefreshToken, nil)
		if err != nil {
			err = fmt.Errorf("guardian: error hashing token: %s", err.Error())
			return
		}
	}

	params := &dynamodb.PutItemInput{
		TableName: aws.String(db.Config.AuthorizationsTable),
		Item:      accessDataToAttributeValues(data),
	}

	_, err = db.DB.PutItem(params)
	if err != nil {
		return
	}
	return
}

// LoadAccess retrieves access data keyed by access token
func (db *Storage) LoadAccess(token string) (data *osin.AccessData, err error) {
	data, err = db.loadToken(token)
	if err != nil {
		return
	}
	return
}

func (db *Storage) loadToken(token string) (data *osin.AccessData, err error) {
	tokenHash, _, err := DefaultTokenGenerator.HashSecret(token, nil)
	if err != nil {
		err = fmt.Errorf("guardian: error hashing token: %s", err.Error())
		return
	}

	params := &dynamodb.GetItemInput{
		TableName: aws.String(db.Config.AuthorizationsTable),
		Key: map[string]*dynamodb.AttributeValue{
			"token_hash": {S: aws.String(tokenHash)},
		},
		ConsistentRead: aws.Bool(true),
	}

	out, err := db.DB.GetItem(params)
	if err != nil {
		return
	}

	if len(out.Item) == 0 {
		return
	}

	data, err = attributeValuesToAccessData(out.Item, db)
	if err != nil {
		return
	}
	return
}

// RemoveAccess removes access data from dynamodb
func (db *Storage) RemoveAccess(token string) (err error) {
	err = db.removeToken(token)
	if err != nil {
		return
	}
	return
}

func (db *Storage) removeToken(token string) (err error) {
	tokenHash, _, err := DefaultTokenGenerator.HashSecret(token, nil)
	if err != nil {
		err = fmt.Errorf("guardian: error hashing token: %s", err.Error())
		return
	}

	params := &dynamodb.DeleteItemInput{
		TableName: aws.String(db.Config.AuthorizationsTable),
		Key: map[string]*dynamodb.AttributeValue{
			"token_hash": {S: aws.String(tokenHash)},
		},
	}

	_, err = db.DB.DeleteItem(params)
	if err != nil {
		return
	}

	return
}

// LoadRefresh retrieves accessdata for a refresh token from dynamodb
func (db *Storage) LoadRefresh(token string) (data *osin.AccessData, err error) {
	data, err = db.loadToken(token)
	if err != nil {
		return
	}
	return
}

// RemoveRefresh removes refresh token from dynamodb
func (db *Storage) RemoveRefresh(token string) (err error) {
	err = db.removeToken(token)
	if err != nil {
		return
	}
	return
}

// TokenCheck is the object returned from CheckToken call. includes info on
// access token and user associated with token
type TokenCheck struct {
	Token *oauth2.Token `json:"token"`
	User  *User         `json:"user"`
}

// CheckToken checks for validity of token and returns token information
func (db *Storage) CheckToken(token string) (tc *TokenCheck, ok bool) {
	data, err := db.LoadAccess(token)
	if err != nil {
		log.Printf("error loading access token: %s", err.Error())
		return
	}

	if !CompareVersion(data) {
		return
	}

	user, isUser := data.UserData.(*User)
	if !isUser && data.UserData != nil {
		log.Printf("error checking token: user data is not of type *guardian.User")
		return
	}

	tc = &TokenCheck{
		Token: &oauth2.Token{
			TokenType: "Bearer",
			Expiry:    data.ExpireAt(),
		},
		User: user,
	}
	ok = true

	if data.RefreshTokenHash != "" {
		tc.Token.RefreshToken = token
	} else if data.AccessTokenHash != "" {
		tc.Token.AccessToken = token
	}
	return
}
