// Copyright 2015-2018 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 servicediscovery

import (
	"fmt"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/context"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/cloudformation"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
	utils "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/compose"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

const (
	privateDNSNamespaceStackNameFormat     = "amazon-ecs-cli-setup-private-dns-namespace-%s-%s"
	serviceDiscoveryServiceStackNameFormat = "amazon-ecs-cli-setup-service-discovery-service-%s-%s"
)

const (
	cfnTemplateOutputPrivateNamespaceID = "PrivateDNSNamespaceID"
	cfnTemplateOutputSDSARN             = "ServiceDiscoveryServiceARN"
)

// CreateFunc is the interface/signature for Create
// This helps when writing code in other packages that need to mock Create (specifically it's a nicety that helps IDE features work)
type CreateFunc func(networkMode, serviceName string, c *context.ECSContext) (*ecs.ServiceRegistry, error)

// Create creates a DNS namespace (or uses an existing one) and creates a Service Discovery Service
// The Service Discovery ARN is returned so that it can be used to enable ECS Service Discovery
func Create(networkMode, serviceName string, c *context.ECSContext) (*ecs.ServiceRegistry, error) {
	cfnClient := cloudformation.NewCloudformationClient(c.CommandConfig)

	var ecsParamsSD *utils.ServiceDiscovery
	if c.ECSParams != nil {
		ecsParamsSD = &c.ECSParams.RunParams.ServiceDiscovery
	} else {
		ecsParamsSD = &utils.ServiceDiscovery{}
	}

	return create(c.CLIContext, networkMode, serviceName, cfnClient, ecsParamsSD, c.CommandConfig)
}

// UpdateFunc is the interface/signature for Create
// This helps when writing code in other packages that need to mock Update (specifically it's a nicety that helps IDE features work)
type UpdateFunc func(networkMode, serviceName string, c *context.ECSContext) error

// Update updates values for Service Discovery
// Only a few values on the SDS are available for update: DNS TTL and FailureThreshold
func Update(networkMode, serviceName string, c *context.ECSContext) error {
	cfnClient := cloudformation.NewCloudformationClient(c.CommandConfig)

	var ecsParamsSD *utils.ServiceDiscovery
	if c.ECSParams != nil {
		ecsParamsSD = &c.ECSParams.RunParams.ServiceDiscovery
	} else {
		ecsParamsSD = &utils.ServiceDiscovery{}
	}

	return update(c.CLIContext, networkMode, serviceName, c.CommandConfig.Cluster, cfnClient, ecsParamsSD)
}

// DeleteFunc is the interface/signature for Delete
// This helps when writing code in other packages that need to mock Create (specifically it's a nicety that helps IDE features work)
type DeleteFunc func(serviceName string, c *context.ECSContext) error

// Delete deletes resources for service discovery
func Delete(serviceName string, c *context.ECSContext) error {
	cfnClient := cloudformation.NewCloudformationClient(c.CommandConfig)

	return delete(c.CLIContext, cfnClient, serviceName, c.ProjectName, c.CommandConfig.Cluster)
}

func update(c *cli.Context, networkMode, serviceName, clusterName string, cfnClient cloudformation.CloudformationClient, ecsParamsSD *utils.ServiceDiscovery) error {
	warnOnFlagsNotValidForUpdate(c)

	sdsInput, err := mergeSDSFields(c, ecsParamsSD.ServiceDiscoveryService)
	if err != nil {
		return err
	}

	sdsStackName := cfnStackName(serviceDiscoveryServiceStackNameFormat, clusterName, serviceName)
	existingParameters, err := cfnClient.GetStackParameters(sdsStackName)
	if err != nil {
		return errors.Wrap(err, "CloudFormation stack not found for Service Discovery Service")
	}

	sdsParams, err := getSDSCFNParamsForUpdate(networkMode, sdsInput, existingParameters)
	if err != nil {
		return err
	}
	if err := sdsParams.Validate(); err != nil {
		return err
	}

	if _, err := cfnClient.UpdateStack(sdsStackName, sdsParams); err != nil {
		return err
	}

	logrus.Info("Waiting for your Service Discovery resources to be updated...")
	return cfnClient.WaitUntilUpdateComplete(sdsStackName)
}

func delete(c *cli.Context, cfnClient cloudformation.CloudformationClient, serviceName, projectName, clusterName string) error {
	sdsStackName := cfnStackName(serviceDiscoveryServiceStackNameFormat, clusterName, serviceName)
	err := deleteStack(sdsStackName, projectName, "Service Discovery Service", cfnClient, true, false)
	if err != nil {
		return err
	}

	if c.Bool(flags.DeletePrivateNamespaceFlag) {
		namspaceStackName := cfnStackName(privateDNSNamespaceStackNameFormat, clusterName, serviceName)
		err = deleteStack(namspaceStackName, projectName, "Private DNS Namespace", cfnClient, false, false)
		if err != nil {
			return err
		}
	}
	return nil
}

func deleteStack(stackName, projectName, resource string, cfnClient cloudformation.CloudformationClient, ignoreValidation, cleanUp bool) error {
	if err := cfnClient.ValidateStackExists(stackName); err != nil {
		if ignoreValidation {
			return nil
		}
		return errors.Wrapf(err, "no %s CloudFormation stack found for project '%s'", resource, projectName)
	}
	if cleanUp {
		logrus.Info("Cleaning up existing CloudFormation stack...")
	} else {
		logrus.Infof("Waiting for your %s resource to be deleted...", resource)
	}
	if err := cfnClient.DeleteStack(stackName); err != nil {
		return err
	}
	return cfnClient.WaitUntilDeleteComplete(stackName)
}

func create(c *cli.Context, networkMode, serviceName string, cfnClient cloudformation.CloudformationClient, ecsParamsSD *utils.ServiceDiscovery, config *config.CommandConfig) (*ecs.ServiceRegistry, error) {
	err := validateNameAndIdExclusive(c, ecsParamsSD)
	if err != nil {
		return nil, err
	}
	mergedInput, err := mergeSDFlagsAndInput(c, ecsParamsSD)
	if err != nil {
		return nil, err
	}
	err = validateMergedSDInputFields(mergedInput, networkMode)
	if err != nil {
		return nil, err
	}
	namespaceWarningsWhenIDSpecified(mergedInput)

	namespaceID, err := getOrCreateNamespace(c, networkMode, serviceName, cfnClient, mergedInput, config)
	if err != nil {
		return nil, err
	}

	// create SDS
	sdsParams := getSDSCFNParams(aws.StringValue(namespaceID), serviceName, networkMode, mergedInput)
	if err := sdsParams.Validate(); err != nil {
		return nil, err
	}

	sdsStackName := cfnStackName(serviceDiscoveryServiceStackNameFormat, config.Cluster, serviceName)

	// first try to delete the SDS Stack to clean up previous attempts that failed
	if err := deleteStack(sdsStackName, serviceName, "Service Discovery Service", cfnClient, true, true); err != nil {
		return nil, errors.Wrapf(err, "A Service Discovery Service CloudFormation stack for %s already exists, failed to delete existing stack", serviceName)
	}

	if _, err := cfnClient.CreateStack(cloudformation.GetSDSTemplate(), sdsStackName, false, sdsParams, nil); err != nil {
		return nil, err
	}

	logrus.Info("Waiting for the Service Discovery Service to be created...")
	cfnClient.WaitUntilCreateComplete(sdsStackName)

	registryARN, err := getOutputIDFromStack(cfnClient, sdsStackName, cfnTemplateOutputSDSARN)
	var containerName *string
	if mergedInput.ContainerName != "" {
		containerName = aws.String(mergedInput.ContainerName)
	}
	serviceRegistry := &ecs.ServiceRegistry{
		RegistryArn:   registryARN,
		ContainerName: containerName,
		ContainerPort: mergedInput.ContainerPort,
	}
	return serviceRegistry, err
}

// createNamespace creates a private DNS namespace
// This function is used if getExistingNamespace() fails to find an existing namespace with the required settings
// If a CFN stack with the same name exists already, we therefore know that it doesn't contain a namespace (stack create failed),
// So we can safely delete the existing stack
func createNamespace(c *cli.Context, networkMode, serviceName, clusterName string, cfnClient cloudformation.CloudformationClient, input *utils.ServiceDiscovery) (*string, error) {
	namespaceParams := getNamespaceCFNParams(input)
	if err := namespaceParams.Validate(); err != nil {
		return nil, err
	}

	namespaceStackName := cfnStackName(privateDNSNamespaceStackNameFormat, clusterName, serviceName)

	if err := deleteStack(namespaceStackName, serviceName, "Private DNS Namespace", cfnClient, true, true); err != nil {
		return nil, errors.Wrapf(err, "A Private DNS Namespace CloudFormation stack for %s already exists, failed to delete existing stack: %s", serviceName, err)
	}

	if _, err := cfnClient.CreateStack(cloudformation.GetPrivateNamespaceTemplate(), namespaceStackName, false, namespaceParams, nil); err != nil {
		return nil, err
	}

	logrus.Info("Waiting for the private DNS namespace to be created...")
	cfnClient.WaitUntilCreateComplete(namespaceStackName)

	// Get the ID of the namespace we just created
	return getOutputIDFromStack(cfnClient, namespaceStackName, cfnTemplateOutputPrivateNamespaceID)
}

func getOrCreateNamespace(c *cli.Context, networkMode, serviceName string, cfnClient cloudformation.CloudformationClient, input *utils.ServiceDiscovery, config *config.CommandConfig) (*string, error) {
	namespace, err := getExistingNamespace(input, config)
	if err != nil {
		return nil, err
	}
	if namespace == nil {
		namespace, err = createNamespace(c, networkMode, serviceName, config.Cluster, cfnClient, input)
	} else {
		logrus.Infof("Using existing namespace %s", *namespace)
	}
	return namespace, err
}

func getExistingNamespace(input *utils.ServiceDiscovery, config *config.CommandConfig) (*string, error) {
	switch {
	case input.PrivateDNSNamespace.ID != "":
		return aws.String(input.PrivateDNSNamespace.ID), nil
	case input.PublicDNSNamespace.ID != "":
		return aws.String(input.PublicDNSNamespace.ID), nil
	case input.PrivateDNSNamespace.Name != "":
		return findPrivateNamespace(input.PrivateDNSNamespace.Name, input.PrivateDNSNamespace.VPC, config)
	case input.PublicDNSNamespace.Name != "":
		return getPublicNamespaceSpecifiedByName(input.PublicDNSNamespace.Name, config)
	default:
		return nil, nil
	}
}

func getPublicNamespaceSpecifiedByName(name string, config *config.CommandConfig) (*string, error) {
	namespace, err := findPublicNamespace(name, config)
	if err != nil {
		return nil, err
	}
	if namespace == nil {
		// we do not create public namespaces, so failing to find it is in an error case
		return nil, fmt.Errorf("Failed to find public namespace %s", name)
	}
	return namespace, err
}

// Flags override the values specified in ECS Params
// Merges fields for Namespace and SDS
// This function just merges fields; it doesn't validate them
func mergeSDFlagsAndInput(c *cli.Context, ecsParamsSD *utils.ServiceDiscovery) (*utils.ServiceDiscovery, error) {
	// Private DNS Namespace fields
	privNamespace := ecsParamsSD.PrivateDNSNamespace
	privNamespace.Namespace = resolveNamespaceOverride(c.String(flags.PrivateDNSNamespaceNameFlag), c.String(flags.PrivateDNSNamespaceIDFlag), "private", privNamespace.Namespace)
	privNamespace.VPC = resolveStringFieldOverride(c, flags.VpcIdFlag, privNamespace.VPC, "private_dns_namespace.vpc")
	ecsParamsSD.PrivateDNSNamespace = privNamespace

	// Public DNS Namespace fields
	pubNamespace := ecsParamsSD.PublicDNSNamespace
	pubNamespace.Namespace = resolveNamespaceOverride(c.String(flags.PublicDNSNamespaceNameFlag), c.String(flags.PublicDNSNamespaceIDFlag), "public", pubNamespace.Namespace)
	ecsParamsSD.PublicDNSNamespace = pubNamespace

	// SDS fields
	sds := ecsParamsSD.ServiceDiscoveryService
	var err error
	ecsParamsSD.ServiceDiscoveryService, err = mergeSDSFields(c, sds)
	if err != nil {
		return nil, err
	}

	// top level fields
	// these container fields aren't used when creating Route53 resources in this package, but we aggregate them here so that we can do error checking- SRV records require these.
	ecsParamsSD.ContainerName = resolveStringFieldOverride(c, flags.ServiceDiscoveryContainerNameFlag, ecsParamsSD.ContainerName, "container_name")
	port, err := resolveIntPointerFieldOverride(c, flags.ServiceDiscoveryContainerPortFlag, ecsParamsSD.ContainerPort, "container_port")
	if err != nil {
		return nil, err
	}
	ecsParamsSD.ContainerPort = port

	return ecsParamsSD, nil
}

// Merges flags and ECS Params for the SDS
func mergeSDSFields(c *cli.Context, sds utils.ServiceDiscoveryService) (utils.ServiceDiscoveryService, error) {
	sds.DNSConfig.Type = resolveStringFieldOverride(c, flags.DNSTypeFlag, sds.DNSConfig.Type, "dns_config.type")
	ttl, err := resolveIntPointerFieldOverride(c, flags.DNSTTLFlag, sds.DNSConfig.TTL, "dns_config.ttl")
	if err != nil {
		return sds, err
	}
	sds.DNSConfig.TTL = ttl
	threshold, err := resolveIntPointerFieldOverride(c, flags.HealthcheckCustomConfigFailureThresholdFlag, sds.HealthCheckCustomConfig.FailureThreshold, "failure_threshold")
	if err != nil {
		return sds, err
	}
	sds.HealthCheckCustomConfig.FailureThreshold = threshold

	return sds, nil
}
