// Copyright 2015-2017 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 config

import (
	"fmt"
	"os"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/endpoints"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/urfave/cli"
)

const (
	configVersion               = "v1"
	composeProjectNamePrefixKey = "compose-project-name-prefix"
	composeServiceNamePrefixKey = "compose-service-name-prefix"
	cfnStackNamePrefixKey       = "cfn-stack-name-prefix"
	awsAccessKey                = "aws_access_key_id"
	awsSecretKey                = "aws_secret_access_key"
	awsSessionToken             = "aws_session_token"
	clusterKey                  = "cluster"
	clustersKey                 = "clusters"
	regionKey                   = "region"
	iniConfigVersion            = 0
	yamlConfigVersion           = 1
)

// LocalConfig is the top level struct representing the local ECS configuration
type LocalConfig struct {
	Version                  int // which format version was the config file that was read. 1 == yaml, 0 == old ini
	Cluster                  string
	AWSProfile               string
	Region                   string
	AWSAccessKey             string
	AWSSecretKey             string
	AWSSessionToken          string
	ComposeServiceNamePrefix string
	ComposeProjectNamePrefix string // Deprecated; remains for backwards compatibility
	CFNStackName             string
	CFNStackNamePrefix       string // Deprecated; remains for backwards compatibility
	DefaultLaunchType        string
}

// Profile is a simple struct for storing a single AWS profile config
type Profile struct {
	AWSAccessKey    string `yaml:"aws_access_key_id"`
	AWSSecretKey    string `yaml:"aws_secret_access_key"`
	AWSSessionToken string `yaml:"aws_session_token,omitempty"`
}

// Cluster is a simple struct for storing a single ECS cluster config
type Cluster struct {
	Cluster                  string `yaml:"cluster"`
	Region                   string `yaml:"region"`
	ComposeServiceNamePrefix string `yaml:"compose-service-name-prefix,omitempty"`
	CFNStackName             string `yaml:"cfn-stack-name,omitempty"`
	DefaultLaunchType        string `yaml:"default_launch_type"`
}

// ClusterConfig is the top level struct representing the cluster config file
type ClusterConfig struct {
	Version  string
	Default  string             `yaml:"default"`
	Clusters map[string]Cluster `yaml:"clusters"`
}

// ProfileConfig is the top level struct representing the Credentials file
type ProfileConfig struct {
	Version  string
	Default  string             `yaml:"default"`
	Profiles map[string]Profile `yaml:"ecs_profiles"`
}

// NewLocalConfig creates a new instance of LocalConfig from the cluster name.
func NewLocalConfig(cluster string) *LocalConfig {
	return &LocalConfig{Cluster: cluster}
}

// ToAWSSession creates a new Session object from the LocalConfig object.
// Region: Order of resolution
//  1) ECS CLI Flags
//   a) Region Flag --region
//   b) Cluster Config Flag (--cluster-config)
//  2) ECS Config - attempts to fetch the region from the default ECS Profile
//  3) Environment Variable - attempts to fetch the region from environment variables:
//    a) AWS_REGION (OR)
//    b) AWS_DEFAULT_REGION
//  4) AWS Profile - attempts to use region from AWS profile name
//    a) --aws-profile flag
//    b) AWS_PROFILE environment variable
//    c) AWS_DEFAULT_PROFILE environment variable (defaults to 'default')
//
// Credentials: Order of resolution
//  1) ECS CLI Profile Flags
//   a) ECS Profile (--ecs-profile)
//   b) AWS Profile (--aws-profile)
//  2) Environment Variables - attempts to fetch the credentials from environment variables:
//   a) ECS_PROFILE
//   b) AWS_PROFILE
//   c) AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, Optional: AWS_SESSION_TOKEN
//  3) ECS Config - attempts to fetch the credentials from the default ECS Profile
//  4) Default AWS Profile - attempts to use credentials (aws_access_key_id, aws_secret_access_key) or assume_role (role_arn, source_profile) from AWS profile name
//    a) AWS_DEFAULT_PROFILE environment variable (defaults to 'default')
//  5) EC2 Instance role
func (cfg *LocalConfig) ToAWSSession(context *cli.Context) (*session.Session, error) {
	svcConfig := aws.Config{
		CredentialsChainVerboseErrors: aws.Bool(true),
	}
	if ecsEndpoint := RecursiveFlagSearch(context, flags.EndpointFlag); ecsEndpoint != "" {
		defaultResolver := endpoints.DefaultResolver()
		ecsCustomResolverFn := func(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
			if service == "ecs" {
				return endpoints.ResolvedEndpoint{
					URL: ecsEndpoint,
				}, nil
			}
			return defaultResolver.EndpointFor(service, region, optFns...)
		}
		svcConfig.EndpointResolver = endpoints.ResolverFunc(ecsCustomResolverFn)
	}

	return cfg.toAWSSessionWithConfig(context, &svcConfig)
}

