// 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 network provides functionality to setup and teardown the ECS local network.
package network

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/docker"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/client"
	"github.com/sirupsen/logrus"
	"golang.org/x/net/context"
)

// LocalEndpointsStarter groups Docker functions to create the ECS local network and
// the Local Container Endpoints container if they don't exist.
type LocalEndpointsStarter interface {
	networkCreator
	imagePuller
	containerStarter
}

type networkCreator interface {
	NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
	NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
}

type imagePuller interface {
	ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error)
	ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
}

type containerStarter interface {
	ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error)
	ContainerCreate(ctx context.Context, config *container.Config,
		hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig,
		containerName string) (container.ContainerCreateCreatedBody, error)
	ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error
}

// Configuration for the ECS local network.
const (
	// EcsLocalNetworkName is the name of the network created for local ECS tasks to join.
	EcsLocalNetworkName = "ecs-local-network"

	// Range of IP addresses that containers in the network get assigned.
	ecsLocalNetworkSubnet = "169.254.170.0/24"
)

// Configuration for the Local Endpoints container.
const (
	// Name of the image used to pull from DockerHub, see https://hub.docker.com/r/amazon/amazon-ecs-local-container-endpoints
	localEndpointsImageName = "amazon/amazon-ecs-local-container-endpoints"

	// Reserved IP address that the container listens to answer requests on metadata, creds, and stats.
	localEndpointsContainerIpAddr = "169.254.170.2"

	// Name of the container, we need to give it a name so that we don't re-create a container every time we setup.
	localEndpointsContainerName = "amazon-ecs-local-container-endpoints"


)

// Setup creates a user-defined bridge network with a running Local Container Endpoints container. It will pull
// the Local Endpoints image if it doesn't exist.
//
// If the network, image, and container already exist, then do nothing.
// If there is any unexpected errors, we exit the program with a fatal log.
func Setup(dockerClient LocalEndpointsStarter) {
	setupLocalNetwork(dockerClient)
	setupLocalEndpointsImage(dockerClient)
	setupLocalEndpointsContainer(dockerClient)
}

func setupLocalNetwork(dockerClient networkCreator) {
	if localNetworkExists(dockerClient) {
		logrus.Infof("The network %s already exists", EcsLocalNetworkName)
		return
	}
	createLocalNetwork(dockerClient)
}

func localNetworkExists(dockerClient networkCreator) bool {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	_, err := dockerClient.NetworkInspect(ctx, EcsLocalNetworkName, types.NetworkInspectOptions{})
	if err != nil {
		if client.IsErrNotFound(err) {
			return false
		}
		// Unexpected error while inspecting docker networks, we want to crash the app.
		logrus.Fatalf("Failed to inspect docker network %s due to %v", EcsLocalNetworkName, err)
	}
	return true
}

func createLocalNetwork(dockerClient networkCreator) {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	logrus.Infof("Creating network: %s...", EcsLocalNetworkName)
	resp, err := dockerClient.NetworkCreate(ctx, EcsLocalNetworkName, types.NetworkCreate{
		IPAM: &network.IPAM{
			Config: []network.IPAMConfig{
				{
					Subnet: ecsLocalNetworkSubnet,
				},
			},
		},
	})
	if err != nil {
		logrus.Fatalf("Failed to create network %s with subnet %s due to %v", EcsLocalNetworkName, ecsLocalNetworkSubnet, err)
	}
	logrus.Infof("Created network %s with ID %s", EcsLocalNetworkName, resp.ID)
}

func setupLocalEndpointsImage(dockerClient imagePuller) {
	if localEndpointsImageExists(dockerClient) {
		return
	}
	pullLocalEndpointsImage(dockerClient)
}

