// Copyright 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 serviceconnect

import (
	"fmt"
	"net"
	"strings"

	"github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs"
	"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
	"github.com/aws/aws-sdk-go-v2/aws"
)

const (
	BridgeNetworkMode              = "bridge"
	AWSVPCNetworkMode              = "awsvpc"
	missingContainerInTaskFormat   = `service connect container name=%s does not exist in the task`
	duplicateContainerInTaskFormat = `found %d duplicate service connect container name=%s in the task`
	invalidCidrFormat              = `CIDR=%s is not a valid %s CIDR`
	portCollisionFormat            = `%s port collision detected in the ingress config with %s port=%d, and listener name=%s`
	invalidDnsAddressFormat        = `hostname=%s, address=%s in the DNS config is not valid: %w`
	noScSupportNetworkModeFormat   = `service connect does not support for %s newtork mode`
	missingListenerInIngressFormat = `missing listener name in the ingress config with intercept port=%d`
	invalidPortRangeFormat         = `port=%d is not a valid port. A valid port ranges from 1 through 65535`
	invalidIpAddressFormat         = `address=%s is not a valid IP address`
	invalidIngressPortFormat       = `%s port=%d in the ingress config is not valid: %w`
	warningIngressPortFormat       = `Service connect config: %s port should not exist in the ingress config for %s network mode`
	missingDnsEntryFormat          = `missing %s in the DNS config with hostname=%s, and address=%s`
)

// validateContainerName validates the service connect container name exists in the task and no duplication.
func validateContainerName(scContainerName string, taskContainers []*ecsacs.Container) error {
	// service connect container name is required
	if scContainerName == "" {
		return fmt.Errorf("missing service connect container name")
	}

	// validate the specified service connect container name exists in the task
	numOfFoundScContainer := 0
	for _, container := range taskContainers {
		if aws.ToString(container.Name) == scContainerName {
			numOfFoundScContainer += 1
		}
	}

	if numOfFoundScContainer == 0 {
		return fmt.Errorf(missingContainerInTaskFormat, scContainerName)
	} else if numOfFoundScContainer > 1 {
		return fmt.Errorf(duplicateContainerInTaskFormat, numOfFoundScContainer, scContainerName)
	}

	return nil
}

// validateEgressConfig validates the listener name, IPv4 CIDR format and IPv6 CIDR format in the service connect egress config.
func validateEgressConfig(scEgressConfig *EgressConfig, ipv6Enabled bool) error {
	// egress config can be empty for the first service since there are no other tasks that it can talk to
	if scEgressConfig == nil {
		return nil
	}

	// ListenerName is required if the egress config exists
	if scEgressConfig.ListenerName == "" {
		return fmt.Errorf("missing listener name in the egress config")
	}

	// validate IPV4CIDR if it exists
	if scEgressConfig.VIP.IPV4CIDR != "" {
		trimmedIpv4Cidr := strings.TrimSpace(scEgressConfig.VIP.IPV4CIDR)
		if err := validateCIDR(trimmedIpv4Cidr, "IPv4"); err != nil {
			return err
		}
	}

	// validate IPV6CIDR if it exists
	if scEgressConfig.VIP.IPV6CIDR != "" {
		trimmedIpv6Cidr := strings.TrimSpace(scEgressConfig.VIP.IPV6CIDR)
		if err := validateCIDR(trimmedIpv6Cidr, "IPv6"); err != nil {
			return err
		}
	}

	return nil
}

// validateCIDR validates the passed CIDR is a valid IPv4/IPv6 CIDR based on the protocol.
func validateCIDR(cidr, protocol string) error {
	ip, _, err := net.ParseCIDR(cidr)
	if err == nil {
		if valid := getProtocol(ip, protocol); valid {
			return nil
		}
	}

	return fmt.Errorf(invalidCidrFormat, cidr, protocol)
}

// getProtocol returns validity of the given IP based on the target protocol.
func getProtocol(ip net.IP, protocol string) bool {
	switch protocol {
	case "IPv4":
		if ip.To4() != nil {
			return true
		}
	case "IPv6":
		if ip.To16() != nil {
			return true
		}
	default:
		return false
	}
	return false
}

// validateDnsConfig validates hostnames and addresses in the service connnect DNS config.
func validateDnsConfig(scDnsConfligList []DNSConfigEntry) error {
	for _, dnsEntry := range scDnsConfligList {
		// HostName is required
		if dnsEntry.HostName == "" {
			return fmt.Errorf(missingDnsEntryFormat, "hostname", dnsEntry.HostName, dnsEntry.Address)
		}

		// Address is required
		if dnsEntry.Address == "" {
			return fmt.Errorf(missingDnsEntryFormat, "address", dnsEntry.HostName, dnsEntry.Address)
		}

		// validate the address is a valid IPv4/IPv6 address
		if err := validateAddress(dnsEntry.Address); err != nil {
			return fmt.Errorf(invalidDnsAddressFormat, dnsEntry.HostName, dnsEntry.Address, err)
		}
	}

	return nil
}

// validateAddress validates the passed address is a valid IPv4/IPv6 address.
func validateAddress(address string) error {
	if ip := net.ParseIP(address); ip == nil {
		return fmt.Errorf(invalidIpAddressFormat, address)
	}
	return nil
}

// validateIngressConfig validates the service connect ingress config based on given network mode.
func validateIngressConfig(scIngressConfigList []IngressConfigEntry, taskNetworkMode string) error {
	// ingress config can be empty since an ECS service can only act as a client
	if len(scIngressConfigList) == 0 {
		return nil
	}

	switch taskNetworkMode {
	case BridgeNetworkMode, AWSVPCNetworkMode:
		if err := validateIngressConfigEntry(scIngressConfigList, taskNetworkMode); err != nil {
			return err
		}
	default:
		return fmt.Errorf(noScSupportNetworkModeFormat, taskNetworkMode)
	}

	return nil
}

