// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package stack

import (
	"encoding/json"
	"fmt"
	"hash/crc32"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/aws/copilot-cli/internal/pkg/deploy/upload/customresource"

	"github.com/aws/copilot-cli/internal/pkg/aws/partitions"
	"github.com/aws/copilot-cli/internal/pkg/deploy"
	"github.com/aws/copilot-cli/internal/pkg/template/override"

	"github.com/aws/copilot-cli/internal/pkg/aws/s3"

	"github.com/aws/aws-sdk-go/aws"

	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/aws/copilot-cli/internal/pkg/template"
)

const (
	enabled  = "ENABLED"
	disabled = "DISABLED"
)

// SQS Queue field values.
const (
	sqsDedupeScopeMessageGroup              = "messageGroup"
	sqsFIFOThroughputLimitPerMessageGroupId = "perMessageGroupId"
)

// Default values for EFS options
const (
	defaultRootDirectory   = "/"
	defaultIAM             = disabled
	defaultReadOnly        = true
	defaultWritePermission = false
	defaultNLBProtocol     = manifest.TCP
)

// Supported capacityproviders for Fargate services
const (
	capacityProviderFargateSpot = "FARGATE_SPOT"
	capacityProviderFargate     = "FARGATE"
)

// MinimumHealthyPercent and MaximumPercent configurations as per deployment strategy.
const (
	minHealthyPercentRecreate = 0
	maxPercentRecreate        = 100
	minHealthyPercentDefault  = 100
	maxPercentDefault         = 200
)

var (
	taskDefOverrideRulePrefixes = []string{"Resources", "TaskDefinition", "Properties"}
	subnetPlacementForTemplate  = map[manifest.PlacementString]string{
		manifest.PrivateSubnetPlacement: template.PrivateSubnetsPlacement,
		manifest.PublicSubnetPlacement:  template.PublicSubnetsPlacement,
	}
)

func convertPortMappings(exposedPorts []manifest.ExposedPort) []*template.PortMapping {
	portMapping := make([]*template.PortMapping, len(exposedPorts))
	for idx, exposedPort := range exposedPorts {
		portMapping[idx] = &template.PortMapping{
			ContainerPort: exposedPort.Port,
			Protocol:      exposedPort.Protocol,
			ContainerName: exposedPort.ContainerName,
		}
	}
	return portMapping
}

