package manager

import (
	"context"
	"strconv"
	"sync"

	"code.justin.tv/systems/sandstorm/closer"
	"code.justin.tv/systems/sandstorm/internal/envelope/envelopeiface"
	"code.justin.tv/systems/sandstorm/internal/stat"
	"code.justin.tv/systems/sandstorm/internal/stat/statiface"
	"code.justin.tv/systems/sandstorm/inventory/heartbeat"
	"code.justin.tv/systems/sandstorm/inventory/heartbeat/heartbeatiface"
	"code.justin.tv/systems/sandstorm/logging"
	"code.justin.tv/systems/sandstorm/queue"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
	"github.com/aws/aws-sdk-go/service/kms/kmsiface"
	"github.com/aws/aws-sdk-go/service/sns"
	"github.com/aws/aws-sdk-go/service/sqs"
)

// API describes the manager interface
type API interface {
	AuditTableName() string
	CheckKMS() error
	CheckTable() error
	Copy(source, destination string) error
	Decrypt(secret *Secret) error
	Delete(secretName string) error
	DescribeQueue() (*queue.Status, error)
	Exist(secretName string) (bool, error)
	CrossEnvironmentSecretsSet(secretNames []string) (crossEnvSecrets map[string]struct{}, err error)
	FlushHeartbeat(ctx context.Context)
	Get(secretName string) (*Secret, error)
	GetEncrypted(secretName string) (*Secret, error)
	GetVersion(secretName string, version int64) (*Secret, error)
	GetVersionsEncrypted(secretName string, limit int64, offsetKey int64) (*VersionsPage, error)
	List() ([]*Secret, error)
	ListenForUpdates() error
	ListNamespace(namespace string) ([]*Secret, error)
	ListNamespaces() ([]string, error)
	NamespaceTableName() string
	Patch(patchInput *PatchInput) error
	Post(secret *Secret) error
	Put(secret *Secret) error
	Revert(name string, version int64) error
	RegisterSecretUpdateCallback(secretName string, callback func(input SecretChangeset))
	ClearSecretCallbacks()
	Seal(secret *Secret) error
	StopListeningForUpdates() error
	TableName() string
	CleanUp() error

	DeleteSecret(input *DeleteSecretInput) (output *DeleteSecretOutput, err error)
	RevertSecret(input *RevertSecretInput) (output *RevertSecretOutput, err error)
	CopySecret(input *CopySecretInput) (output *CopySecretOutput, err error)
}

// InputValidationError represents error if input was invalid
type InputValidationError struct {
	err string
}

func (e *InputValidationError) Error() string {
	return e.err
}

const version = "1.0"

const (
	dynamoDBKeyTombstone       = "tombstone"
	dynamoDBKeyActionUser      = "action_user"
	dynamoDBKeyServiceName     = "service_name"
	dynamoDBKeyPreviousVersion = "previous_version"
)

const (
	attempted = "attempted"
	success   = "success"
	failure   = "failure"
	cached    = "cached"
	notFound  = "not_found"
)

const awsRegionUsWest2 = "us-west-2"

// Manager is the struct that a caller interacts with for managing
// secrets.
type Manager struct {
	DynamoDB                dynamodbiface.DynamoDBAPI
	Config                  Config
	Logger                  logging.Logger
	statter                 statiface.API
	inventoryClient         heartbeatiface.API
	queue                   queue.Queuer
	cache                   *cache
	stopListeningForUpdates chan struct{}

	secretCallbacksLock sync.Mutex
	secretCallbacks     map[string][]func(SecretChangeset)

	clientLoader *clientLoader
	closer       *closer.Closer
	enveloper    envelopeiface.Enveloper
}

const (
	unknownServiceName     = "unknown-service"
	defaultQueueNamePrefix = "sandstorm"
)

// New creates a Manager from config. Merges configuration with
// DefaultConfig()
func New(config Config) *Manager {
	mgr, err := NewWithError(config)
	if err != nil {
		config.Logger.Errorf("error configuring Manager: %s", err.Error())
	}
	return mgr
}

