package manager

import (
	"fmt"
	"strconv"

	"code.justin.tv/common/ddbmetrics"
	"code.justin.tv/systems/sandstorm/errors"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

// Get will fetch and decrypt a secret, returning a pointer to a
// Secret struct.
func (m *Manager) Get(secretName string) (*Secret, error) {
	secret, err := m.get(secretName)
	if err != nil {
		return nil, err
	}
	err = m.Decrypt(secret)
	if err != nil {
		return nil, err
	}
	return secret, nil
}

// GetVersion will fetch and decrypt a secret, returning a pointer to a
// previous version of a Secret struct.
func (m *Manager) GetVersion(secretName string, version int64) (*Secret, error) {
	secret, err := m.getVersion(secretName, version)
	if err != nil {
		return nil, err
	}
	err = m.Decrypt(secret)
	if err != nil {
		return nil, err
	}
	return secret, nil
}

// VersionsPage represents a page of versions of a secret
type VersionsPage struct {
	Secrets []*Secret

	// Value of Limit that was used when retrieving this page of Versions.
	Limit int64

	// Value of OffsetKey to use for the next page of results. Will be 0 if there
	// are no more results.
	NextKey int64

	// Value of OffsetKey used when retrieving VersionsPage.Secrets
	OffsetKey int64
}

// GetVersionsEncrypted returns all versions of a secret with pagination keys.
// Pass the same instance of offset to paginate results.
func (m *Manager) GetVersionsEncrypted(secretName string, limit int64, offsetKey int64) (*VersionsPage, error) {
	return m.getVersions(secretName, limit, offsetKey)
}

// Exist check the given secret exist in the system.
// This also performs the check to make sure that the secret can
// be unmarshalled.
func (m *Manager) Exist(secretName string) (bool, error) {
	secret, err := m.get(secretName)
	return secret != nil, err
}

// GetEncrypted returns a pointer to a Secret, but doees not actually
// perform the decryption. This is used by the JSON API.
func (m *Manager) GetEncrypted(secretName string) (*Secret, error) {
	secret, err := m.get(secretName)
	return secret, err
}

func (m *Manager) get(name string) (*Secret, error) {
	queryInput := m.inputForGet(name)
	queryOutput, err := m.DynamoDB.Query(queryInput)
	if err != nil {
		return nil, err
	}
	m.metrics.Report(ddbmetrics.Read, queryOutput.ConsumedCapacity)
	if *queryOutput.Count == 0 {
		return nil, nil
	}
	secret, err := unmarshalSecret(queryOutput.Items[0])
	if err != nil {
		return nil, err
	}

	return secret, nil
}

func (m *Manager) getVersion(name string, version int64) (*Secret, error) {
	queryInput := m.inputForGetVersion(name, version)
	queryOutput, err := m.DynamoDB.Query(queryInput)
	if err != nil {
		return nil, err
	}

	if *queryOutput.Count < 1 {
		return nil, nil
	}

	secret, err := unmarshalSecret(queryOutput.Items[0])
	if err != nil {
		return nil, err
	}

	return secret, nil
}

func (m *Manager) getVersions(name string, limit int64, offsetKey int64) (*VersionsPage, error) {
	if limit < 1 {
		return nil, fmt.Errorf("limit must be greater than 0")
	}
	queryOutput, err := m.DynamoDB.Query(m.inputForGetVersionsEncrypted(name, limit, offsetKey))
	if err != nil {
		return nil, err
	}
	versions := VersionsPage{
		Secrets:   make([]*Secret, 0, *queryOutput.Count),
		Limit:     limit,
		OffsetKey: 0,
	}
	for _, item := range queryOutput.Items {
		secret, err := unmarshalSecret(item)
		if err != nil {
			if errors.IsSecretTombstoned(err) {
				continue
			}
			return nil, err
		}
		versions.Secrets = append(versions.Secrets, secret)
	}

	if queryOutput.LastEvaluatedKey != nil {
		next, err := strconv.Atoi(*queryOutput.LastEvaluatedKey["updated_at"].N)
		if err != nil {
			return nil, err
		}
		versions.NextKey = int64(next)
	}

	versions.Limit = limit
	versions.OffsetKey = offsetKey
	return &versions, nil
}

func (m *Manager) inputBase() *dynamodb.QueryInput {
	return &dynamodb.QueryInput{
		ConsistentRead: aws.Bool(true),
		ExpressionAttributeNames: map[string]*string{
			"#N": aws.String("name"),
		},
		ReturnConsumedCapacity: aws.String("INDEXES"),
	}
}

func (m *Manager) inputForGet(name string) *dynamodb.QueryInput {
	q := m.inputBase()
	q.ExpressionAttributeValues = map[string]*dynamodb.AttributeValue{
		":name": {
			S: aws.String(name),
		},
	}
	q.KeyConditionExpression = aws.String("#N = :name")
	q.TableName = aws.String(m.TableName())
	return q
}

func (m *Manager) inputForGetVersionsEncrypted(name string, limit int64, offsetKey int64) *dynamodb.QueryInput {
	q := m.inputForGet(name)
	if offsetKey > 0 {
		q.ExclusiveStartKey = map[string]*dynamodb.AttributeValue{
			"name":       {S: aws.String(name)},
			"updated_at": {N: aws.String(fmt.Sprintf("%d", offsetKey))},
		}
	}
	q.TableName = aws.String(m.AuditTableName())
	q.Limit = aws.Int64(limit)
	q.ScanIndexForward = aws.Bool(false)
	return q
}

func (m *Manager) inputForGetVersion(name string, version int64) *dynamodb.QueryInput {
	q := m.inputBase()
	q.ExpressionAttributeValues = map[string]*dynamodb.AttributeValue{
		":name":       {S: aws.String(name)},
		":updated_at": {N: aws.String(fmt.Sprintf("%d", version))},
	}
	q.KeyConditionExpression = aws.String("#N = :name AND updated_at = :updated_at")
	q.TableName = aws.String(m.AuditTableName())
	return q
}
