package manager

import (
	"fmt"
	"strconv"

	"code.justin.tv/systems/sandstorm/inventory/heartbeat"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

// Get will fetch and decrypt a secret, returning a pointer to a
// Secret struct.
func (m *Manager) Get(secretName string) (*Secret, error) {

	var secret *Secret
	err := m.statter.WithMeasuredResult("secret.get", nil, func() (err error) {

		if secret = m.cache.Get(secretName); secret != nil {
			return nil
		}
		secret, err = m.get(secretName)
		if err != nil {
			return parseGetError(secretName, err)
		}

		err = m.Decrypt(secret)
		if err != nil {
			return fmt.Errorf("Failed to decrypt secret %s. Error was: %s ", secretName, err.Error())
		}

		m.cache.Set(secretName, secret)
		if secret != nil && m.inventoryClient != nil {
			// Report the heartbeat for the secret.
			secretToReport := &heartbeat.Secret{
				Name:      secret.Name,
				UpdatedAt: secret.UpdatedAt,
			}
			m.inventoryClient.UpdateHeartbeat(secretToReport)
		}
		return nil
	})

	return secret, err
}

// 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) {

	var secret *Secret
	err := m.statter.WithMeasuredResult("secret.get", nil, func() (err error) {
		secret, err = m.getVersion(secretName, version)
		if err != nil {
			return err
		}
		err = m.Decrypt(secret)
		return err
	})

	return secret, err
}

// 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) {

	var vp *VersionsPage
	err := m.statter.WithMeasuredResult("secret.get.versions.encrypted", nil, func() (err error) {
		vp, err = m.getVersions(secretName, limit, offsetKey)
		return
	})
	return vp, err
}

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

type crossEnv struct {
}

// CrossEnvironmentSecretsSet returns the set of all CrossEnvironment secrets
func (m *Manager) CrossEnvironmentSecretsSet(secretNames []string) (crossEnvSecrets map[string]struct{}, err error) {
	if len(secretNames) == 0 {
		return
	}

	crossEnvSecrets = map[string]struct{}{}

	keysAndAttributes := dynamodb.KeysAndAttributes{}
	keysAndAttributes.SetProjectionExpression("#N,cross_env")
	keysAndAttributes.SetExpressionAttributeNames(
		map[string]*string{"#N": aws.String("name")},
	)

	keys := []map[string]*dynamodb.AttributeValue{}
	for _, secretName := range secretNames {
		v := dynamodb.AttributeValue{}
		v.SetS(secretName)
		key := map[string]*dynamodb.AttributeValue{"name": &v}
		keys = append(keys, key)
	}
	keysAndAttributes.SetKeys(keys)
	requestItems := map[string]*dynamodb.KeysAndAttributes{
		m.TableName(): &keysAndAttributes,
	}
	for len(requestItems) > 0 {
		var batchGetItemOutput *dynamodb.BatchGetItemOutput

		batchGetItemInput := dynamodb.BatchGetItemInput{}
		batchGetItemInput.SetRequestItems(requestItems)

		batchGetItemOutput, err = m.dynamoDB().BatchGetItem(&batchGetItemInput)
		if err != nil {
			return
		}

		items, ok := batchGetItemOutput.Responses[m.TableName()]
		if !ok {
			return
		}

		for _, item := range items {
			s := dynamoSecret{}
			err = dynamodbattribute.UnmarshalMap(item, &s)
			if err != nil {
				return
			}

			if s.CrossEnv {
				crossEnvSecrets[s.Name] = struct{}{}
			}
		}

		// set requestItems to UnprocessedKeys
		requestItems = batchGetItemOutput.UnprocessedKeys
	}
	return
}

func (m *Manager) get(name string) (*Secret, error) {
	queryInput := m.inputForGet(name)
	queryOutput, err := m.dynamoDB().Query(queryInput)
	if err != nil {
		return nil, err
	}
	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, ErrNegativeLimit
	}
	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 err == ErrSecretTombstoned {
				continue
			}
			return nil, err
		}
		versions.Secrets = append(versions.Secrets, secret)
	}

	if queryOutput.LastEvaluatedKey != nil && len(queryOutput.LastEvaluatedKey) > 0 {
		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
}
