package port

import (
	"strconv"
	"strings"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
)

// ContainerPortSpecs represents a container exposed ports.
type ContainerPortSpecs []ContainerPortSpec

func (c ContainerPortSpecs) Dump() string {
	ss := make([]string, 0, len(c))
	for _, spec := range c {
		ss = append(ss, spec.String())
	}

	return strings.Join(ss, "\n")
}

// InSpecs checks if given port matches a spec.
func (c ContainerPortSpecs) MatchSpec(s string) bool {
	// Skip validation if No port are exposed or no container port spec.
	if len(c) == 0 || !strings.Contains(s, "::") {
		return true
	}
	for _, spec := range c {
		if spec.MatchSpec(s) {
			return true
		}
	}

	return false
}

// ToTunnels convert port specs to tunnels.
func (c ContainerPortSpecs) ToTunnels(address string) PortTunnels {
	tt := make(PortTunnels, 0, len(c))
	for _, spec := range c {
		tt = append(tt, spec.ToTunnel(address))
	}

	return tt
}

// Find finds a matching container port.
func (c ContainerPortSpecs) Find(pf *PFAnn) (ContainerPortSpec, bool) {
	for _, spec := range c {
		if spec.Match(pf) {
			return spec, true
		}
	}

	return ContainerPortSpec{}, false
}

// Match checks if container ports match a pf annotation.
func (c ContainerPortSpecs) Match(pf *PFAnn) bool {
	for _, spec := range c {
		if spec.Match(pf) {
			return true
		}
	}

	return false
}

func (c ContainerPortSpecs) MatchAnnotations(s string) PFAnns {
	pfs, err := ParsePFs(s)
	if err != nil {
		return nil
	}

	mm := make(PFAnns, 0, len(c))
	for _, pf := range pfs {
		if pf.Match(c) {
			mm = append(mm, pf)
		}
	}

	return mm
}

// FromContainerPorts hydrates from a pod container specification.
func FromContainerPorts(co string, pp []v1.ContainerPort) ContainerPortSpecs {
	specs := make(ContainerPortSpecs, 0, len(pp))
	for _, p := range pp {
		if p.Protocol != v1.ProtocolTCP {
			continue
		}
		specs = append(specs, NewPortSpec(co, p.Name, p.ContainerPort))
	}

	return specs
}

// ContainerPortSpec represents a container port specification.
type ContainerPortSpec struct {
	Container string
	PortName  string
	PortNum   string
}

// NewPortSpec returns a new instance.
func NewPortSpec(co, portName string, port int32) ContainerPortSpec {
	return ContainerPortSpec{
		Container: co,
		PortName:  portName,
		PortNum:   strconv.Itoa(int(port)),
	}
}

func (c ContainerPortSpec) MatchSpec(s string) bool {
	tokens := strings.Split(s, "::")
	if len(tokens) < 2 {
		return false
	}

	return tokens[0] == c.Container && tokens[1] == c.PortNum
}

func (c ContainerPortSpec) ToTunnel(address string) PortTunnel {
	return PortTunnel{
		Address:       address,
		LocalPort:     c.PortNum,
		ContainerPort: c.PortNum,
	}
}

func (c ContainerPortSpec) Port() intstr.IntOrString {
	if c.PortName != "" {
		return intstr.Parse(c.PortName)
	}

	return intstr.Parse(c.PortNum)
}

func (c ContainerPortSpec) ToPFAnn() *PFAnn {
	return &PFAnn{
		Container:     c.Container,
		ContainerPort: c.Port(),
		LocalPort:     c.PortNum,
	}
}

// Match checks if the container spec matches an annotation.
func (c ContainerPortSpec) Match(ann *PFAnn) bool {
	if c.Container != ann.Container {
		return false
	}

	switch ann.ContainerPort.Type {
	case intstr.String:
		return c.PortName == ann.ContainerPort.String()
	case intstr.Int:
		return c.PortNum == ann.ContainerPort.String()
	default:
		return false
	}
}

// String dumps spec to string.
func (c ContainerPortSpec) String() string {
	s := c.Container + "::" + c.PortNum
	if c.PortName != "" {
		s += "(" + c.PortName + ")"
	}
	return s
}
