// Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package clients implements the secrets.SecretDecrypter interface for AWS clients.
package clients

import (
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
	"github.com/aws/aws-sdk-go/aws"
	arnParser "github.com/aws/aws-sdk-go/aws/arn"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
	"github.com/aws/aws-sdk-go/service/ssm"
	"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
	"github.com/pkg/errors"
)

// ssmSeparator is used to check if ssm parameter names are fully qualified paths
const ssmSeparator = "/"

const splitAllStrings = -1

// region represents an AWS region.
type region string

// SSMDecrypter represents a SSM client that implements the secrets.SecretDecrypter interface.
type SSMDecrypter struct {
	ssmiface.SSMAPI

	// clients holds regional SSM clients.
	// The region "default" is always present and points to the same session as the user's default region.
	clients map[region]ssmiface.SSMAPI
}

// SecretsManagerDecrypter represents a SecretsManager client that implements the secrets.SecretDecrypter interface.
type SecretsManagerDecrypter struct {
	secretsmanageriface.SecretsManagerAPI

	// clients holds regional SecretsManager clients.
	// The region "default" is always present and points to the same session as the user's default region.
	clients map[region]secretsmanageriface.SecretsManagerAPI
}

// DecryptSecret returns the decrypted parameter value from SSM.
//
// If the parameter is an ARN then the decrypted value is retrieved from the appropriate region.
// If the parameter is just the name of the parameter then the decrypted value is retrieved from the default region.
func (d *SSMDecrypter) DecryptSecret(arnOrName string) (string, error) {
	defer func() {
		// Reset the region of the client in case another SSM secret uses only the param name instead of full ARN.
		d.SSMAPI = d.getClient("default")
	}()

	// If the value is an ARN we need to retrieve the parameter name and update the region of the client.
	paramName := arnOrName
	if arnParser.IsARN(arnOrName) {
		parsedARN, err := arnParser.Parse(arnOrName)
		if err != nil {
			return "", errors.Wrapf(err, "failed to parse resource identifier %s due to %v", arnOrName, err)
		}
		paramName = parsedARN.Resource[len("parameter"):] // Resource is formatted as parameter/{paramName}.
		d.SSMAPI = d.getClient(region(parsedARN.Region))
	}

	val, err := d.GetParameter(&ssm.GetParameterInput{
		Name:           aws.String(paramName),
		WithDecryption: aws.Bool(true),
	})
	if err != nil {
		return "", errors.Wrapf(err, "failed to retrieve decrypted secret from %s due to %v", arnOrName, err)
	}
	return *val.Parameter.Value, nil
}

// getClient returns the SSM client for a given region.
// If there is no client available for that region, then creates and caches it.
func (d *SSMDecrypter) getClient(r region) ssmiface.SSMAPI {
	if c, ok := d.clients[r]; ok {
		return c
	}
	c := ssm.New(session.Must(session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region:                        aws.String(string(r)),
			CredentialsChainVerboseErrors: aws.Bool(true),
		},
	})))
	d.clients[r] = c
	return c
}

// DecryptSecret returns the decrypted secret value from Secrets Manager.
func (d *SecretsManagerDecrypter) DecryptSecret(arn string) (string, error) {
	parsedARN, err := arnParser.Parse(arn)
	if err != nil {
		return "", errors.Wrapf(err, "failed to parse ARN %s", arn)
	}
	d.SecretsManagerAPI = d.getClient(region(parsedARN.Region))
	val, err := d.GetSecretValue(&secretsmanager.GetSecretValueInput{
		SecretId: aws.String(arn),
	})
	if err != nil {
		return "", errors.Wrapf(err, "failed to retrieve decrypted secret from %s due to %v", arn, err)
	}
	return *val.SecretString, nil
}

func (d *SecretsManagerDecrypter) getClient(r region) secretsmanageriface.SecretsManagerAPI {
	if c, ok := d.clients[r]; ok {
		return c
	}
	c := secretsmanager.New(session.Must(session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region:                        aws.String(string(r)),
			CredentialsChainVerboseErrors: aws.Bool(true),
		},
	})))
	d.clients[r] = c
	return c
}

// NewSSMDecrypter returns a new SSMDecrypter using the ECS CLI's default region.
func NewSSMDecrypter() (*SSMDecrypter, error) {
	sess, err := getDefaultSession()
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create a new AWS session due to %v", err)
	}
	defaultClient := ssm.New(sess)

	clients := make(map[region]ssmiface.SSMAPI)
	clients["default"] = defaultClient
	clients[region(aws.StringValue(sess.Config.Region))] = defaultClient
	return &SSMDecrypter{
		defaultClient,
		clients,
	}, nil
}

// NewSecretsManagerDecrypter returns a new SecretsManagerDecrypter using the ECS CLI's default region.
func NewSecretsManagerDecrypter() (*SecretsManagerDecrypter, error) {
	sess, err := getDefaultSession()
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create a new AWS session due to %v", err)
	}

	defaultClient := secretsmanager.New(sess)

	clients := make(map[region]secretsmanageriface.SecretsManagerAPI)
	clients["default"] = defaultClient
	clients[region(aws.StringValue(sess.Config.Region))] = defaultClient
	return &SecretsManagerDecrypter{
		defaultClient,
		clients,
	}, nil
}

// getDefaultSession returns a session for AWS clients where the region is set to the ECS CLI's default region.
// See https://github.com/aws/amazon-ecs-cli/blob/master/README.md#order-of-resolution-for-region
func getDefaultSession() (*session.Session, error) {
	rdwr, err := config.NewReadWriter()
	if err != nil {
		return nil, err
	}
	cmdConf, err := config.NewCommandConfig(nil, rdwr)
	if err != nil {
		return nil, err
	}
	return cmdConf.Session, nil
}
