package manager

import (
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
	"strings"

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

// Errors
var (
	// ErrorSecretVersionDoesNotExist will be raised user is trying to operate with
	// a version that does not exist. See Revert
	ErrorSecretVersionDoesNotExist = errors.New("manager: secret version does not exist")

	ErrAutogenerateAndPlaintextBothSent = errors.New("both autogenerate and plaintext cannot be sent")
	ErrSecretDoesNotExist               = errors.New("secret does not exist")
)

// PatchInput in the input to Patch. nil pointers mean no change.
type PatchInput struct {
	// primary key - should be set in the usually
	Name string

	// patches
	Plaintext      []byte       `json:"plaintext,omitempty"`
	DoNotBroadcast *bool        `json:"do_not_broadcast,omitempty" dynamodbav:"do_not_broadcast"`
	Class          *SecretClass `json:"class,omitempty" dynamodbav:"class"`
	CrossEnv       *bool        `json:"cross_env,omitempty" dynamodbav:"cross_env"`

	// Autogenerate fills Plaintext with a randomly server-side generated value
	Autogenerate *FillPlaintextRequest `json:"autogenerate"`

	// these should be set if Plaintext was updated
	UpdatedAt *int64  `dynamodbav:"updated_at"`
	Key       *string `dynamodbav:"key"`
	Value     *string `dynamodbav:"value"`
	KeyARN    *string `dynamodbav:"key_arn"`

	// true if validated at least once
	validated bool
}

// Validate parameters
func (p *PatchInput) Validate() (err error) {
	if p.validated {
		return
	}

	if p.Autogenerate != nil && p.Plaintext != nil {
		return ErrAutogenerateAndPlaintextBothSent
	}

	if p.Autogenerate != nil {
		err = p.Autogenerate.Validate()
		if err != nil {
			return
		}
	}

	if (p.UpdatedAt != nil) || (p.Key != nil) || (p.Value != nil) || (p.KeyARN != nil) {
		return errors.New("attributes UpdatedAt, Key, Value, KeyARN should not be sent to be patched")
	}

	p.validated = true
	return
}

// Patch will take a secrest object and
// -Insert it into DynamoDB if secrets have changed. OR
// - Update the class and DoNotBroadcast of the existing item
// This+POST will eventually replace PUT method.
func (m *Manager) Patch(patchInput *PatchInput) error {
	return m.patch(patchInput, true)
}

func (m *Manager) patch(patchInput *PatchInput, updateAuditTable bool) (err error) {
	err = patchInput.Validate()
	if err != nil {
		return
	}

	var oldSecret *Secret
	oldSecret, err = m.GetEncrypted(patchInput.Name)

	if err != nil {
		return err
	}
	if oldSecret == nil {
		//It has verified that the secert exist, If we get here its most likely a dev error
		err = ErrSecretDoesNotExist
		return
	}

	var updatedAt int64
	// if Plaintext is patched, we will want to Seal and use the new UpdatedAt for the audit table
	// else, use the old UpdatedAt for the audit table
	if patchInput.Autogenerate != nil || len(patchInput.Plaintext) > 0 {
		err = m.sealPatchInputPlaintext(patchInput)
		if err != nil {
			return
		}
		updatedAt = *patchInput.UpdatedAt
	} else {
		updatedAt = oldSecret.UpdatedAt
	}

	updateInput, err := m.inputForSecretUpdate(patchInput, m.Config.TableName)
	if err != nil {
		return
	}
	if updateInput == nil {
		// no changes
		return
	}

	err = m.updateItemWithMetrics(updateInput)
	if err != nil {
		return err
	}

	if updateAuditTable {
		updateInput, err = m.inputForAuditUpdate(patchInput, m.AuditTableName(), updatedAt)
		if err != nil {
			return
		}
		if updateInput == nil {
			// no changes
			return
		}
		err = m.updateItemWithMetrics(updateInput)
		if err != nil {
			return err
		}
	}

	return nil
}

func (m *Manager) sealPatchInputPlaintext(patchInput *PatchInput) (err error) {
	secret := &Secret{
		Name:      patchInput.Name,
		Plaintext: patchInput.Plaintext,
	}

	if patchInput.Autogenerate != nil {
		err = secret.FillPlaintext(patchInput.Autogenerate)
		if err != nil {
			return
		}
	}

	err = m.Seal(secret)
	if err != nil {
		return
	}

	ddbSecret := secret.asDynamoSecret()
	patchInput.UpdatedAt = &ddbSecret.UpdatedAt
	patchInput.Key = &ddbSecret.Key
	patchInput.Value = &ddbSecret.Value
	patchInput.KeyARN = &ddbSecret.KeyARN
	return
}

// Post will take a plaintext secret, encrypt and then insert a new record in DynamoDB
func (m *Manager) Post(secret *Secret) error {
	oldSecret, err := m.GetEncrypted(secret.Name)
	if err != nil {
		return err
	}
	if oldSecret != nil {
		//It has verified that the secert exist, If we get here its most likely a dev error
		return fmt.Errorf("Secret %s already found. Please use 'PATCH' to update a secret", secret.Name)
	}
	return m.put(secret, true)
}

//Put @Deprecated
// Put will take a plaintext secret, encrypt and then insert a new record in DynamoDB
func (m *Manager) Put(secret *Secret) error {
	return m.put(secret, true)
}

func (m *Manager) put(secret *Secret, updateAuditTable bool) error {
	// Create the newest version of the secret and persist it
	if err := m.Seal(secret); err != nil {
		return err
	}
	return m.putItem(secret, updateAuditTable)
}

func (m *Manager) putItem(secret *Secret, updateAuditTable bool) error {

	putInput := m.inputForPut(secret)
	err := m.putItemWithMetrics(putInput)
	if err != nil {
		return err
	}

	//@TODO: @bach/@job I think we should use lambda to update the audit/namespace table
	// to control what goes go in those table and when
	if updateAuditTable {
		auditPutInput := putInput
		auditPutInput.TableName = aws.String(m.AuditTableName())
		err = m.putItemWithMetrics(auditPutInput)
		if err != nil {
			return err
		}
	}

	//@TODO @bach/@jon should we get rid of namespace table ?
	nsInput := m.inputForNamespacePut(SecretNameToNamespace(secret.Name))
	err = m.putItemWithMetrics(nsInput)
	if err != nil {
		return err
	}

	return nil
}

func (m *Manager) putItemWithMetrics(input *dynamodb.PutItemInput) error {
	putOutput, err := m.DynamoDB.PutItem(input)
	if err != nil {
		return fmt.Errorf("Error writing to %s: %s", *input.TableName, err)
	}
	m.metrics.Report(ddbmetrics.Write, putOutput.ConsumedCapacity)
	return nil
}

func (m *Manager) updateItemWithMetrics(input *dynamodb.UpdateItemInput) error {
	putOutput, err := m.DynamoDB.UpdateItem(input)
	if err != nil {
		return fmt.Errorf("Error writing to %s: %s", *input.TableName, err)
	}
	m.metrics.Report(ddbmetrics.Write, putOutput.ConsumedCapacity)
	return nil
}

// Revert will revert a key to a previous value
// This will *not* append to the audit table
func (m *Manager) Revert(name string, version int64) error {
	secret, err := m.getVersion(name, version)
	if err != nil {
		return err
	}
	if secret == nil {
		return ErrorSecretVersionDoesNotExist
	}
	return m.putItem(secret, false)
}

// SecretNameToNamespace strips the namespace from a secret's Name
func SecretNameToNamespace(secretName string) string {
	splitSecret := strings.SplitN(secretName, "/", 2)
	return splitSecret[0]
}

func (m *Manager) inputForPut(secret *Secret) *dynamodb.PutItemInput {

	if secret.Class == ClassUnknown {
		secret.Class = ClassTopSecret
	}

	return &dynamodb.PutItemInput{
		Item: map[string]*dynamodb.AttributeValue{
			"name":             &dynamodb.AttributeValue{S: aws.String(secret.Name)},
			"namespace":        &dynamodb.AttributeValue{S: aws.String(SecretNameToNamespace(secret.Name))},
			"updated_at":       &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(secret.UpdatedAt, 10))},
			"key":              &dynamodb.AttributeValue{S: aws.String(base64.StdEncoding.EncodeToString(secret.key))},
			"value":            &dynamodb.AttributeValue{S: aws.String(base64.StdEncoding.EncodeToString(secret.ciphertext))},
			"do_not_broadcast": &dynamodb.AttributeValue{BOOL: aws.Bool(secret.DoNotBroadcast)},
			"key_arn":          &dynamodb.AttributeValue{S: aws.String(secret.KeyARN)},
			"class":            &dynamodb.AttributeValue{N: aws.String(strconv.Itoa(int(secret.Class)))},
			"cross_env":        &dynamodb.AttributeValue{BOOL: aws.Bool(secret.CrossEnv)},
		},
		ReturnConsumedCapacity: aws.String("INDEXES"),
		TableName:              aws.String(m.Config.TableName),
	}
}

