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

// ECS Params Reader is used to parse the ecs-params.yml file and marshal the data into the ECSParams struct

import (
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"time"

	"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/adapter"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/ecs"
	libYaml "github.com/docker/libcompose/yaml"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v2"
)

///////////////////////////////////
///// ECS Params Schema types /////
///////////////////////////////////

// ECSParams contains the information parsed from the ecs-params.yml file
type ECSParams struct {
	Version        string
	TaskDefinition EcsTaskDef `yaml:"task_definition"`
	RunParams      RunParams  `yaml:"run_params"`
}

// EcsTaskDef corresponds to fields in an ECS TaskDefinition
type EcsTaskDef struct {
	NetworkMode          string         `yaml:"ecs_network_mode"`
	TaskRoleArn          string         `yaml:"task_role_arn"`
	PIDMode              string         `yaml:"pid_mode"`
	IPCMode              string         `yaml:"ipc_mode"`
	ContainerDefinitions ContainerDefs  `yaml:"services"`
	ExecutionRole        string         `yaml:"task_execution_role"`
	TaskSize             TaskSize       `yaml:"task_size"` // Needed to run FARGATE tasks
	DockerVolumes        []DockerVolume `yaml:"docker_volumes"`
	EFSVolumes           []EFSVolume    `yaml:"efs_volumes"`
	PlacementConstraints []Constraint   `yaml:"placement_constraints"`
}

// ContainerDefs is a map of ContainerDefs within a task definition
type ContainerDefs map[string]ContainerDef

// ContainerDef holds fields for an ECS Container Definition that are not supplied by docker-compose
type ContainerDef struct {
	Essential             bool                  `yaml:"essential"`
	InitProcessEnabled    bool                  `yaml:"init_process_enabled"`
	RepositoryCredentials RepositoryCredentials `yaml:"repository_credentials"`
	// resource field yaml names correspond to equivalent docker-compose field
	Cpu                   int64                  `yaml:"cpu_shares"`
	Memory                libYaml.MemStringorInt `yaml:"mem_limit"`
	MemoryReservation     libYaml.MemStringorInt `yaml:"mem_reservation"`
	HealthCheck           *HealthCheck           `yaml:"healthcheck"`
	Logging               Logging                `yaml:"logging"`
	FirelensConfiguration FirelensConfiguration  `yaml:"firelens_configuration"`
	Secrets               []Secret               `yaml:"secrets"`
	GPU                   string                 `yaml:"gpu"`
	ContainerDependencies []ContainerDependency  `yaml:"depends_on"`
}

type Volume struct {
	DockerVolumeConfig DockerVolume
	EFSVolumeConfig    EFSVolume
}

type DockerVolume struct {
	Name          string            `yaml:"name"`
	Scope         *string           `yaml:"scope"`
	Autoprovision *bool             `yaml:"autoprovision"`
	Driver        *string           `yaml:"driver"`
	DriverOptions map[string]string `yaml:"driver_opts"`
	Labels        map[string]string `yaml:"labels"`
}

type EFSVolume struct {
	Name                  string  `yaml:"name"`
	FileSystemID          *string `yaml:"filesystem_id"` // Required
	RootDirectory         *string `yaml:"root_directory"`
	TransitEncryption     *string `yaml:"transit_encryption"` // Optional. default: DISABLED. options: ENABLED or DISABLED
	TransitEncryptionPort *int64  `yaml:"transit_encryption_port"`
	AccessPointID         *string `yaml:"access_point"`
	IAM                   *string `yaml:"iam"` // default: DISABLED. options: ENABLED or DISABLED
}

// Firelens holds all possible fields for logging via Firelens
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_FirelensConfiguration.html
type FirelensConfiguration struct {
	Type    string            `yaml:"type"`
	Options map[string]string `yaml:"options"`
}