// ToAWSSessionWithConfig processes credential order of precedence
// The argument svcConfig is needed to allow important unit tests to work
// (for example: assume role)
func (cfg *LocalConfig) toAWSSessionWithConfig(context *cli.Context, svcConfig *aws.Config) (*session.Session, error) {
	region, err := cfg.getRegion()

	if err != nil || region == "" {
		return nil, fmt.Errorf("Set a region using ecs-cli configure command with the --%s flag or %s environment variable or --%s flag", flags.RegionFlag, flags.AwsRegionEnvVar, flags.ProfileFlag)
	}

	if hasProfileFlags(context) {
		// The AWS SDK Go lets Env Vars override sourcing a profile from the shared credential file
		// This means that if the Env Vars are present, it will ignore the profile.
		// So we unset them and then reset them, to allow our order of precedence to be correct.
		keyID, secretKey := unsetEnvVars()
		defer resetEnvVars(keyID, secretKey)
		return sessionFromECSConfig(cfg, region, svcConfig)
	} else if hasEnvVars(context) {
		return sessionFromProfile("", region, svcConfig)
	} else if isDefaultECSProfileCase(cfg) {
		return sessionFromECSConfig(cfg, region, svcConfig)
	}

	return sessionFromProfile("", region, svcConfig)
}

func (cfg *LocalConfig) applyFlags(context *cli.Context) error {
	// Determine Launch Type
	// The launch type flag overrides default launch type stored in the local config
	if launchTypeFromFlag := RecursiveFlagSearch(context, flags.LaunchTypeFlag); launchTypeFromFlag != "" {
		cfg.DefaultLaunchType = launchTypeFromFlag
	}

	if err := ValidateLaunchType(cfg.DefaultLaunchType); err != nil {
		return err
	}

	// Determine cluster
	// Order of cluster resolution:
	//  1) Inline flag
	//  2) Environment Variable
	//  3) ECS Config
	if clusterFromEnv := os.Getenv(flags.ClusterEnvVar); clusterFromEnv != "" {
		cfg.Cluster = clusterFromEnv
	}
	if clusterFromFlag := RecursiveFlagSearch(context, flags.ClusterFlag); clusterFromFlag != "" {
		cfg.Cluster = clusterFromFlag
	}

	// Determine region
	// The --region flag takes highest precedence
	if regionFromFlag := RecursiveFlagSearch(context, flags.RegionFlag); regionFromFlag != "" {
		cfg.Region = regionFromFlag
	}

	// Determine profile
	// The --profile flag takes highest precedence
	if awsProfileFromFlag := RecursiveFlagSearch(context, flags.AWSProfileFlag); awsProfileFromFlag != "" {
		cfg.AWSProfile = awsProfileFromFlag
		// unset Access Key and Secret Key, otherwise they will take precedence
		cfg.AWSAccessKey = ""
		cfg.AWSSecretKey = ""
	}

	return nil
}

func hasProfileFlags(context *cli.Context) bool {
	return (RecursiveFlagSearch(context, flags.ECSProfileFlag) != "" || RecursiveFlagSearch(context, flags.AWSProfileFlag) != "")
}