func (m *Manager) inputForSecretUpdate(patchInput *PatchInput, tableName string) (*dynamodb.UpdateItemInput, error) {
	key := map[string]*dynamodb.AttributeValue{
		"name": &dynamodb.AttributeValue{S: aws.String(patchInput.Name)},
	}
	return m.inputForUpdate(patchInput, key, tableName)
}

func (m *Manager) inputForAuditUpdate(patchInput *PatchInput, tableName string, updatedAt int64) (*dynamodb.UpdateItemInput, error) {
	// no need to update updatedAt for audit table since it's the range key
	patchInputCopy := *patchInput
	patchInputCopy.UpdatedAt = nil

	key := map[string]*dynamodb.AttributeValue{
		"name":       &dynamodb.AttributeValue{S: aws.String(patchInput.Name)},
		"updated_at": &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(updatedAt, 10))},
	}
	return m.inputForUpdate(&patchInputCopy, key, tableName)
}

// build a secret update request
func (m *Manager) inputForUpdate(
	patchInput *PatchInput,
	key map[string]*dynamodb.AttributeValue,
	tableName string,
) (
	// input to dynamodb.UpdateItem. If no changes are provided, this will be nil.
	updateInput *dynamodb.UpdateItemInput,
	err error,
) {
	updateExpressionClauses := []string{}
	expressionAttributeNames := map[string]*string{}
	expressionAttributeValues := map[string]*dynamodb.AttributeValue{}

	updates, err := dynamodbattribute.MarshalMap(patchInput)
	if err != nil {
		return
	}

	for key, attributeValue := range updates {
		if attributeValue.NULL != nil && *attributeValue.NULL {
			continue
		}

		attrExpName := "#" + key
		attrExpValue := ":" + key
		updateExpressionClauses = append(updateExpressionClauses, attrExpName+" = "+attrExpValue)
		expressionAttributeNames[attrExpName] = aws.String(key)
		expressionAttributeValues[attrExpValue] = attributeValue
	}

	if len(updateExpressionClauses) == 0 {
		return
	}

	updateExpression := "SET " + strings.Join(updateExpressionClauses, ", ")

	updateInput = &dynamodb.UpdateItemInput{
		Key:                       key,
		TableName:                 aws.String(tableName),
		ReturnConsumedCapacity:    aws.String("INDEXES"),
		UpdateExpression:          aws.String(updateExpression),
		ExpressionAttributeNames:  expressionAttributeNames,
		ExpressionAttributeValues: expressionAttributeValues,
	}
	return
}

func (m *Manager) inputForNamespacePut(namespace string) *dynamodb.PutItemInput {
	return &dynamodb.PutItemInput{
		Item: map[string]*dynamodb.AttributeValue{
			"namespace": {S: aws.String(namespace)},
		},
		TableName:              aws.String(m.NamespaceTableName()),
		ReturnConsumedCapacity: aws.String("INDEXES"),
	}
}
