// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package serviceregistration

import (
	"fmt"
	"strconv"

	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/plugins/drivers"
)

// GetAddress returns the IP (or custom advertise address) and port to use for a
// service or check registration. If no port label is specified (an empty value)
// and no custom address is specified, zero values are returned because no address
// could be resolved.
func GetAddress(
	address, // custom address, if set
	addressMode,
	portLabel string,
	networks structs.Networks,
	driverNet *drivers.DriverNetwork,
	ports structs.AllocatedPorts,
	netStatus *structs.AllocNetworkStatus,
) (string, int, error) {
	switch addressMode {
	case structs.AddressModeAuto:
		switch {
		case address != "":
			// No port no problem, just return the advertise address.
			if portLabel == "" {
				return address, 0, nil
			}
			// A custom advertise address can be used with a port map; using the
			// Value and ignoring the IP. The routing from your custom address to
			// the group network address is DIY. (e.g. EC2 public address)
			if mapping, exists := ports.Get(portLabel); exists {
				return address, mapping.Value, nil
			}
			// If not a port map we can interpolate a numeric port for you.
			port, err := strconv.Atoi(portLabel)
			if err != nil {
				return "", 0, fmt.Errorf("invalid port: %q: not a valid port mapping or numeric port", portLabel)
			}
			return address, port, nil
		case driverNet.Advertise():
			return GetAddress("", structs.AddressModeDriver, portLabel, networks, driverNet, ports, netStatus)
		default:
			return GetAddress("", structs.AddressModeHost, portLabel, networks, driverNet, ports, netStatus)
		}
	case structs.AddressModeHost:
		// Cannot use address mode host with custom advertise address.
		if address != "" {
			return "", 0, fmt.Errorf("cannot use custom advertise address with %q address mode", structs.AddressModeHost)
		}

		if portLabel == "" {
			if len(networks) != 1 {
				// If no networks are specified return zero
				// values. Consul will advertise the host IP
				// with no port. This is the pre-0.7.1 behavior
				// some people rely on.
				return "", 0, nil
			}
			return networks[0].IP, 0, nil
		}

		// Default path: use host ip:port
		// Try finding port in the AllocatedPorts struct first
		// Check in Networks struct for backwards compatibility if not found
		mapping, ok := ports.Get(portLabel)
		if !ok {
			mapping = networks.Port(portLabel)
			if mapping.Value > 0 {
				return mapping.HostIP, mapping.Value, nil
			}

			// If port isn't a label, try to parse it as a literal port number
			port, err := strconv.Atoi(portLabel)
			if err != nil {
				// Don't include Atoi error message as user likely
				// never intended it to be a numeric and it creates a
				// confusing error message
				return "", 0, fmt.Errorf("invalid port %q: port label not found", portLabel)
			}
			if port <= 0 {
				return "", 0, fmt.Errorf("invalid port: %q: port must be >0", portLabel)
			}

			// A number was given which will use the Consul agent's address and the given port
			// Returning a blank string as an address will use the Consul agent's address
			return "", port, nil
		}
		return mapping.HostIP, mapping.Value, nil

	case structs.AddressModeDriver:
		// Cannot use address mode driver with custom advertise address.
		if address != "" {
			return "", 0, fmt.Errorf("cannot use custom advertise address with %q address mode", structs.AddressModeDriver)
		}

		// Require a driver network if driver address mode is used
		if driverNet == nil {
			return "", 0, fmt.Errorf(`cannot use address_mode="driver": no driver network exists`)
		}

		// If no port label is specified just return the IP
		if portLabel == "" {
			return driverNet.IP, 0, nil
		}

		// If the port is a label, use the driver's port (not the host's)
		if port, ok := ports.Get(portLabel); ok {
			return driverNet.IP, port.To, nil
		}

		// Check if old style driver portmap is used
		if port, ok := driverNet.PortMap[portLabel]; ok {
			return driverNet.IP, port, nil
		}

		// If port isn't a label, try to parse it as a literal port number
		port, err := strconv.Atoi(portLabel)
		if err != nil {
			// Don't include Atoi error message as user likely
			// never intended it to be a numeric and it creates a
			// confusing error message
			return "", 0, fmt.Errorf("invalid port label %q: port labels in driver address_mode must be numeric or in the driver's port map", portLabel)
		}
		if port <= 0 {
			return "", 0, fmt.Errorf("invalid port: %q: port must be >0", portLabel)
		}

		return driverNet.IP, port, nil

	case structs.AddressModeAlloc:
		// Cannot use address mode alloc with custom advertise address.
		if address != "" {
			return "", 0, fmt.Errorf("cannot use custom advertise address with %q address mode", structs.AddressModeAlloc)
		}

		// Going to need a network for this.
		if netStatus == nil {
			return "", 0, fmt.Errorf(`cannot use address_mode="alloc": no allocation network status reported`)
		}

		// If no port label is specified just return the IP
		if portLabel == "" {
			return netStatus.Address, 0, nil
		}

		// If port is a label and is found then return it
		if port, ok := ports.Get(portLabel); ok {
			// Use port.To value unless not set
			if port.To > 0 {
				return netStatus.Address, port.To, nil
			}
			return netStatus.Address, port.Value, nil
		}

		// Check if port is a literal number
		port, err := strconv.Atoi(portLabel)
		if err != nil {
			// User likely specified wrong port label here
			return "", 0, fmt.Errorf("invalid port %q: port label not found or is not numeric", portLabel)
		}
		if port <= 0 {
			return "", 0, fmt.Errorf("invalid port: %q: port must be >0", portLabel)
		}
		return netStatus.Address, port, nil

	default:
		// Shouldn't happen due to validation, but enforce invariants
		return "", 0, fmt.Errorf("invalid address mode %q", addressMode)
	}
}