// HealthCheck holds all possible fields for HealthCheck, including fields
// supported by docker compose vs ECS
type HealthCheck struct {
	Test        libYaml.Stringorslice
	Command     libYaml.Stringorslice
	Timeout     string `yaml:"timeout,omitempty"`
	Interval    string `yaml:"interval,omitempty"`
	Retries     int64  `yaml:"retries,omitempty"`
	StartPeriod string `yaml:"start_period,omitempty"`
}

// RepositoryCredentials holds CredentialParameters for a ContainerDef
type RepositoryCredentials struct {
	CredentialsParameter string `yaml:"credentials_parameter"`
}

// ContainerDependency holds a dependency container
type ContainerDependency struct {
	ContainerName string `yaml:"container_name"`
	Condition     string `yaml:"condition"`
}

// Logging holds a list of Secrets within SecretOptions. They are essentially
// identical to secrets injected into the container environment by ECS, but are
// instead injected into the Docker logging configuration.
type Logging struct {
	SecretOptions []Secret `yaml:"secret_options"`
}

// Secret supports the ECS Secrets integration with SSM Parameter Store
type Secret struct {
	ValueFrom string `yaml:"value_from"`
	Name      string `yaml:"name"`
}

// TaskSize holds Cpu and Memory values needed for Fargate tasks
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
type TaskSize struct {
	Cpu    string `yaml:"cpu_limit"`
	Memory string `yaml:"mem_limit"`
}

// RunParams specifies non-TaskDefinition specific parameters
type RunParams struct {
	NetworkConfiguration NetworkConfiguration `yaml:"network_configuration"`
	TaskPlacement        TaskPlacement        `yaml:"task_placement"`
	ServiceDiscovery     ServiceDiscovery     `yaml:"service_discovery"`
}

// NetworkConfiguration specifies the network config for the task definition.
// Supports values 'awsvpc' (required for Fargate), 'bridge', 'host' or 'none'
type NetworkConfiguration struct {
	AwsVpcConfiguration AwsVpcConfiguration `yaml:"awsvpc_configuration"`
}

// AwsVpcConfiguration specifies the networking resources available to
// tasks running in 'awsvpc' networking mode
type AwsVpcConfiguration struct {
	Subnets        []string       `yaml:"subnets"`
	SecurityGroups []string       `yaml:"security_groups"`
	AssignPublicIp AssignPublicIp `yaml:"assign_public_ip"` // Needed to run FARGATE tasks
}

// TODO: Remove; use enum in aws-sdk-go instead (AssignPublicIpEnabled, AssignPublicIpDisabled)
type AssignPublicIp string

// ServiceDiscovery holds information related to ECS/Route53 Service Discovery
type ServiceDiscovery struct {
	ContainerName           string                  `yaml:"container_name"`
	ContainerPort           *int64                  `yaml:"container_port"`
	PrivateDNSNamespace     PrivateDNSNamespace     `yaml:"private_dns_namespace"`
	PublicDNSNamespace      PublicDNSNamespace      `yaml:"public_dns_namespace"`
	ServiceDiscoveryService ServiceDiscoveryService `yaml:"service_discovery_service"`
}

// Namespace holds the basic information for any type of namespace
type Namespace struct {
	ID   string `yaml:"id"`
	Name string `yaml:"name"`
}

// PrivateDNSNamespace holds information related to Route53 private DNS namespaces
type PrivateDNSNamespace struct {
	Namespace   `yaml:",inline"`
	VPC         string `yaml:"vpc"`
	Description string `yaml:"description"`
}

// PublicDNSNamespace holds information related to Route53 public DNS namespaces
type PublicDNSNamespace struct {
	Namespace `yaml:",inline"`
}

// ServiceDiscoveryService holds information related to Route53 Service Discovery Services
type ServiceDiscoveryService struct {
	Name                    string                  `yaml:"name"`
	Description             string                  `yaml:"description"`
	DNSConfig               DNSConfig               `yaml:"dns_config"`
	HealthCheckCustomConfig HealthCheckCustomConfig `yaml:"healthcheck_custom_config"`
}

