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"
)

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

// 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(secret *Secret) error {
	return m.patch(secret, true)
}

func (m *Manager) patch(secret *Secret, updateAuditTable bool) 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 not found. Please use 'POST' to create a secret", secret.Name)
	}
	if secret.Class == ClassUnknown && oldSecret.Class != ClassUnknown {
		secret.Class = oldSecret.Class
	}

	if len(secret.Plaintext) != 0 {
		return m.put(secret, updateAuditTable)
	}

	// update the do_not_broadcast flag for the secret as per user input.
	// Preserve rest of the fields from current secret stored in DB.
	oldSecret.DoNotBroadcast = secret.DoNotBroadcast
	oldSecret.Class = secret.Class
	secret = oldSecret
	secret.Plaintext = nil

	updateInput := m.inputForUpdate(secret, m.Config.TableName)
	err = m.updateItemWithMetrics(updateInput)
	if err != nil {
		return err
	}

	if updateAuditTable {
		updateInput = m.inputForAuditUpdate(secret, m.AuditTableName())
		err = m.updateItemWithMetrics(updateInput)
		if err != nil {
			return err
		}
	}

	return nil
}

// 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)))},
		},
		ReturnConsumedCapacity: aws.String("INDEXES"),
		TableName:              aws.String(m.Config.TableName),
	}
}

func (m *Manager) inputForUpdate(secret *Secret, tableName string) *dynamodb.UpdateItemInput {
	return &dynamodb.UpdateItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			"name": &dynamodb.AttributeValue{S: aws.String(secret.Name)},
		},
		TableName:              aws.String(tableName),
		ReturnConsumedCapacity: aws.String("INDEXES"),
		UpdateExpression:       aws.String("SET #CLASS = :CLASS, #DNB = :DNB"),
		ExpressionAttributeNames: map[string]*string{
			"#CLASS": aws.String("class"),
			"#DNB":   aws.String("do_not_broadcast"),
		},
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":DNB":   &dynamodb.AttributeValue{BOOL: aws.Bool(secret.DoNotBroadcast)},
			":CLASS": &dynamodb.AttributeValue{N: aws.String(strconv.Itoa(int(secret.Class)))},
		},
	}

}

func (m *Manager) inputForAuditUpdate(secret *Secret, tableName string) *dynamodb.UpdateItemInput {
	return &dynamodb.UpdateItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			"name":       &dynamodb.AttributeValue{S: aws.String(secret.Name)},
			"updated_at": &dynamodb.AttributeValue{N: aws.String(strconv.FormatInt(secret.UpdatedAt, 10))},
		},
		TableName:              aws.String(tableName),
		ReturnConsumedCapacity: aws.String("INDEXES"),
		UpdateExpression:       aws.String("SET #CLASS = :CLASS, #DNB = :DNB"),
		ExpressionAttributeNames: map[string]*string{
			"#CLASS": aws.String("class"),
			"#DNB":   aws.String("do_not_broadcast"),
		},
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":DNB":   &dynamodb.AttributeValue{BOOL: aws.Bool(secret.DoNotBroadcast)},
			":CLASS": &dynamodb.AttributeValue{N: aws.String(strconv.Itoa(int(secret.Class)))},
		},
	}

}

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"),
	}
}
