package specgenutil

import (
	"io/ioutil"
	"net"
	"strconv"
	"strings"

	"github.com/containers/podman/v3/libpod/network/types"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// ReadPodIDFile reads the specified file and returns its content (i.e., first
// line).
func ReadPodIDFile(path string) (string, error) {
	content, err := ioutil.ReadFile(path)
	if err != nil {
		return "", errors.Wrap(err, "error reading pod ID file")
	}
	return strings.Split(string(content), "\n")[0], nil
}

// ReadPodIDFiles reads the specified files and returns their content (i.e.,
// first line).
func ReadPodIDFiles(files []string) ([]string, error) {
	ids := []string{}
	for _, file := range files {
		id, err := ReadPodIDFile(file)
		if err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	return ids, nil
}

// ParseFilters transforms one filter format to another and validates input
func ParseFilters(filter []string) (map[string][]string, error) {
	// TODO Remove once filter refactor is finished and url.Values done.
	filters := map[string][]string{}
	for _, f := range filter {
		t := strings.SplitN(f, "=", 2)
		filters = make(map[string][]string)
		if len(t) < 2 {
			return map[string][]string{}, errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
		}
		filters[t[0]] = append(filters[t[0]], t[1])
	}
	return filters, nil
}

// createExpose parses user-provided exposed port definitions and converts them
// into SpecGen format.
// TODO: The SpecGen format should really handle ranges more sanely - we could
// be massively inflating what is sent over the wire with a large range.
func createExpose(expose []string) (map[uint16]string, error) {
	toReturn := make(map[uint16]string)

	for _, e := range expose {
		// Check for protocol
		proto := "tcp"
		splitProto := strings.Split(e, "/")
		if len(splitProto) > 2 {
			return nil, errors.Errorf("invalid expose format - protocol can only be specified once")
		} else if len(splitProto) == 2 {
			proto = splitProto[1]
		}

		// Check for a range
		start, len, err := parseAndValidateRange(splitProto[0])
		if err != nil {
			return nil, err
		}

		var index uint16
		for index = 0; index < len; index++ {
			portNum := start + index
			protocols, ok := toReturn[portNum]
			if !ok {
				toReturn[portNum] = proto
			} else {
				newProto := strings.Join(append(strings.Split(protocols, ","), strings.Split(proto, ",")...), ",")
				toReturn[portNum] = newProto
			}
		}
	}

	return toReturn, nil
}

// CreatePortBindings iterates ports mappings into SpecGen format.
func CreatePortBindings(ports []string) ([]types.PortMapping, error) {
	// --publish is formatted as follows:
	// [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol]
	toReturn := make([]types.PortMapping, 0, len(ports))

	for _, p := range ports {
		var (
			ctrPort                 string
			proto, hostIP, hostPort *string
		)

		splitProto := strings.Split(p, "/")
		switch len(splitProto) {
		case 1:
			// No protocol was provided
		case 2:
			proto = &(splitProto[1])
		default:
			return nil, errors.Errorf("invalid port format - protocol can only be specified once")
		}

		remainder := splitProto[0]
		haveV6 := false

		// Check for an IPv6 address in brackets
		splitV6 := strings.Split(remainder, "]")
		switch len(splitV6) {
		case 1:
			// Do nothing, proceed as before
		case 2:
			// We potentially have an IPv6 address
			haveV6 = true
			if !strings.HasPrefix(splitV6[0], "[") {
				return nil, errors.Errorf("invalid port format - IPv6 addresses must be enclosed by []")
			}
			if !strings.HasPrefix(splitV6[1], ":") {
				return nil, errors.Errorf("invalid port format - IPv6 address must be followed by a colon (':')")
			}
			ipNoPrefix := strings.TrimPrefix(splitV6[0], "[")
			hostIP = &ipNoPrefix
			remainder = strings.TrimPrefix(splitV6[1], ":")
		default:
			return nil, errors.Errorf("invalid port format - at most one IPv6 address can be specified in a --publish")
		}

		splitPort := strings.Split(remainder, ":")
		switch len(splitPort) {
		case 1:
			if haveV6 {
				return nil, errors.Errorf("invalid port format - must provide host and destination port if specifying an IP")
			}
			ctrPort = splitPort[0]
		case 2:
			hostPort = &(splitPort[0])
			ctrPort = splitPort[1]
		case 3:
			if haveV6 {
				return nil, errors.Errorf("invalid port format - when v6 address specified, must be [ipv6]:hostPort:ctrPort")
			}
			hostIP = &(splitPort[0])
			hostPort = &(splitPort[1])
			ctrPort = splitPort[2]
		default:
			return nil, errors.Errorf("invalid port format - format is [[hostIP:]hostPort:]containerPort")
		}

		newPort, err := parseSplitPort(hostIP, hostPort, ctrPort, proto)
		if err != nil {
			return nil, err
		}

		toReturn = append(toReturn, newPort)
	}

	return toReturn, nil
}

// parseSplitPort parses individual components of the --publish flag to produce
// a single port mapping in SpecGen format.
func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (types.PortMapping, error) {
	newPort := types.PortMapping{}
	if ctrPort == "" {
		return newPort, errors.Errorf("must provide a non-empty container port to publish")
	}
	ctrStart, ctrLen, err := parseAndValidateRange(ctrPort)
	if err != nil {
		return newPort, errors.Wrapf(err, "error parsing container port")
	}
	newPort.ContainerPort = ctrStart
	newPort.Range = ctrLen

	if protocol != nil {
		if *protocol == "" {
			return newPort, errors.Errorf("must provide a non-empty protocol to publish")
		}
		newPort.Protocol = *protocol
	}
	if hostIP != nil {
		if *hostIP == "" {
			return newPort, errors.Errorf("must provide a non-empty container host IP to publish")
		} else if *hostIP != "0.0.0.0" {
			// If hostIP is 0.0.0.0, leave it unset - CNI treats
			// 0.0.0.0 and empty differently, Docker does not.
			testIP := net.ParseIP(*hostIP)
			if testIP == nil {
				return newPort, errors.Errorf("cannot parse %q as an IP address", *hostIP)
			}
			newPort.HostIP = testIP.String()
		}
	}
	if hostPort != nil {
		if *hostPort == "" {
			// Set 0 as a placeholder. The server side of Specgen
			// will find a random, open, unused port to use.
			newPort.HostPort = 0
		} else {
			hostStart, hostLen, err := parseAndValidateRange(*hostPort)
			if err != nil {
				return newPort, errors.Wrapf(err, "error parsing host port")
			}
			if hostLen != ctrLen {
				return newPort, errors.Errorf("host and container port ranges have different lengths: %d vs %d", hostLen, ctrLen)
			}
			newPort.HostPort = hostStart
		}
	}

	hport := newPort.HostPort
	logrus.Debugf("Adding port mapping from %d to %d length %d protocol %q", hport, newPort.ContainerPort, newPort.Range, newPort.Protocol)

	return newPort, nil
}

// Parse and validate a port range.
// Returns start port, length of range, error.
func parseAndValidateRange(portRange string) (uint16, uint16, error) {
	splitRange := strings.Split(portRange, "-")
	if len(splitRange) > 2 {
		return 0, 0, errors.Errorf("invalid port format - port ranges are formatted as startPort-stopPort")
	}

	if splitRange[0] == "" {
		return 0, 0, errors.Errorf("port numbers cannot be negative")
	}

	startPort, err := parseAndValidatePort(splitRange[0])
	if err != nil {
		return 0, 0, err
	}

	var rangeLen uint16 = 1
	if len(splitRange) == 2 {
		if splitRange[1] == "" {
			return 0, 0, errors.Errorf("must provide ending number for port range")
		}
		endPort, err := parseAndValidatePort(splitRange[1])
		if err != nil {
			return 0, 0, err
		}
		if endPort <= startPort {
			return 0, 0, errors.Errorf("the end port of a range must be higher than the start port - %d is not higher than %d", endPort, startPort)
		}
		// Our range is the total number of ports
		// involved, so we need to add 1 (8080:8081 is
		// 2 ports, for example, not 1)
		rangeLen = endPort - startPort + 1
	}

	return startPort, rangeLen, nil
}

// Turn a single string into a valid U16 port.
func parseAndValidatePort(port string) (uint16, error) {
	num, err := strconv.Atoi(port)
	if err != nil {
		return 0, errors.Wrapf(err, "invalid port number")
	}
	if num < 1 || num > 65535 {
		return 0, errors.Errorf("port numbers must be between 1 and 65535 (inclusive), got %d", num)
	}
	return uint16(num), nil
}