func hasEnvVars(context *cli.Context) bool {
	return (os.Getenv(flags.AWSSecretKeyEnvVar) != "" && os.Getenv(flags.AWSAccessKeyEnvVar) != "")
}

func isDefaultECSProfileCase(cfg *LocalConfig) bool {
	return (cfg.AWSAccessKey != "" || cfg.AWSSecretKey != "" || cfg.AWSProfile != "")
}

func sessionFromECSConfig(cfg *LocalConfig, region string, svcConfig *aws.Config) (*session.Session, error) {
	if cfg.AWSSecretKey != "" {
		return sessionFromKeys(region, cfg.AWSAccessKey, cfg.AWSSecretKey, cfg.AWSSessionToken, svcConfig)
	}

	return sessionFromProfile(cfg.AWSProfile, region, svcConfig)
}

func unsetEnvVars() (keyID string, secretKey string) {
	keyID = os.Getenv(flags.AWSAccessKeyEnvVar)
	secretKey = os.Getenv(flags.AWSSecretKeyEnvVar)
	os.Unsetenv(flags.AWSAccessKeyEnvVar)
	os.Unsetenv(flags.AWSSecretKeyEnvVar)

	return keyID, secretKey
}

func resetEnvVars(keyID string, secretKey string) {
	os.Setenv(flags.AWSAccessKeyEnvVar, keyID)
	os.Setenv(flags.AWSSecretKeyEnvVar, secretKey)
}

func sessionFromProfile(profile string, region string, svcConfig *aws.Config) (*session.Session, error) {
	svcConfig.Region = aws.String(region)
	return session.NewSessionWithOptions(session.Options{
		Config:            *svcConfig,
		Profile:           profile,
		SharedConfigState: session.SharedConfigEnable,
	})
}

func sessionFromKeys(region string, awsAccess string, awsSecret string, sessionToken string, svcConfig *aws.Config) (*session.Session, error) {
	svcConfig.Region = aws.String(region)
	svcConfig.Credentials = credentials.NewStaticCredentials(awsAccess, awsSecret, sessionToken)

	return session.NewSession(svcConfig)
}

// Region: Order of resolution
//  1) ECS CLI Flags
//   a) Region Flag --region
//   b) Cluster Config Flag (--cluster-config)
//  2) ECS Config - attempts to fetch the region from the default ECS Profile
//  3) Environment Variable - attempts to fetch the region from environment variables:
//    a) AWS_REGION (OR)
//    b) AWS_DEFAULT_REGION
//  4) AWS Profile - attempts to use region from AWS profile name
//    a) --aws-profile flag
//    b) AWS_PROFILE environment variable
//    c) AWS_DEFAULT_PROFILE environment variable (defaults to 'default')
func (cfg *LocalConfig) getRegion() (string, error) {
	region := cfg.Region

	if region == "" {
		// Search the chain of environment variables for region.
		for _, envVar := range []string{flags.AwsRegionEnvVar, flags.AwsDefaultRegionEnvVar} {
			region = os.Getenv(envVar)
			if region != "" {
				break
			}
		}
	}

	var err error
	if region == "" {
		region, err = cfg.getRegionFromAWSProfile()
	}

	return region, err
}

func (cfg *LocalConfig) getRegionFromAWSProfile() (string, error) {
	awsProfile := ""
	if cfg.AWSProfile != "" {
		awsProfile = cfg.AWSProfile
	} else {
		awsProfile = os.Getenv(flags.AwsDefaultProfileEnvVar)
	}

	s, err := session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
		Profile:           awsProfile,
	})
	if err != nil {
		return "", err
	}

	return aws.StringValue(s.Config.Region), nil
}

// ValidateLaunchType checks that the launch type specified was an allowed value
func ValidateLaunchType(launchType string) error {
	if (launchType != "") && (launchType != LaunchTypeEC2) && (launchType != LaunchTypeFargate) {
		return fmt.Errorf("Supported launch types are '%s' and '%s'; %s is not a valid launch type.", LaunchTypeEC2, LaunchTypeFargate, launchType)
	}
	return nil
}
