// 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 (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"

	"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/options"
	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
	"golang.org/x/net/context"
)

// Table formatting settings used by the Docker CLI.
// See https://github.com/docker/cli/blob/0904fbfc77dbd4b6296c56e68be573b889d049e3/cli/command/formatter/formatter.go#L74
const (
	cellWidthInSpaces         = 20
	widthBetweenCellsInSpaces = 1
	cellPaddingInSpaces       = 3
	paddingCharacter          = ' '
	noFormatting              = 0
	maxContainerIDLength      = 12
)

// JSON formatting settings.
const (
	jsonPrefix = ""
	jsonIndent = "  "
)

// Ps lists the status of the ECS task containers running locally as a table.
//
// Defaults to listing containers from the local Compose file.
// If the --all flag is provided, then list all local ECS task containers.
// If the --json flag is provided, then output the format as JSON instead.
func Ps(c *cli.Context) {
	if err := options.ValidateFlagPairs(c); err != nil {
		logrus.Fatal(err.Error())
	}
	containers, err := listContainers(c)
	if err != nil {
		logrus.Fatalf("Failed to list containers due to:\n%v", err)
	}
	if err = displayContainers(c, containers); err != nil {
		logrus.Fatalf("Failed to display containers due to:\n%v", err)
	}
}

func listContainers(c *cli.Context) ([]types.Container, error) {
	if c.IsSet(flags.TaskDefinitionFile) {
		file, err := filepath.Abs(c.String(flags.TaskDefinitionFile))
		if err != nil {
			return nil, err
		}
		return listContainersWithFilters(filters.NewArgs(
			filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelValue, file)),
			filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelType, localproject.LocalTaskDefType)),
		))
	}
	if c.IsSet(flags.TaskDefinitionRemote) {
		return listContainersWithFilters(filters.NewArgs(
			filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelValue,
				c.String(flags.TaskDefinitionRemote))),
			filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelType, localproject.RemoteTaskDefType)),
		))
	}
	if c.Bool(flags.All) {
		return listContainersWithFilters(filters.NewArgs(
			filters.Arg("label", converter.TaskDefinitionLabelValue),
		))
	}

	defaultFile, err := filepath.Abs(localproject.LocalInFileName)
	if err != nil {
		return nil, err
	}

	return listContainersWithFilters(filters.NewArgs(
		filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelValue, defaultFile)),
		filters.Arg("label", fmt.Sprintf("%s=%s", converter.TaskDefinitionLabelType, localproject.LocalTaskDefType)),
	))
}

func listContainersWithFilters(args filters.Args) ([]types.Container, error) {
	ctx, cancel := context.WithTimeout(context.Background(), docker.TimeoutInS)
	defer cancel()

	cl := docker.NewClient()
	containers, err := cl.ContainerList(ctx, types.ContainerListOptions{
		All:     true, // Include containers that are not running so that they can be removed with local down.
		Filters: args,
	})
	if err != nil {
		return nil, errors.Wrapf(err, "failed to list containers with args=%v", args)
	}
	return containers, nil
}

func displayContainers(c *cli.Context, containers []types.Container) error {
	if c.Bool(flags.JSON) {
		return displayAsJSON(containers)
	}
	return displayAsTable(containers)
}

func displayAsJSON(containers []types.Container) error {
	data, err := json.MarshalIndent(containers, jsonPrefix, jsonIndent)
	if err != nil {
		return errors.Wrap(err, "failed to marshal containers to JSON")
	}
	fmt.Fprintln(os.Stdout, string(data))
	return nil
}

func displayAsTable(containers []types.Container) error {
	w := new(tabwriter.Writer)

	w.Init(os.Stdout, cellWidthInSpaces, widthBetweenCellsInSpaces, cellPaddingInSpaces, paddingCharacter, noFormatting)
	fmt.Fprintln(w, "CONTAINER ID\tIMAGE\tSTATUS\tPORTS\tNAMES\tTASKDEFINITION")
	for _, container := range containers {
		row := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s",
			container.ID[:maxContainerIDLength],
			container.Image,
			container.Status,
			prettifyPorts(container.Ports),
			prettifyNames(container.Names),
			container.Labels[converter.TaskDefinitionLabelValue])
		fmt.Fprintln(w, row)
	}
	return w.Flush()
}

func prettifyPorts(containerPorts []types.Port) string {
	var prettyPorts []string
	for _, port := range containerPorts {
		// See https://github.com/docker/cli/blob/0904fbfc77dbd4b6296c56e68be573b889d049e3/cli/command/formatter/container.go#L268
		prettyPorts = append(prettyPorts, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
	}
	return strings.Join(prettyPorts, ", ")
}

func prettifyNames(containerNames []string) string {
	return strings.Join(containerNames, ", ")
}