// DNSConfig holds the dns configuration for Service Discovery Services
type DNSConfig struct {
	Type string `yaml:"type"`
	TTL  *int64 `yaml:"ttl"`
}

// HealthCheckCustomConfig
type HealthCheckCustomConfig struct {
	FailureThreshold *int64 `yaml:"failure_threshold"`
}

const (
	Enabled  AssignPublicIp = "ENABLED"
	Disabled AssignPublicIp = "DISABLED"
)

type TaskPlacement struct {
	Strategies  []Strategy   `yaml:"strategy"`
	Constraints []Constraint `yaml:"constraints"`
}

type Strategy struct {
	Field string `yaml:"field"`
	Type  string `yaml:"type"`
}

type Constraint struct {
	Expression string `yaml:"expression"`
	Type       string `yaml:"type"`
}

/////////////////////////////
///// Parsing Functions /////
/////////////////////////////

// Having a custom Unmarshaller on ContainerDef allows us to set custom defaults on the Container Definition
func (cd *ContainerDef) UnmarshalYAML(unmarshal func(interface{}) error) error {
	type rawContainerDef ContainerDef
	raw := rawContainerDef{Essential: true} //  If essential is not specified, we want it to be true
	if err := unmarshal(&raw); err != nil {
		return err
	}

	*cd = ContainerDef(raw)
	return nil
}

// ReadECSParams parses the ecs-params.yml file and puts it into an ECSParams struct.
func ReadECSParams(filename string) (*ECSParams, error) {
	if filename == "" {
		defaultFilename := "ecs-params.yml"
		if _, err := os.Stat(defaultFilename); err == nil {
			filename = defaultFilename
		} else {
			return nil, nil
		}
	}

	// NOTE: Readfile reads all data into memory and closes file. Could
	// eventually refactor this to read different sections separately.
	ecsParamsData, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, errors.Wrapf(err, "Error reading file '%v'", filename)
	}
	ecsParamsData = []byte(os.ExpandEnv(string(ecsParamsData)))
	ecsParams := &ECSParams{}

	if err = yaml.Unmarshal([]byte(ecsParamsData), &ecsParams); err != nil {
		return nil, errors.Wrapf(err, "Error unmarshalling yaml data from ECS params file: %v", filename)
	}

	return ecsParams, nil
}

/////////////////////
//// Converters ////
////////////////////

// ConvertToECSNetworkConfiguration extracts out the NetworkConfiguration from
// the ECSParams into a format that is compatible with ECSClient calls.
func ConvertToECSNetworkConfiguration(ecsParams *ECSParams) (*ecs.NetworkConfiguration, error) {
	if ecsParams == nil {
		return nil, nil
	}

	networkMode := ecsParams.TaskDefinition.NetworkMode

	if networkMode != "awsvpc" {
		return nil, nil
	}

	awsvpcConfig := ecsParams.RunParams.NetworkConfiguration.AwsVpcConfiguration

	subnets := awsvpcConfig.Subnets

	if len(subnets) < 1 {
		return nil, errors.New("at least one subnet is required in the network configuration")
	}

	securityGroups := awsvpcConfig.SecurityGroups
	assignPublicIp := string(awsvpcConfig.AssignPublicIp)

	ecsSubnets := make([]*string, len(subnets))
	for i, subnet := range subnets {
		ecsSubnets[i] = aws.String(subnet)
	}

	ecsSecurityGroups := make([]*string, len(securityGroups))
	for i, sg := range securityGroups {
		ecsSecurityGroups[i] = aws.String(sg)
	}

	ecsAwsVpcConfig := &ecs.AwsVpcConfiguration{
		Subnets:        ecsSubnets,
		SecurityGroups: ecsSecurityGroups,
	}

	// For tasks launched with network config in EC2 mode, assign_pubic_ip field is not accepted
	if assignPublicIp != "" {
		ecsAwsVpcConfig.AssignPublicIp = aws.String(assignPublicIp)
	}

	ecsNetworkConfig := &ecs.NetworkConfiguration{
		AwsvpcConfiguration: ecsAwsVpcConfig,
	}

	return ecsNetworkConfig, nil
}