func localEndpointsImageExists(dockerClient imagePuller) bool {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	args := filters.NewArgs(filters.Arg("reference", localEndpointsImageName))
	imgs, err := dockerClient.ImageList(ctx, types.ImageListOptions{
		Filters: args,
	})
	if err != nil {
		logrus.Fatalf("Failed to list images with filters %v due to %v", args, err)
	}

	for _, img := range imgs {
		for _, repotag := range img.RepoTags {
			if strings.HasPrefix(repotag, localEndpointsImageName) {
				return true
			}
		}
	}
	return false
}

func pullLocalEndpointsImage(dockerClient imagePuller) {
	ctx, cancel := context.WithTimeout(context.Background(), docker.LongTimeoutInS)
	defer cancel()

	logrus.Infof("Pulling image %s", localEndpointsImageName)
	rc, err := dockerClient.ImagePull(ctx, localEndpointsImageName, types.ImagePullOptions{})
	if err != nil {
		logrus.Fatalf("Failed to pull image %s due to %v", localEndpointsImageName, err)
	}
	defer rc.Close()

	_, err = ioutil.ReadAll(rc)
	if err != nil {
		logrus.Fatalf("Failed to download the image %s due to %v", localEndpointsImageName, err)
	}
	logrus.Infof("Pulled image %s", localEndpointsImageName)
}

func setupLocalEndpointsContainer(docker containerStarter) {
	containerID := createLocalEndpointsContainer(docker)
	startContainer(docker, containerID)
}

// createLocalEndpointsContainer returns the ID of the newly created container.
// If the container already exists, returns the ID of the existing container.
func createLocalEndpointsContainer(dockerClient containerStarter) string {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	profile, profile_set := os.LookupEnv("AWS_PROFILE")
	if !profile_set {
		profile = "default"
	}

	// See First Scenario in https://aws.amazon.com/blogs/compute/a-guide-to-locally-testing-containers-with-amazon-ecs-local-endpoints-and-docker-compose/
	// for an explanation of these fields.
	resp, err := dockerClient.ContainerCreate(ctx,
		&container.Config{
			Image: localEndpointsImageName,
			Env: []string{
				fmt.Sprintf("AWS_PROFILE=%s", profile),
				"HOME=/home",
			},
		},
		&container.HostConfig{
			Binds: []string{
				"/var/run:/var/run",
				fmt.Sprintf("%s/.aws/:/home/.aws/", os.Getenv("HOME")),
			},
		},
		&network.NetworkingConfig{
			EndpointsConfig: map[string]*network.EndpointSettings{
				EcsLocalNetworkName: {
					NetworkID: EcsLocalNetworkName,
					IPAMConfig: &network.EndpointIPAMConfig{
						IPv4Address: localEndpointsContainerIpAddr,
					},
				},
			},
		},
		localEndpointsContainerName,
	)
	if err != nil {
		if strings.Contains(err.Error(), "Conflict") {
			// We already created this container before, fetch its ID and return it.
			containerID := localEndpointsContainerID(dockerClient)
			logrus.Infof("The %s container already exists with ID %s", localEndpointsContainerName, containerID)
			return containerID
		}
		logrus.Fatalf("Failed to create container %s due to %v", localEndpointsContainerName, err)
	}

	logrus.Infof("Created the %s container with ID %s", localEndpointsContainerName, resp.ID)
	return resp.ID
}

func localEndpointsContainerID(dockerClient containerStarter) string {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	resp, err := dockerClient.ContainerList(ctx, types.ContainerListOptions{
		All: true,
		Filters: filters.NewArgs(
			filters.Arg("name", localEndpointsContainerName),
		),
	})
	if err != nil {
		logrus.Fatalf("Failed to list containers with name %s due to %v", localEndpointsContainerName, err)
	}
	if len(resp) != 1 {
		logrus.Fatalf("Expected to find one container named %s but found %d", localEndpointsContainerName, len(resp))
	}
	return resp[0].ID
}

func startContainer(dockerClient containerStarter, containerID string) {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	// If the container is already running, Docker does not return an error response.
	if err := dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
		logrus.Fatalf("Failed to start container with ID %s due to %v", containerID, err)
	}
	logrus.Infof("Started container with ID %s", containerID)
}
