// 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 local

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/converter"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/docker"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/localproject"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/network"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/options"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/secrets"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/secrets/clients"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/aws/aws-sdk-go/service/ssm"
	composeV3 "github.com/docker/cli/cli/compose/types"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

// Up creates a Compose file from an ECS task definition and runs it locally.
//
// The Amazon ECS Local Endpoints container needs to be running already for any
// local ECS task to work (see
// https://github.com/awslabs/amazon-ecs-local-container-endpoints). If the
// container is not running, this command creates a new network for all local
// ECS tasks to join and communicate with the Amazon ECS Local Endpoints
// container.
func Up(c *cli.Context) {
	if err := options.ValidateFlagPairs(c); err != nil {
		logrus.Fatalf(err.Error())
	}
	basePath, err := composeProjectPath(c)
	if err != nil {
		logrus.Fatalf("Failed to create Compose files due to:\n%v", err)
	}
	overridePaths, err := composeOverridePaths(basePath, c.StringSlice(flags.ComposeOverride))
	if err != nil {
		logrus.Fatalf("Failed to get the path of override Compose files due to:\n%v", err)
	}

	runContainers(basePath, overridePaths)
}

// composeProjectPath creates Compose files if necessary and returns the path of the base Compose file.
//
// If the user set the TaskDefinitionCompose flag, then return that Compose
// file path.  If the user doesn't have any flags set, and doesn't have
// LocalInFileName but has a LocalOutDefaultFileName, then we use the
// LocalOutDefaultFileName file.  Otherwise, we create a new Compose file from
// the user's flags and return its path.
func composeProjectPath(c *cli.Context) (string, error) {
	if c.IsSet(flags.TaskDefinitionFile) {
		return createNewComposeProject(c)
	}
	if c.IsSet(flags.TaskDefinitionRemote) {
		return createNewComposeProject(c)
	}
	if c.IsSet(flags.TaskDefinitionCompose) {
		return filepath.Abs(c.String(flags.TaskDefinitionCompose))
	}

	// No input flags were provided, prioritize LocalInFileName over LocalOutDefaultFileName.
	if _, err := os.Stat(localproject.LocalInFileName); err == nil {
		return createNewComposeProject(c)
	} else if !os.IsNotExist(err) {
		return "", errors.Wrapf(err, "could not check if file %s exists", localproject.LocalInFileName)
	}
	if _, err := os.Stat(localproject.LocalOutDefaultFileName); err == nil {
		return filepath.Abs(localproject.LocalOutDefaultFileName)
	} else if !os.IsNotExist(err) {
		return "", errors.Wrapf(err, "could not check if file %s exists", localproject.LocalOutDefaultFileName)
	}
	return "", fmt.Errorf("need to provide one of %s or %s", localproject.LocalInFileName, localproject.LocalOutDefaultFileName)
}

func createNewComposeProject(c *cli.Context) (string, error) {
	project := localproject.New(c)
	if err := createLocal(project); err != nil {
		return "", err
	}
	return project.LocalOutFileFullPath()
}

func composeOverridePaths(basePath string, additionalRelPaths []string) ([]string, error) {
	defaultPath := basePath[:len(basePath)-len(filepath.Ext(basePath))] + ".override.yml"
	paths := []string{defaultPath}
	if len(additionalRelPaths) > 0 {
		for _, relPath := range additionalRelPaths {
			p, err := filepath.Abs(relPath)
			if err != nil {
				return nil, errors.Wrapf(err, "cannot get absolute path of %s", relPath)
			}
			paths = append(paths, p)
		}
	}

	// Prune paths that don't exist
	var overridePaths []string
	for _, p := range paths {
		if _, err := os.Stat(p); err == nil {
			overridePaths = append(overridePaths, p)
		} else {
			logrus.Warnf("Skipping Compose file %s due to:\n%v", filepath.Base(p), err)
		}
	}
	return overridePaths, nil
}

func runContainers(basePath string, overridePaths []string) {
	network.Setup(docker.NewClient())

	config := readComposeFile(basePath)
	secrets := readSecrets(config)
	envVars := decryptSecrets(secrets)
	upCompose(envVars, basePath, overridePaths)
}

func readComposeFile(composePath string) *composeV3.Config {
	config, err := converter.UnmarshalComposeFile(composePath)
	if err != nil {
		logrus.Fatalf("Failed to unmarshal Compose file %s due to \n%v", composePath, err)
	}
	return config
}

func readSecrets(config *composeV3.Config) []*secrets.ContainerSecret {
	var containerSecrets []*secrets.ContainerSecret
	for _, service := range config.Services {
		for label, secretARN := range service.Labels {
			if !strings.HasPrefix(label, converter.SecretLabelPrefix) {
				continue
			}
			namespaces := strings.Split(label, ".")
			secretName := namespaces[len(namespaces)-1]

			containerSecrets = append(containerSecrets, secrets.NewContainerSecret(service.Name, secretName, secretARN))
		}
	}
	return containerSecrets
}

func decryptSecrets(containerSecrets []*secrets.ContainerSecret) (envVars map[string]string) {
	ssmClient, err := clients.NewSSMDecrypter()
	if err != nil {
		logrus.Fatalf("Failed to create a SSM client to decrypt secrets due to \n%v", err)
	}
	secretsManagerClient, err := clients.NewSecretsManagerDecrypter()
	if err != nil {
		logrus.Fatalf("Failed to create a SecretsManager client to decrypt secrets due to \n%v", err)
	}

	envVars = make(map[string]string)
	for _, containerSecret := range containerSecrets {
		service, err := containerSecret.ServiceName()
		if err != nil {
			logrus.Fatalf("Failed to retrieve the service of the secret due to \n%v", err)
		}

		decrypted := ""
		switch service {
		case secretsmanager.ServiceName:
			decrypted, err = containerSecret.Decrypt(secretsManagerClient)
		case ssm.ServiceName:
			decrypted, err = containerSecret.Decrypt(ssmClient)
		default:
			err = fmt.Errorf("can't decrypt secret from service %s", service)
		}
		if err != nil {
			logrus.Fatalf("Failed to decrypt secret due to \n%v", err)
		}
		envVars[containerSecret.Name()] = decrypted
	}
	return
}

// upCompose starts the containers in the Compose files with the environment variables defined in envVars.
func upCompose(envVars map[string]string, basePath string, overridePaths []string) {
	// Gather environment variables

	// Pass in $PATH and other env vars needed by docker-compose.
	// See https://stackoverflow.com/a/55371721/1201381, https://github.com/aws/amazon-ecs-cli/issues/892
	envs := os.Environ()
	for env, val := range envVars {
		envs = append(envs, fmt.Sprintf("%s=%s", env, val))
	}

	// Disable orphaned containers checking
	envs = append(envs, "COMPOSE_IGNORE_ORPHANS=true")

	// Gather command arguments
	var b bytes.Buffer
	b.WriteString(filepath.Base(basePath))
	args := []string{"-f", basePath}
	for _, p := range overridePaths {
		b.WriteString(fmt.Sprintf(", %s", filepath.Base(p)))
		args = append(args, "-f", p)
	}
	args = append(args, "up", "--build", "-d")

	// Run the command with the environment variables and arguments
	logrus.Infof("Using %s files to start containers", b.String())
	cmd := exec.Command("docker-compose", args...)
	cmd.Env = envs
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Start(); err != nil {
		logrus.Fatalf("docker-compose up failed to start due to:\n%v", err)
	}
	if err := cmd.Wait(); err != nil {
		logrus.Fatalf("docker-compose up failed to exit successfully due to:\n%v", err)
	}
}