// convertSidecars converts the manifest sidecar configuration into a format parsable by the templates pkg.
func convertSidecars(s map[string]*manifest.SidecarConfig, exposedPorts map[string][]manifest.ExposedPort, rc RuntimeConfig) ([]*template.SidecarOpts, error) {
	var sidecars []*template.SidecarOpts
	if s == nil {
		return nil, nil
	}

	// Sort the sidecars so that the order is consistent and the integration test won't be flaky.
	keys := make([]string, 0, len(s))
	for k := range s {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, name := range keys {
		config := s[name]
		var imageURI string
		if image, ok := rc.PushedImages[name]; ok {
			imageURI = image.URI()
		}
		if uri, hasLocation := config.ImageURI(); hasLocation {
			imageURI = uri
		}
		entrypoint, err := convertEntryPoint(config.EntryPoint)
		if err != nil {
			return nil, err
		}
		command, err := convertCommand(config.Command)
		if err != nil {
			return nil, err
		}
		mp := convertSidecarMountPoints(config.MountPoints)
		sidecars = append(sidecars, &template.SidecarOpts{
			Name:       name,
			Image:      aws.String(imageURI),
			Essential:  config.Essential,
			CredsParam: config.CredsParam,
			Secrets:    convertSecrets(config.Secrets),
			Variables:  convertEnvVars(config.Variables),
			Storage: template.SidecarStorageOpts{
				MountPoints: mp,
			},
			DockerLabels: config.DockerLabels,
			DependsOn:    convertDependsOn(config.DependsOn),
			EntryPoint:   entrypoint,
			HealthCheck:  convertContainerHealthCheck(config.HealthCheck),
			Command:      command,
			PortMappings: convertPortMappings(exposedPorts[name]),
		})
	}
	return sidecars, nil
}

func convertContainerHealthCheck(hc manifest.ContainerHealthCheck) *template.ContainerHealthCheck {
	if hc.IsEmpty() {
		return nil
	}
	// Make sure that unset fields in the healthcheck gets a default value.
	hc.ApplyIfNotSet(manifest.NewDefaultContainerHealthCheck())
	return &template.ContainerHealthCheck{
		Command:     hc.Command,
		Interval:    aws.Int64(int64(hc.Interval.Seconds())),
		Retries:     aws.Int64(int64(aws.IntValue(hc.Retries))),
		StartPeriod: aws.Int64(int64(hc.StartPeriod.Seconds())),
		Timeout:     aws.Int64(int64(hc.Timeout.Seconds())),
	}
}

func convertHostedZone(alias manifest.Alias, defaultHostedZone *string) (template.AliasesForHostedZone, error) {
	aliasesFor := make(map[string][]string)
	if len(alias.AdvancedAliases) != 0 {
		for _, alias := range alias.AdvancedAliases {
			if alias.HostedZone != nil {
				if isDuplicateAliasEntry(aliasesFor[*alias.HostedZone], aws.StringValue(alias.Alias)) {
					continue
				}
				aliasesFor[*alias.HostedZone] = append(aliasesFor[*alias.HostedZone], *alias.Alias)
				continue
			}
			if defaultHostedZone != nil {
				if isDuplicateAliasEntry(aliasesFor[*defaultHostedZone], aws.StringValue(alias.Alias)) {
					continue
				}
				aliasesFor[*defaultHostedZone] = append(aliasesFor[*defaultHostedZone], *alias.Alias)
			}
		}
		return aliasesFor, nil
	}
	if defaultHostedZone == nil {
		return aliasesFor, nil
	}
	aliases, err := alias.ToStringSlice()
	if err != nil {
		return nil, err
	}

	for _, alias := range aliases {
		if isDuplicateAliasEntry(aliasesFor[*defaultHostedZone], alias) {
			continue
		}
		aliasesFor[*defaultHostedZone] = append(aliasesFor[*defaultHostedZone], alias)
	}

	return aliasesFor, nil
}

func isDuplicateAliasEntry(aliasList []string, alias string) bool {
	for _, entry := range aliasList {
		if entry == alias {
			return true
		}
	}
	return false
}

// convertDependsOn converts image and sidecar depends on fields to have upper case statuses.
func convertDependsOn(d manifest.DependsOn) map[string]string {
	if d == nil {
		return nil
	}
	dependsOn := make(map[string]string)
	for name, status := range d {
		dependsOn[name] = strings.ToUpper(status)
	}
	return dependsOn
}

func convertAdvancedCount(a manifest.AdvancedCount) (*template.AdvancedCount, error) {
	if a.IsEmpty() {
		return nil, nil
	}
	autoscaling, err := convertAutoscaling(a)
	if err != nil {
		return nil, err
	}
	return &template.AdvancedCount{
		Spot:        a.Spot,
		Autoscaling: autoscaling,
		Cps:         convertCapacityProviders(a),
	}, nil
}

// convertCapacityProviders transforms the manifest fields into a format
// parsable by the templates pkg.
func convertCapacityProviders(a manifest.AdvancedCount) []*template.CapacityProviderStrategy {
	if a.Spot == nil && a.Range.RangeConfig.SpotFrom == nil {
		return nil
	}
	var cps []*template.CapacityProviderStrategy
	// if Spot specified as count, then weight on Spot CPS should be 1
	cps = append(cps, &template.CapacityProviderStrategy{
		Weight:           aws.Int(1),
		CapacityProvider: capacityProviderFargateSpot,
	})
	rc := a.Range.RangeConfig
	// Return if only spot is specified as count
	if rc.SpotFrom == nil {
		return cps
	}
	// Scaling with spot
	spotFrom := aws.IntValue(rc.SpotFrom)
	min := aws.IntValue(rc.Min)
	// If spotFrom value is greater than or equal to the autoscaling min,
	// then the base value on the Fargate capacity provider must be set
	// to one less than spotFrom
	if spotFrom >= min {
		base := spotFrom - 1
		if base < 0 {
			base = 0
		}
		fgCapacity := &template.CapacityProviderStrategy{
			Base:             aws.Int(base),
			Weight:           aws.Int(0),
			CapacityProvider: capacityProviderFargate,
		}
		cps = append(cps, fgCapacity)
	}
	return cps
}

// convertCooldown converts a service manifest cooldown struct into a format parsable
// by the templates pkg.
func convertCooldown(c manifest.Cooldown) template.Cooldown {
	if c.IsEmpty() {
		return template.Cooldown{}
	}

	cooldown := template.Cooldown{}

	if c.ScaleInCooldown != nil {
		scaleInTime := float64(*c.ScaleInCooldown) / float64(time.Second)
		cooldown.ScaleInCooldown = aws.Float64(scaleInTime)
	}
	if c.ScaleOutCooldown != nil {
		scaleOutTime := float64(*c.ScaleOutCooldown) / float64(time.Second)
		cooldown.ScaleOutCooldown = aws.Float64(scaleOutTime)
	}

	return cooldown
}

// convertScalingCooldown handles the logic of converting generalized and specific cooldowns set
// into the scaling cooldown used in the Auto Scaling configuration.
func convertScalingCooldown(specCooldown, genCooldown manifest.Cooldown) template.Cooldown {
	cooldown := convertCooldown(genCooldown)

	specTemplateCooldown := convertCooldown(specCooldown)
	if specCooldown.ScaleInCooldown != nil {
		cooldown.ScaleInCooldown = specTemplateCooldown.ScaleInCooldown
	}
	if specCooldown.ScaleOutCooldown != nil {
		cooldown.ScaleOutCooldown = specTemplateCooldown.ScaleOutCooldown
	}

	return cooldown
}

// convertAutoscaling converts the service's Auto Scaling configuration into a format parsable
// by the templates pkg.
func convertAutoscaling(a manifest.AdvancedCount) (*template.AutoscalingOpts, error) {
	if a.IsEmpty() {
		return nil, nil
	}
	if a.Spot != nil {
		return nil, nil
	}

	min, max, err := a.Range.Parse()
	if err != nil {
		return nil, err
	}
	autoscalingOpts := template.AutoscalingOpts{
		MinCapacity: &min,
		MaxCapacity: &max,
	}

	if a.CPU.Value != nil {
		autoscalingOpts.CPU = aws.Float64(float64(*a.CPU.Value))
	}
	if a.CPU.ScalingConfig.Value != nil {
		autoscalingOpts.CPU = aws.Float64(float64(*a.CPU.ScalingConfig.Value))
	}
	if a.Memory.Value != nil {
		autoscalingOpts.Memory = aws.Float64(float64(*a.Memory.Value))
	}
	if a.Memory.ScalingConfig.Value != nil {
		autoscalingOpts.Memory = aws.Float64(float64(*a.Memory.ScalingConfig.Value))
	}
	if a.Requests.Value != nil {
		autoscalingOpts.Requests = aws.Float64(float64(*a.Requests.Value))
	}
	if a.Requests.ScalingConfig.Value != nil {
		autoscalingOpts.Requests = aws.Float64(float64(*a.Requests.ScalingConfig.Value))
	}
	if a.ResponseTime.Value != nil {
		responseTime := float64(*a.ResponseTime.Value) / float64(time.Second)
		autoscalingOpts.ResponseTime = aws.Float64(responseTime)
	}
	if a.ResponseTime.ScalingConfig.Value != nil {
		responseTime := float64(*a.ResponseTime.ScalingConfig.Value) / float64(time.Second)
		autoscalingOpts.ResponseTime = aws.Float64(responseTime)
	}

	autoscalingOpts.CPUCooldown = convertScalingCooldown(a.CPU.ScalingConfig.Cooldown, a.Cooldown)
	autoscalingOpts.MemCooldown = convertScalingCooldown(a.Memory.ScalingConfig.Cooldown, a.Cooldown)
	autoscalingOpts.ReqCooldown = convertScalingCooldown(a.Requests.ScalingConfig.Cooldown, a.Cooldown)
	autoscalingOpts.RespTimeCooldown = convertScalingCooldown(a.ResponseTime.ScalingConfig.Cooldown, a.Cooldown)
	autoscalingOpts.QueueDelayCooldown = convertScalingCooldown(a.QueueScaling.Cooldown, a.Cooldown)

	if !a.QueueScaling.IsEmpty() {
		acceptableBacklog, err := a.QueueScaling.AcceptableBacklogPerTask()
		if err != nil {
			return nil, err
		}
		autoscalingOpts.QueueDelay = &template.AutoscalingQueueDelayOpts{
			AcceptableBacklogPerTask: acceptableBacklog,
		}
	}
	return &autoscalingOpts, nil
}

// convertHTTPHealthCheck converts the ALB health check configuration into a format parsable by the templates pkg.
func convertHTTPHealthCheck(hc *manifest.HealthCheckArgsOrString) template.HTTPHealthCheckOpts {
	opts := template.HTTPHealthCheckOpts{
		HealthCheckPath:    manifest.DefaultHealthCheckPath,
		GracePeriod:        manifest.DefaultHealthCheckGracePeriod,
		HealthyThreshold:   hc.Advanced.HealthyThreshold,
		UnhealthyThreshold: hc.Advanced.UnhealthyThreshold,
		SuccessCodes:       aws.StringValue(hc.Advanced.SuccessCodes),
	}

	if hc.IsZero() {
		return opts
	}
	if hc.IsBasic() {
		opts.HealthCheckPath = convertPath(hc.Basic)
		return opts
	}

	if hc.Advanced.Path != nil {
		opts.HealthCheckPath = convertPath(*hc.Advanced.Path)
	}
	if hc.Advanced.Port != nil {
		opts.Port = strconv.Itoa(aws.IntValue(hc.Advanced.Port))
	}
	if hc.Advanced.Interval != nil {
		opts.Interval = aws.Int64(int64(hc.Advanced.Interval.Seconds()))
	}
	if hc.Advanced.Timeout != nil {
		opts.Timeout = aws.Int64(int64(hc.Advanced.Timeout.Seconds()))
	}
	if hc.Advanced.GracePeriod != nil {
		opts.GracePeriod = int64(hc.Advanced.GracePeriod.Seconds())
	}
	return opts
}

// convertNLBHealthCheck converts the NLB health check configuration into a format parsable by the templates pkg.
func convertNLBHealthCheck(nlbHC *manifest.NLBHealthCheckArgs) template.NLBHealthCheck {
	hc := template.NLBHealthCheck{
		HealthyThreshold:   nlbHC.HealthyThreshold,
		UnhealthyThreshold: nlbHC.UnhealthyThreshold,
		GracePeriod:        aws.Int64(int64(manifest.DefaultHealthCheckGracePeriod)),
	}
	if nlbHC.Port != nil {
		hc.Port = strconv.Itoa(aws.IntValue(nlbHC.Port))
	}
	if nlbHC.Timeout != nil {
		hc.Timeout = aws.Int64(int64(nlbHC.Timeout.Seconds()))
	}
	if nlbHC.Interval != nil {
		hc.Interval = aws.Int64(int64(nlbHC.Interval.Seconds()))
	}
	if nlbHC.GracePeriod != nil {
		hc.GracePeriod = aws.Int64(int64(nlbHC.GracePeriod.Seconds()))
	}
	return hc
}

type networkLoadBalancerConfig struct {
	settings *template.NetworkLoadBalancer

	// If a domain is associated these values are not empty.
	appDNSDelegationRole *string
	appDNSName           *string
}

func convertELBAccessLogsConfig(mft *manifest.Environment) *template.ELBAccessLogs {
	elbAccessLogsArgs, isELBAccessLogsSet := mft.ELBAccessLogs()
	if !isELBAccessLogsSet {
		return nil
	}

	if elbAccessLogsArgs == nil {
		return &template.ELBAccessLogs{}
	}

	return &template.ELBAccessLogs{
		BucketName: aws.StringValue(elbAccessLogsArgs.BucketName),
		Prefix:     aws.StringValue(elbAccessLogsArgs.Prefix),
	}
}

// convertFlowLogsConfig converts the VPC FlowLog configuration into a format parsable by the templates pkg.
func convertFlowLogsConfig(mft *manifest.Environment) (*template.VPCFlowLogs, error) {
	vpcFlowLogs := mft.EnvironmentConfig.Network.VPC.FlowLogs
	if vpcFlowLogs.IsZero() {
		return nil, nil
	}
	retentionInDays := aws.Int(14)
	if vpcFlowLogs.Advanced.Retention != nil {
		retentionInDays = vpcFlowLogs.Advanced.Retention
	}
	return &template.VPCFlowLogs{
		Retention: retentionInDays,
	}, nil

}

func convertEnvSecurityGroupCfg(mft *manifest.Environment) (*template.SecurityGroupConfig, error) {
	securityGroupConfig, isSecurityConfigSet := mft.EnvSecurityGroup()
	if !isSecurityConfigSet {
		return nil, nil
	}
	var ingress = make([]template.SecurityGroupRule, len(securityGroupConfig.Ingress))
	var egress = make([]template.SecurityGroupRule, len(securityGroupConfig.Egress))
	for idx, ingressValue := range securityGroupConfig.Ingress {
		ingress[idx].IpProtocol = ingressValue.IpProtocol
		ingress[idx].CidrIP = ingressValue.CidrIP
		if fromPort, toPort, err := ingressValue.GetPorts(); err != nil {
			return nil, err
		} else {
			ingress[idx].ToPort = toPort
			ingress[idx].FromPort = fromPort
		}
	}
	for idx, egressValue := range securityGroupConfig.Egress {
		egress[idx].IpProtocol = egressValue.IpProtocol
		egress[idx].CidrIP = egressValue.CidrIP
		if fromPort, toPort, err := egressValue.GetPorts(); err != nil {
			return nil, err
		} else {
			egress[idx].ToPort = toPort
			egress[idx].FromPort = fromPort
		}
	}
	return &template.SecurityGroupConfig{
		Ingress: ingress,
		Egress:  egress,
	}, nil
}

func (s *LoadBalancedWebService) convertALBListener() (*template.ALBListener, error) {
	rrConfig := s.manifest.HTTPOrBool
	if rrConfig.Disabled() || rrConfig.IsEmpty() {
		return nil, nil
	}
	var rules []template.ALBListenerRule
	var aliasesFor template.AliasesForHostedZone
	for _, routingRule := range rrConfig.RoutingRules() {
		httpRedirect := true
		if routingRule.RedirectToHTTPS != nil {
			httpRedirect = aws.BoolValue(routingRule.RedirectToHTTPS)
		}
		rule, err := routingRuleConfigConverter{
			rule:            routingRule,
			manifest:        s.manifest,
			httpsEnabled:    s.httpsEnabled,
			redirectToHTTPS: httpRedirect,
		}.convert()
		if err != nil {
			return nil, err
		}
		rules = append(rules, *rule)
		aliasesFor, err = convertHostedZone(rrConfig.Main.Alias, rrConfig.Main.HostedZone)
		if err != nil {
			return nil, err
		}
	}
	return &template.ALBListener{
		Rules:             rules,
		IsHTTPS:           s.httpsEnabled,
		HostedZoneAliases: aliasesFor,
	}, nil
}

func (s *BackendService) convertALBListener() (*template.ALBListener, error) {
	rrConfig := s.manifest.HTTP
	if rrConfig.IsEmpty() {
		return nil, nil
	}
	var rules []template.ALBListenerRule
	var hostedZoneAliases template.AliasesForHostedZone
	for _, routingRule := range rrConfig.RoutingRules() {
		rule, err := routingRuleConfigConverter{
			rule:            routingRule,
			manifest:        s.manifest,
			httpsEnabled:    s.httpsEnabled,
			redirectToHTTPS: s.httpsEnabled,
		}.convert()
		if err != nil {
			return nil, err
		}
		rules = append(rules, *rule)
		hostedZoneAliases, err = convertHostedZone(rrConfig.Main.Alias, rrConfig.Main.HostedZone)
		if err != nil {
			return nil, err
		}
	}

	return &template.ALBListener{
		Rules:             rules,
		IsHTTPS:           s.httpsEnabled,
		MainContainerPort: s.manifest.MainContainerPort(),
		HostedZoneAliases: hostedZoneAliases,
	}, nil
}

func (s *BackendService) convertGracePeriod() *int64 {
	if s.manifest.HTTP.Main.HealthCheck.Advanced.GracePeriod != nil {
		return aws.Int64(int64(s.manifest.HTTP.Main.HealthCheck.Advanced.GracePeriod.Seconds()))
	}
	return aws.Int64(int64(manifest.DefaultHealthCheckGracePeriod))
}

func (s *BackendService) convertImportedALB() (*template.ImportedALB, error) {
	if s.importedALB == nil {
		return nil, nil
	}
	var listeners []template.LBListener
	for _, listener := range s.importedALB.Listeners {
		listeners = append(listeners, template.LBListener{
			ARN:      listener.ARN,
			Port:     listener.Port,
			Protocol: listener.Protocol,
		})
	}
	var securityGroups []template.LBSecurityGroup
	for _, sg := range s.importedALB.SecurityGroups {
		securityGroups = append(securityGroups, template.LBSecurityGroup{
			ID: sg,
		})
	}
	return &template.ImportedALB{
		Name:           s.importedALB.Name,
		ARN:            s.importedALB.ARN,
		DNSName:        s.importedALB.DNSName,
		HostedZoneID:   s.importedALB.HostedZoneID,
		Listeners:      listeners,
		SecurityGroups: securityGroups,
	}, nil
}

type loadBalancerTargeter interface {
	MainContainerPort() string
	ExposedPorts() (manifest.ExposedPortsIndex, error)
}

type routingRuleConfigConverter struct {
	rule            manifest.RoutingRule
	manifest        loadBalancerTargeter
	httpsEnabled    bool
	redirectToHTTPS bool
}

// convertPath attempts to standardize manifest paths on '/path' or '/' patterns.
//   - If the path starts with a / (including '/'), return it unmodified.
//   - Otherwise, prepend a leading '/' character.
//
// CFN health check and path patterns expect a leading '/', so we do that here instead of in the template.
//
// Empty strings, if they make it to this point, are converted to '/'.
func convertPath(path string) string {
	if path == "" {
		return "/"
	}
	if path[0] == '/' {
		return path
	}
	return "/" + path
}

func (conv routingRuleConfigConverter) convert() (*template.ALBListenerRule, error) {
	var aliases []string
	var err error

	exposedPorts, err := conv.manifest.ExposedPorts()
	if err != nil {
		return nil, err
	}
	targetContainer, targetPort, err := conv.rule.Target(exposedPorts)
	if err != nil {
		return nil, err
	}

	if conv.httpsEnabled {
		aliases, err = convertAlias(conv.rule.Alias)
		if err != nil {
			return nil, err
		}
	}

	config := &template.ALBListenerRule{
		Path:                convertPath(aws.StringValue(conv.rule.Path)),
		TargetContainer:     targetContainer,
		TargetPort:          targetPort,
		Aliases:             aliases,
		HTTPHealthCheck:     convertHTTPHealthCheck(&conv.rule.HealthCheck),
		AllowedSourceIps:    convertAllowedSourceIPs(conv.rule.AllowedSourceIps),
		Stickiness:          strconv.FormatBool(aws.BoolValue(conv.rule.Stickiness)),
		HTTPVersion:         aws.StringValue(convertHTTPVersion(conv.rule.ProtocolVersion)),
		RedirectToHTTPS:     conv.redirectToHTTPS,
		DeregistrationDelay: convertDeregistrationDelay(conv.rule.DeregistrationDelay),
	}
	return config, nil
}

func convertDeregistrationDelay(delay *time.Duration) *int64 {
	if delay == nil {
		return aws.Int64(int64(manifest.DefaultDeregistrationDelay))
	}
	return aws.Int64(int64(delay.Seconds()))
}

type nlbListeners []template.NetworkLoadBalancerListener

// isCertRequired returns true if any of the NLB listeners have protocol as TLS set.
func (ls nlbListeners) isCertRequired() bool {
	for _, listener := range ls {
		if listener.Protocol == manifest.TLS {
			return true
		}
	}
	return false
}

func (s *LoadBalancedWebService) convertNetworkLoadBalancer() (networkLoadBalancerConfig, error) {
	nlbConfig := s.manifest.NLBConfig
	if nlbConfig.IsEmpty() {
		return networkLoadBalancerConfig{}, nil
	}
	exposedPorts, err := s.manifest.ExposedPorts()
	if err != nil {
		return networkLoadBalancerConfig{}, err
	}
	listeners := make(nlbListeners, len(nlbConfig.NLBListeners()))
	for idx, listener := range nlbConfig.NLBListeners() {
		// Parse targetContainer and targetPort for the Network Load Balancer targets.
		targetContainer, targetPort, err := listener.Target(exposedPorts)
		if err != nil {
			return networkLoadBalancerConfig{}, err
		}

		// Parse listener port and protocol.
		port, protocol, err := manifest.ParsePortMapping(listener.Port)
		if err != nil {
			return networkLoadBalancerConfig{}, err
		}

		if protocol == nil {
			protocol = aws.String(defaultNLBProtocol)
		}

		listeners[idx] = template.NetworkLoadBalancerListener{
			Port:                aws.StringValue(port),
			Protocol:            strings.ToUpper(aws.StringValue(protocol)),
			TargetContainer:     targetContainer,
			TargetPort:          targetPort,
			SSLPolicy:           listener.SSLPolicy,
			HealthCheck:         convertNLBHealthCheck(&listener.HealthCheck),
			Stickiness:          listener.Stickiness,
			DeregistrationDelay: convertDeregistrationDelay(listener.DeregistrationDelay),
		}
	}

	aliases, err := convertAlias(nlbConfig.Aliases)
	if err != nil {
		return networkLoadBalancerConfig{}, fmt.Errorf(`convert "nlb.alias" to string slice: %w`, err)
	}

	config := networkLoadBalancerConfig{
		settings: &template.NetworkLoadBalancer{
			Listener:            listeners,
			Aliases:             aliases,
			MainContainerPort:   s.manifest.MainContainerPort(),
			CertificateRequired: listeners.isCertRequired(),
		},
	}

	if s.dnsDelegationEnabled {
		dnsDelegationRole, dnsName := convertAppInformation(s.appInfo)
		config.appDNSName = dnsName
		config.appDNSDelegationRole = dnsDelegationRole
	}
	return config, nil
}

func (s *LoadBalancedWebService) convertGracePeriod() *int64 {
	if s.manifest.HTTPOrBool.Main.HealthCheck.Advanced.GracePeriod != nil {
		return aws.Int64(int64(s.manifest.HTTPOrBool.Main.HealthCheck.Advanced.GracePeriod.Seconds()))
	}
	if s.manifest.NLBConfig.Listener.HealthCheck.GracePeriod != nil {
		return aws.Int64(int64(s.manifest.NLBConfig.Listener.HealthCheck.GracePeriod.Seconds()))
	}
	return aws.Int64(int64(manifest.DefaultHealthCheckGracePeriod))
}

func (s *LoadBalancedWebService) convertImportedALB() (*template.ImportedALB, error) {
	if s.importedALB == nil {
		return nil, nil
	}
	var listeners []template.LBListener
	for _, listener := range s.importedALB.Listeners {
		listeners = append(listeners, template.LBListener{
			ARN:      listener.ARN,
			Port:     listener.Port,
			Protocol: listener.Protocol,
		})
	}
	var securityGroups []template.LBSecurityGroup
	for _, sg := range s.importedALB.SecurityGroups {
		securityGroups = append(securityGroups, template.LBSecurityGroup{
			ID: sg,
		})
	}
	return &template.ImportedALB{
		Name:           s.importedALB.Name,
		ARN:            s.importedALB.ARN,
		DNSName:        s.importedALB.DNSName,
		HostedZoneID:   s.importedALB.HostedZoneID,
		Listeners:      listeners,
		SecurityGroups: securityGroups,
	}, nil
}

func convertExecuteCommand(e *manifest.ExecuteCommand) *template.ExecuteCommandOpts {
	if e.Config.IsEmpty() && !aws.BoolValue(e.Enable) {
		return nil
	}
	return &template.ExecuteCommandOpts{}
}

func convertAllowedSourceIPs(allowedSourceIPs []manifest.IPNet) []string {
	var sourceIPs []string
	for _, ipNet := range allowedSourceIPs {
		sourceIPs = append(sourceIPs, string(ipNet))
	}
	return sourceIPs
}

func convertServiceConnectServer(s manifest.ServiceConnectBoolOrArgs, target *manifest.ServiceConnectTargetContainer) *template.ServiceConnectServer {
	if target == nil || target.Port == "" || target.Port == template.NoExposedContainerPort {
		return nil
	}

	return &template.ServiceConnectServer{
		Name:  target.Container,
		Port:  target.Port,
		Alias: aws.StringValue(s.Alias),
	}
}

func convertLogging(lc manifest.Logging) *template.LogConfigOpts {
	if lc.IsEmpty() {
		return nil
	}
	return &template.LogConfigOpts{
		Image:          lc.LogImage(),
		ConfigFile:     lc.ConfigFile,
		EnableMetadata: lc.GetEnableMetadata(),
		Destination:    lc.Destination,
		SecretOptions:  convertSecrets(lc.SecretOptions),
		Variables:      convertEnvVars(lc.Variables),
		Secrets:        convertSecrets(lc.Secrets),
	}
}

func convertTaskDefOverrideRules(inRules []manifest.OverrideRule) []override.Rule {
	var res []override.Rule
	suffixStr := strings.Join(taskDefOverrideRulePrefixes, override.PathSegmentSeparator)
	for _, r := range inRules {
		res = append(res, override.Rule{
			Path:  strings.Join([]string{suffixStr, r.Path}, override.PathSegmentSeparator),
			Value: r.Value,
		})
	}
	return res
}

// convertStorageOpts converts a manifest Storage field into template data structures which can be used
// to execute CFN templates
func convertStorageOpts(wlName *string, in manifest.Storage) *template.StorageOpts {
	if in.IsEmpty() {
		return nil
	}
	return &template.StorageOpts{
		Ephemeral:         convertEphemeral(in.Ephemeral),
		ReadonlyRootFS:    in.ReadonlyRootFS,
		Volumes:           convertVolumes(in.Volumes),
		MountPoints:       convertMountPoints(in.Volumes),
		EFSPerms:          convertEFSPermissions(in.Volumes),
		ManagedVolumeInfo: convertManagedFSInfo(wlName, in.Volumes),
	}
}

func convertEphemeral(in *int) *int {
	// Min value for extensible ephemeral storage is 21; if customer specifies 20, which is the default size,
	// we shouldn't let CF error out. Instead, we'll just omit it from the config.
	if aws.IntValue(in) == 20 {
		return nil
	}
	return in
}

// convertSidecarMountPoints is used to convert from manifest to template objects.
func convertSidecarMountPoints(in []manifest.SidecarMountPoint) []*template.MountPoint {
	if len(in) == 0 {
		return nil
	}
	var output []*template.MountPoint
	for _, smp := range in {
		output = append(output, convertMountPoint(smp.SourceVolume, smp.ContainerPath, smp.ReadOnly))
	}
	return output
}

func convertMountPoint(sourceVolume, containerPath *string, readOnly *bool) *template.MountPoint {
	// readOnly defaults to true.
	oReadOnly := aws.Bool(defaultReadOnly)
	if readOnly != nil {
		oReadOnly = readOnly
	}
	return &template.MountPoint{
		ReadOnly:      oReadOnly,
		ContainerPath: containerPath,
		SourceVolume:  sourceVolume,
	}
}

func convertMountPoints(input map[string]*manifest.Volume) []*template.MountPoint {
	if len(input) == 0 {
		return nil
	}

	// Sort by names for consistent testing
	var names []string
	for name := range input {
		names = append(names, name)
	}
	sort.Strings(names)

	var output []*template.MountPoint
	for _, name := range names {
		volume := input[name]
		output = append(output, convertMountPoint(aws.String(name), volume.ContainerPath, volume.ReadOnly))
	}
	return output
}

func convertEFSPermissions(input map[string]*manifest.Volume) []*template.EFSPermission {
	// Sort by names for consistent testing
	var names []string
	for name := range input {
		names = append(names, name)
	}
	sort.Strings(names)

	var output []*template.EFSPermission
	for _, name := range names {
		volume := input[name]
		// If there's no EFS configuration, we don't need to generate any permissions.
		if volume.EmptyVolume() {
			continue
		}
		// If EFS is explicitly disabled, we don't need to generate permisisons.
		if volume.EFS.Disabled() {
			continue
		}
		// Managed FS permissions are rendered separately in the template.
		if volume.EFS.UseManagedFS() {
			continue
		}

		// Write defaults to false.
		write := defaultWritePermission
		if volume.ReadOnly != nil {
			write = !aws.BoolValue(volume.ReadOnly)
		}
		accessPointID := volume.EFS.Advanced.AuthConfig.AccessPointID
		output = append(output, &template.EFSPermission{
			Write:         write,
			AccessPointID: accessPointID,
			FilesystemID:  convertFileSystemID(volume.EFS.Advanced),
		})
	}
	return output
}

func convertManagedFSInfo(wlName *string, input map[string]*manifest.Volume) *template.ManagedVolumeCreationInfo {
	// Sort by names for consistent testing
	var names []string
	for name := range input {
		names = append(names, name)
	}
	sort.Strings(names)

	var output *template.ManagedVolumeCreationInfo
	for _, name := range names {
		volume := input[name]
		if volume.EmptyVolume() || !volume.EFS.UseManagedFS() {
			continue
		}
		uid := volume.EFS.Advanced.UID
		gid := volume.EFS.Advanced.GID
		if uid == nil && gid == nil {
			crc := aws.Uint32(getRandomUIDGID(wlName))
			uid = crc
			gid = crc
		}
		output = &template.ManagedVolumeCreationInfo{
			Name:    aws.String(name),
			DirName: wlName,
			UID:     uid,
			GID:     gid,
		}
	}
	return output
}

// getRandomUIDGID returns the 32-bit checksum of the service name for use as CreationInfo in the EFS Access Point.
// See https://stackoverflow.com/a/14210379/5890422 for discussion of the possibility of collisions in CRC32 with
// small numbers of hashes.
func getRandomUIDGID(name *string) uint32 {
	return crc32.ChecksumIEEE([]byte(aws.StringValue(name)))
}

func convertVolumes(input map[string]*manifest.Volume) []*template.Volume {
	// Sort by names for consistent testing
	var names []string
	for name := range input {
		names = append(names, name)
	}
	sort.Strings(names)

	var output []*template.Volume
	for _, name := range names {
		volume := input[name]
		// Volumes can contain either:
		//   a) an EFS configuration, which must be valid
		//   b) no EFS configuration, in which case the volume is created using task scratch storage in order to share
		//      data between containers.

		// If EFS is not configured, just add the name to create an empty volume and continue.
		if volume.EmptyVolume() {
			output = append(
				output,
				&template.Volume{
					Name: aws.String(name),
				},
			)
			continue
		}

		// If we're using managed EFS, continue.
		if volume.EFS.UseManagedFS() {
			continue
		}

		// Convert EFS configuration to template struct.
		output = append(
			output,
			&template.Volume{
				Name: aws.String(name),
				EFS:  convertEFSConfiguration(volume.EFS.Advanced),
			},
		)
	}
	return output
}

func convertEFSConfiguration(in manifest.EFSVolumeConfiguration) *template.EFSVolumeConfiguration {
	// Set default values correctly.
	rootDir := in.RootDirectory
	if aws.StringValue(rootDir) == "" {
		rootDir = aws.String(defaultRootDirectory)
	}
	// Set default values for IAM and AccessPointID
	iam := aws.String(defaultIAM)
	if in.AuthConfig.IsEmpty() {
		return &template.EFSVolumeConfiguration{
			Filesystem:    convertFileSystemID(in),
			RootDirectory: rootDir,
			IAM:           iam,
		}
	}
	// AuthConfig exists; check the properties.
	if aws.BoolValue(in.AuthConfig.IAM) {
		iam = aws.String(enabled)
	}

	return &template.EFSVolumeConfiguration{
		Filesystem:    convertFileSystemID(in),
		RootDirectory: rootDir,
		IAM:           iam,
		AccessPointID: in.AuthConfig.AccessPointID,
	}
}

func convertFileSystemID(in manifest.EFSVolumeConfiguration) template.FileSystemID {
	if in.FileSystemID.Plain != nil {
		return template.PlainFileSystemID(aws.StringValue(in.FileSystemID.Plain))
	}
	return template.ImportedFileSystemID(aws.StringValue(in.FileSystemID.FromCFN.Name))
}

func convertNetworkConfig(network manifest.NetworkConfig) template.NetworkOpts {
	if network.IsEmpty() {
		return template.NetworkOpts{
			AssignPublicIP: template.EnablePublicIP,
			SubnetsType:    template.PublicSubnetsPlacement,
		}
	}
	opts := template.NetworkOpts{
		AssignPublicIP: template.EnablePublicIP,
		SubnetsType:    template.PublicSubnetsPlacement,
	}
	inSGs := network.VPC.SecurityGroups.GetIDs()
	outSGs := make([]template.SecurityGroup, len(inSGs))
	for i, sg := range inSGs {
		if sg.Plain != nil {
			outSGs[i] = template.PlainSecurityGroup(aws.StringValue(sg.Plain))
		} else {
			outSGs[i] = template.ImportedSecurityGroup(aws.StringValue(sg.FromCFN.Name))
		}
	}
	opts.SecurityGroups = outSGs
	opts.DenyDefaultSecurityGroup = network.VPC.SecurityGroups.IsDefaultSecurityGroupDenied()

	placement := network.VPC.Placement
	if placement.IsEmpty() {
		return opts
	}
	if placement.PlacementString != nil {
		if *placement.PlacementString == manifest.PrivateSubnetPlacement {
			opts.AssignPublicIP = template.DisablePublicIP
		}
		opts.SubnetsType = subnetPlacementForTemplate[*placement.PlacementString]
		return opts
	}
	opts.AssignPublicIP = template.DisablePublicIP
	opts.SubnetsType = ""
	opts.SubnetIDs = placement.PlacementArgs.Subnets.IDs
	return opts
}

func convertRDWSNetworkConfig(network manifest.RequestDrivenWebServiceNetworkConfig) template.NetworkOpts {
	opts := template.NetworkOpts{}
	if network.IsEmpty() {
		return opts
	}
	placement := network.VPC.Placement
	if placement.IsEmpty() {
		return opts
	}
	if placement.PlacementString != nil {
		opts.SubnetsType = subnetPlacementForTemplate[*placement.PlacementString]
		return opts
	}
	opts.SubnetIDs = placement.PlacementArgs.Subnets.IDs
	return opts
}

func convertAlias(alias manifest.Alias) ([]string, error) {
	out, err := alias.ToStringSlice()
	if err != nil {
		return nil, fmt.Errorf(`convert "http.alias" to string slice: %w`, err)
	}
	return out, nil
}

func convertEntryPoint(entrypoint manifest.EntryPointOverride) ([]string, error) {
	out, err := entrypoint.ToStringSlice()
	if err != nil {
		return nil, fmt.Errorf(`convert "entrypoint" to string slice: %w`, err)
	}
	return out, nil
}

func convertDeploymentControllerConfig(in manifest.DeploymentControllerConfig) template.DeploymentConfigurationOpts {
	out := template.DeploymentConfigurationOpts{
		MinHealthyPercent: minHealthyPercentDefault,
		MaxPercent:        maxPercentDefault,
	}
	if strings.EqualFold(aws.StringValue(in.Rolling), manifest.ECSRecreateRollingUpdateStrategy) {
		out.MinHealthyPercent = minHealthyPercentRecreate
		out.MaxPercent = maxPercentRecreate
	}
	return out
}

func convertDeploymentConfig(in manifest.DeploymentConfig) template.DeploymentConfigurationOpts {
	out := convertDeploymentControllerConfig(in.DeploymentControllerConfig)
	out.Rollback = template.RollingUpdateRollbackConfig{
		AlarmNames:        in.RollbackAlarms.Basic,
		CPUUtilization:    in.RollbackAlarms.Advanced.CPUUtilization,
		MemoryUtilization: in.RollbackAlarms.Advanced.MemoryUtilization,
	}
	return out
}

func convertWorkerDeploymentConfig(in manifest.WorkerDeploymentConfig) template.DeploymentConfigurationOpts {
	out := convertDeploymentControllerConfig(in.DeploymentControllerConfig)
	out.Rollback = template.RollingUpdateRollbackConfig{
		AlarmNames:        in.WorkerRollbackAlarms.Basic,
		CPUUtilization:    in.WorkerRollbackAlarms.Advanced.CPUUtilization,
		MemoryUtilization: in.WorkerRollbackAlarms.Advanced.MemoryUtilization,
		MessagesDelayed:   in.WorkerRollbackAlarms.Advanced.MessagesDelayed,
	}
	return out
}

func convertCommand(command manifest.CommandOverride) ([]string, error) {
	out, err := command.ToStringSlice()
	if err != nil {
		return nil, fmt.Errorf(`convert "command" to string slice: %w`, err)
	}
	return out, nil
}

func convertPublish(topics []manifest.Topic, accountID, region, app, env, svc string) (*template.PublishOpts, error) {
	if len(topics) == 0 {
		return nil, nil
	}
	partition, err := partitions.Region(region).Partition()
	if err != nil {
		return nil, err
	}
	var publishers template.PublishOpts
	// convert the topics to template Topics
	for _, topic := range topics {
		var fifoConfig *template.FIFOTopicConfig
		if topic.FIFO.IsEnabled() {
			fifoConfig = &template.FIFOTopicConfig{}
			if !topic.FIFO.Advanced.IsEmpty() {
				fifoConfig = &template.FIFOTopicConfig{
					ContentBasedDeduplication: topic.FIFO.Advanced.ContentBasedDeduplication,
				}
			}
		}
		publishers.Topics = append(publishers.Topics, &template.Topic{
			Name:            topic.Name,
			FIFOTopicConfig: fifoConfig,
			AccountID:       accountID,
			Partition:       partition.ID(),
			Region:          region,
			App:             app,
			Env:             env,
			Svc:             svc,
		})
	}

	return &publishers, nil
}

func convertSubscribe(s *manifest.WorkerService) (*template.SubscribeOpts, error) {
	if s.Subscribe.Topics == nil {
		return nil, nil
	}
	var subscriptions template.SubscribeOpts
	for _, sb := range s.Subscriptions() {
		ts, err := convertTopicSubscription(sb)
		if err != nil {
			return nil, err
		}
		subscriptions.Topics = append(subscriptions.Topics, ts)
	}
	subscriptions.Queue = convertQueue(s.Subscribe.Queue)
	return &subscriptions, nil
}

func convertTopicSubscription(t manifest.TopicSubscription) (
	*template.TopicSubscription, error) {
	filterPolicy, err := convertFilterPolicy(t.FilterPolicy)
	if err != nil {
		return nil, err
	}
	if aws.BoolValue(t.Queue.Enabled) {
		return &template.TopicSubscription{
			Name:         t.Name,
			Service:      t.Service,
			Queue:        &template.SQSQueue{},
			FilterPolicy: filterPolicy,
		}, nil
	}
	return &template.TopicSubscription{
		Name:         t.Name,
		Service:      t.Service,
		Queue:        convertQueue(t.Queue.Advanced),
		FilterPolicy: filterPolicy,
	}, nil
}

func convertFilterPolicy(filterPolicy map[string]interface{}) (*string, error) {
	if len(filterPolicy) == 0 {
		return nil, nil
	}
	bytes, err := json.Marshal(filterPolicy)
	if err != nil {
		return nil, fmt.Errorf(`convert "filter_policy" to a JSON string: %w`, err)
	}
	return aws.String(string(bytes)), nil
}

func convertQueue(in manifest.SQSQueue) *template.SQSQueue {
	if in.IsEmpty() {
		return nil
	}

	queue := &template.SQSQueue{
		Retention:  convertRetention(in.Retention),
		Delay:      convertDelay(in.Delay),
		Timeout:    convertTimeout(in.Timeout),
		DeadLetter: convertDeadLetter(in.DeadLetter),
	}

	if !in.FIFO.IsEnabled() {
		return queue
	}

	if aws.BoolValue(in.FIFO.Enable) {
		queue.FIFOQueueConfig = &template.FIFOQueueConfig{}
		return queue
	}

	if !in.FIFO.Advanced.IsEmpty() {
		queue.FIFOQueueConfig = &template.FIFOQueueConfig{
			ContentBasedDeduplication: in.FIFO.Advanced.ContentBasedDeduplication,
			DeduplicationScope:        in.FIFO.Advanced.DeduplicationScope,
			FIFOThroughputLimit:       in.FIFO.Advanced.FIFOThroughputLimit,
		}
		if aws.BoolValue(in.FIFO.Advanced.HighThroughputFifo) {
			queue.FIFOQueueConfig.FIFOThroughputLimit = aws.String(sqsFIFOThroughputLimitPerMessageGroupId)
			queue.FIFOQueueConfig.DeduplicationScope = aws.String(sqsDedupeScopeMessageGroup)
		}
	}
	return queue
}

func convertTime(t *time.Duration) *int64 {
	if t == nil {
		return nil
	}
	return aws.Int64(int64(t.Seconds()))
}

func convertRetention(t *time.Duration) *int64 {
	return convertTime(t)
}

func convertDelay(t *time.Duration) *int64 {
	return convertTime(t)
}

func convertTimeout(t *time.Duration) *int64 {
	return convertTime(t)
}

func convertDeadLetter(d manifest.DeadLetterQueue) *template.DeadLetterQueue {
	if d.IsEmpty() {
		return nil
	}
	return &template.DeadLetterQueue{
		Tries: d.Tries,
	}
}

func convertAppInformation(app deploy.AppInformation) (delegationRole *string, domain *string) {
	role := app.DNSDelegationRole()
	if role != "" {
		delegationRole = &role
	}
	if app.Domain != "" {
		domain = &app.Domain
	}
	return
}

func convertPlatform(platform manifest.PlatformArgsOrString) template.RuntimePlatformOpts {
	if platform.IsEmpty() {
		return template.RuntimePlatformOpts{}
	}

	os := template.OSLinux
	switch platform.OS() {
	case manifest.OSWindows, manifest.OSWindowsServer2019Core:
		os = template.OSWindowsServer2019Core
	case manifest.OSWindowsServer2019Full:
		os = template.OSWindowsServer2019Full
	case manifest.OSWindowsServer2022Core:
		os = template.OSWindowsServer2022Core
	case manifest.OSWindowsServer2022Full:
		os = template.OSWindowsServer2022Full
	}

	arch := template.ArchX86
	if manifest.IsArmArch(platform.Arch()) {
		arch = template.ArchARM64
	}
	return template.RuntimePlatformOpts{
		OS:   os,
		Arch: arch,
	}
}

func convertHTTPVersion(protocolVersion *string) *string {
	if protocolVersion == nil {
		return nil
	}
	pv := strings.ToUpper(*protocolVersion)
	return &pv
}

func convertEnvVars(variables map[string]manifest.Variable) map[string]template.Variable {
	if len(variables) == 0 {
		return nil
	}
	m := make(map[string]template.Variable, len(variables))
	for name, variable := range variables {
		if variable.RequiresImport() {
			m[name] = template.ImportedVariable(variable.Value())
			continue
		}
		m[name] = template.PlainVariable(variable.Value())
	}
	return m
}

// convertSecrets converts the manifest Secrets into a format parsable by the templates pkg.
func convertSecrets(secrets map[string]manifest.Secret) map[string]template.Secret {
	if len(secrets) == 0 {
		return nil
	}
	m := make(map[string]template.Secret, len(secrets))
	var tplSecret template.Secret
	for name, mftSecret := range secrets {
		switch {
		case mftSecret.IsSecretsManagerName():
			tplSecret = template.SecretFromSecretsManager(mftSecret.Value())
		case mftSecret.RequiresImport():
			tplSecret = template.SecretFromImportedSSMOrARN(mftSecret.Value())
		default:
			tplSecret = template.SecretFromPlainSSMOrARN(mftSecret.Value())
		}
		m[name] = tplSecret
	}
	return m
}

func convertCustomResources(urlForFunc map[string]string) (map[string]template.S3ObjectLocation, error) {
	out := make(map[string]template.S3ObjectLocation)
	for fn, url := range urlForFunc {
		bucket, key, err := s3.ParseURL(url)
		if err != nil {
			return nil, fmt.Errorf("convert custom resource %q url: %w", fn, err)
		}
		out[fn] = template.S3ObjectLocation{
			Bucket: bucket,
			Key:    key,
		}
	}
	return out, nil
}

type uploadableCRs []*customresource.CustomResource

func (in uploadableCRs) convert() []uploadable {
	out := make([]uploadable, len(in))
	for i, cr := range in {
		out[i] = cr
	}
	return out
}