// ConvertToECSHealthCheck extracts out the HealthCheck from the ECSParams into
// a format that is compatible with ECSClient calls.
func (h *HealthCheck) ConvertToECSHealthCheck() (*ecs.HealthCheck, error) {
	ecsHealthCheck := &ecs.HealthCheck{}
	if len(h.Command) > 0 && len(h.Test) > 0 {
		return nil, fmt.Errorf("healthcheck.test and healthcheck.command can not both be specified")
	}

	if len(h.Command) > 0 {
		ecsHealthCheck.Command = aws.StringSlice(getHealthCheckCommand(h.Command))
	}

	if len(h.Test) > 0 {
		ecsHealthCheck.Command = aws.StringSlice(getHealthCheckCommand(h.Test))
	}

	if h.Retries != 0 {
		ecsHealthCheck.Retries = &h.Retries
	}

	timeout, err := parseHealthCheckTime(h.Timeout)
	if err != nil {
		return ecsHealthCheck, err
	}
	ecsHealthCheck.Timeout = timeout

	startPeriod, err := parseHealthCheckTime(h.StartPeriod)
	if err != nil {
		return ecsHealthCheck, err
	}
	ecsHealthCheck.StartPeriod = startPeriod

	interval, err := parseHealthCheckTime(h.Interval)
	if err != nil {
		return ecsHealthCheck, err
	}
	ecsHealthCheck.Interval = interval

	return ecsHealthCheck, nil
}

// parses the command/test field for healthcheck
func getHealthCheckCommand(command []string) []string {
	if len(command) == 1 {
		// command/test was specified as a single string which wraps it in /bin/sh (CMD-SHELL)
		command = append([]string{"CMD-SHELL"}, command...)
	}
	return command
}

// parses a health check time string which could be a duration or an integer
func parseHealthCheckTime(field string) (*int64, error) {
	if field != "" {
		duration, err := time.ParseDuration(field)
		if err == nil {
			return adapter.ConvertToTimeInSeconds(&duration), nil
		} else if val, err := strconv.ParseInt(field, 10, 64); err == nil {
			return &val, nil
		} else {
			return nil, fmt.Errorf("Could not parse %s either as an integer or a duration (ex: 1m30s)", field)
		}
	}

	return nil, nil
}

// ConvertToECSPlacementConstraint converts a list of Constraints specified in the
// ecs-params into a format that is compatible with ECSClient calls.
func ConvertToECSPlacementConstraints(ecsParams *ECSParams) ([]*ecs.PlacementConstraint, error) {
	if ecsParams == nil {
		return nil, nil
	}

	constraints := ecsParams.RunParams.TaskPlacement.Constraints

	output := []*ecs.PlacementConstraint{}
	for _, constraint := range constraints {
		ecsConstraint := &ecs.PlacementConstraint{
			Type: aws.String(constraint.Type),
		}
		if constraint.Expression != "" {
			ecsConstraint.Expression = aws.String(constraint.Expression)
		}
		output = append(output, ecsConstraint)
	}

	return output, nil
}

// ConvertToECSPlacementStrategy converts a list of Strategies specified in the
// ecs-params into a format that is compatible with ECSClient calls.
func ConvertToECSPlacementStrategy(ecsParams *ECSParams) ([]*ecs.PlacementStrategy, error) {
	if ecsParams == nil {
		return nil, nil
	}
	strategies := ecsParams.RunParams.TaskPlacement.Strategies

	output := []*ecs.PlacementStrategy{}
	for _, strategy := range strategies {
		ecsStrategy := &ecs.PlacementStrategy{
			Type: aws.String(strategy.Type),
		}
		if strategy.Field != "" {
			ecsStrategy.Field = aws.String(strategy.Field)
		}
		output = append(output, ecsStrategy)
	}

	return output, nil
}