// validateIngressConfigEntry validates the service connect ingress config entry based on given network mode.
func validateIngressConfigEntry(scIngressConfigList []IngressConfigEntry, networkMode string) error {
	interceptAndListenerPortsMap := map[uint16]bool{}
	hostPortsMap := map[uint16]bool{}
	listenerPortValue := uint16(0)
	interceptPortValue := uint16(0)
	hostPortValue := uint16(0)

	for _, entry := range scIngressConfigList {
		// show a warning message if
		// 1) a host port exists in the ingress config for awsvpc mode
		// 2) an intercept port exists in the ingress config for bridge mode
		if (entry.HostPort != nil && networkMode == AWSVPCNetworkMode) ||
			(entry.InterceptPort != nil && networkMode == BridgeNetworkMode) {
			invalidPort := "a host"
			if networkMode == BridgeNetworkMode {
				invalidPort = "an intercept"
			}
			warningMsg := fmt.Sprintf(warningIngressPortFormat, invalidPort, networkMode)
			logger.Warn(warningMsg, logger.Fields{
				"listenerName":  entry.ListenerName,
				"listenerPort":  entry.ListenerPort,
				"hostPort":      aws.ToUint16(entry.HostPort),
				"interceptPort": aws.ToUint16(entry.InterceptPort),
			})
		}

		// verify the intercept port for awsvpc mode
		if entry.InterceptPort != nil && networkMode == AWSVPCNetworkMode {
			interceptPortValue = aws.ToUint16(entry.InterceptPort)
			if err := validateInterceptPort(interceptPortValue, entry.ListenerName, interceptAndListenerPortsMap); err != nil {
				return err
			}
			// save the listener port value
			interceptAndListenerPortsMap[interceptPortValue] = true
		}

		// verify the listener port
		if entry.ListenerPort > uint16(0) {
			listenerPortValue = entry.ListenerPort
			if err := validateListenerPort(listenerPortValue, entry.ListenerName, interceptAndListenerPortsMap); err != nil {
				return err
			}
			// save the listener port value
			interceptAndListenerPortsMap[listenerPortValue] = true
		}

		// verify the host port for bridge mode
		if entry.HostPort != nil && networkMode == BridgeNetworkMode {
			hostPortValue = aws.ToUint16(entry.HostPort)
			if err := validateHostPort(hostPortValue, entry.ListenerName, hostPortsMap); err != nil {
				return err
			}
			// save the host port value
			hostPortsMap[hostPortValue] = true
		}
	}

	return nil
}

// validateInterceptPort validates the intercept port is in the valid port range and does not have port collision.
func validateInterceptPort(interceptPortValue uint16, listenerName string, interceptAndListenerPortsMap map[uint16]bool) error {
	if err := validatePort(interceptPortValue); err != nil {
		return fmt.Errorf(invalidIngressPortFormat, "intercept", interceptPortValue, err)
	}

	if listenerName == "" {
		return fmt.Errorf(missingListenerInIngressFormat, interceptPortValue)
	}

	if present := interceptAndListenerPortsMap[interceptPortValue]; present {
		return fmt.Errorf(portCollisionFormat, "intercept", "intercept", interceptPortValue, listenerName)
	}

	return nil
}

// validateListenerPort validates the listener port is in the valid port range and does not have port collision.
func validateListenerPort(listenerPortValue uint16, listenerName string, interceptAndListenerPortsMap map[uint16]bool) error {
	if err := validatePort(listenerPortValue); err != nil {
		return fmt.Errorf(invalidIngressPortFormat, "listener", listenerPortValue, err)
	}

	if present := interceptAndListenerPortsMap[listenerPortValue]; present {
		return fmt.Errorf(portCollisionFormat, "listener", "listener", listenerPortValue, listenerName)
	}

	return nil
}

// validateHostPort validates the host port is in the valid port range and does not have port collision.
func validateHostPort(hostPortValue uint16, listenerName string, hostPortsMap map[uint16]bool) error {
	if err := validatePort(hostPortValue); err != nil {
		return fmt.Errorf(invalidIngressPortFormat, "host", hostPortValue, err)
	}

	if present := hostPortsMap[hostPortValue]; present {
		return fmt.Errorf(portCollisionFormat, "host", "host", hostPortValue, listenerName)
	}

	return nil
}

// validatePort validates port is in valid range.
func validatePort(port uint16) error {
	// valid port range is 1~65535
	if port >= uint16(1) && port <= uint16(65535) {
		return nil
	}

	return fmt.Errorf(invalidPortRangeFormat, port)
}

// ValidateServiceConnectConfig validates service connect container name,
// fields in egress config, dns config and ingress config when
// 1) fields consumed and proceeded by ECS Agent
// 2) fields with a global standard, e.g. CIDR format
func ValidateServiceConnectConfig(scConfig *Config,
	taskContainers []*ecsacs.Container,
	taskNetworkMode string,
	ipv6Enabled bool) error {
	if err := validateContainerName(scConfig.ContainerName, taskContainers); err != nil {
		return err
	}

	if err := validateEgressConfig(scConfig.EgressConfig, ipv6Enabled); err != nil {
		return err
	}

	if err := validateDnsConfig(scConfig.DNSConfig); err != nil {
		return err
	}

	if err := validateIngressConfig(scConfig.IngressConfig, taskNetworkMode); err != nil {
		return err
	}

	return nil
}