// NewWithError creates a Manager from config. Merges configuration with
// DefaultConfig()
func NewWithError(config Config) (*Manager, error) {
	var err error
	if err = config.fillDefaults(getDefaultConfigByEnvironment(config.Environment)); err != nil {
		return nil, err
	}

	sess := config.awsConfigurer().STSSession(config.awsCreds())

	clientLoader := &clientLoader{
		Config: config,
	}

	closer := closer.New()

	cwStatter := &stat.Client{
		Namespace: "Sandstorm",
		Logger:    config.Logger,
		CloudWatch: cloudwatch.New(config.awsConfigurer().STSSession(
			config.awsCreds(), config.stack().CloudwatchRoleARN())),
		Closer: closer,
	}

	mgr := &Manager{
		Config:                  config,
		Logger:                  config.Logger,
		statter:                 cwStatter,
		cache:                   &cache{},
		queue:                   queue.New(sqs.New(sess), sns.New(sess), config.Queue, config.Logger),
		stopListeningForUpdates: make(chan struct{}),
		secretCallbacks:         make(map[string][]func(input SecretChangeset)),
		clientLoader:            clientLoader,
		closer:                  closer,
		enveloper: &Envelope{
			clientLoader: clientLoader,
			kmsKey:       config.stack().KMSPrimaryKey(),
			Logger:       config.Logger,
			statter:      cwStatter,
		},
	}

	mgr.inventoryClient = heartbeat.New(
		config.awsConfigurer().AWSCredentials(config.awsCreds(), config.stack().InventoryRoleARN()),
		heartbeat.Config{
			Interval:                       config.InventoryInterval,
			Service:                        config.ServiceName,
			Host:                           config.Host,
			InstanceID:                     config.InstanceID,
			ManagerVersion:                 version,
			AWSRegion:                      config.stack().AWSRegion(),
			PutHeartbeatLambdaFunctionName: config.stack().PutHeartbeatLambdaFunctionName(),
		},
		config.Logger,
	)
	if err != nil {
		return nil, err
	}
	go mgr.inventoryClient.Start()

	return mgr, nil
}

func (mgr *Manager) newCache() {
	mgr.cache = &cache{
		secretMap: make(map[string]*Secret),
	}
}

// ListenForUpdates manager listens on the sandstorm queue for secret updates.
// It also configures the manager to cache secret plaintext in memory. When an
// update event is received for a secret, that secret is expunged from the in
// memory cache, so the next Get call will fetch that secret from DynamoDB.
func (mgr *Manager) ListenForUpdates() (err error) {
	mgr.newCache()
	err = mgr.queue.Setup()
	if err != nil {
		return
	}

	go func() {
		for {
			select {
			case <-mgr.stopListeningForUpdates:
				return
			default:
				err := mgr.listenForSecretUpdates()
				if err != nil {
					mgr.Logger.Errorf("Failed to listen for secret update. err: %s", err.Error())
				}
			}
		}
	}()
	return
}

func (mgr *Manager) listenForSecretUpdates() (err error) {
	errChan := make(chan error)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		queueSecret, err := mgr.queue.PollForSecret(ctx)
		if err != nil {
			errChan <- err
			return
		}
		if queueSecret != nil {
			secretName := queueSecret.Name.S
			mgr.cache.Delete(secretName)
			// This should always be int, if for some reason conversion fails,
			// send the error over to chan and continue with updatedAt = 0
			var updatedAt int64
			if queueSecret.UpdatedAt.S != "" {
				updatedAt, err = strconv.ParseInt(queueSecret.UpdatedAt.S, 10, 64)
				if err != nil {
					errChan <- err
				}
			}
			mgr.triggerSecretUpdateCallbacks(secretName, updatedAt)
		}
		errChan <- nil
		return
	}()

	select {
	case <-mgr.stopListeningForUpdates:
		err = <-errChan
	case err = <-errChan:
	}
	return
}

// StopListeningForUpdates manager stops listening to secret update queue and
// deletes the queue
func (mgr *Manager) StopListeningForUpdates() (err error) {
	mgr.cache = &cache{}
	close(mgr.stopListeningForUpdates)
	return mgr.queue.Delete()
}

// CleanUp cleans exits any go routines spawned
func (mgr *Manager) CleanUp() (err error) {
	if err := mgr.closer.Close(); err != nil {
		mgr.Logger.Errorf("error closing manager: %s", err.Error())
	}

	inventoryStopErr := mgr.inventoryClient.Stop()
	if inventoryStopErr != nil {
		mgr.Logger.Debugf("error stopping inventory client: %s", inventoryStopErr.Error())
	}
	return
}

// cleanUpSecretColumns cleans up a secret row
// encrypted_data_keys: See SECDEV-1352
func (mgr *Manager) cleanUpSecretColumns(ctx context.Context, secretName string) error {
	_, err := mgr.dynamoDB().UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			"name": {S: aws.String(secretName)},
		},
		TableName:        aws.String(mgr.Config.stack().SecretsTableName()),
		UpdateExpression: aws.String("REMOVE encrypted_data_keys"),
	})
	return err
}

// FlushHeartbeat sends a signal to immediately report heartbeat
func (mgr *Manager) FlushHeartbeat(ctx context.Context) {
	mgr.inventoryClient.FlushHeartbeat(ctx)
}

// DescribeQueue returns information about the queue being used by this manager
func (mgr *Manager) DescribeQueue() (*queue.Status, error) {
	return mgr.queue.Status()
}

func (mgr *Manager) dynamoDB() dynamodbiface.DynamoDBAPI {
	return mgr.clientLoader.DynamoDB(mgr.Config.stack().AWSRegion())
}

func (mgr *Manager) kms() kmsiface.KMSAPI {
	return mgr.clientLoader.KMS(mgr.Config.stack().AWSRegion())
}
