package render

import (
	"context"
	"regexp"
	"strings"

	"github.com/weaveworks/scope/report"
)

// Constants are used in the tests.
const (
	UncontainedID    = "uncontained"
	UncontainedMajor = "Uncontained"

	// Topology for IPs so we can differentiate them at the end
	IP = "IP"
)

// UncontainedIDPrefix is the prefix of uncontained pseudo nodes
var UncontainedIDPrefix = MakePseudoNodeID(UncontainedID, "")

// ContainerRenderer is a Renderer which produces a renderable container
// graph by merging the process graph and the container topology.
// NB We only want processes in container _or_ processes with network connections
// but we need to be careful to ensure we only include each edge once, by only
// including the ProcessRenderer once.
var ContainerRenderer = Memoise(MakeFilter(
	func(n report.Node) bool {
		// Drop deleted containers
		state, ok := n.Latest.Lookup(report.DockerContainerState)
		return !ok || state != report.StateDeleted
	},
	MakeReduce(
		MakeMap(
			MapProcess2Container,
			ProcessRenderer,
		),
		ConnectionJoin(MapContainer2IP, report.Container),
	),
))

const originalNodeID = "original_node_id"

// ConnectionJoin joins the given topology with connections from the
// endpoints topology, using the toIPs function to extract IPs from
// the nodes.
func ConnectionJoin(toIPs func(report.Node) []string, topology string) Renderer {
	return connectionJoin{toIPs: toIPs, topology: topology}
}

type connectionJoin struct {
	toIPs    func(report.Node) []string
	topology string
}

func (c connectionJoin) Render(ctx context.Context, rpt report.Report) Nodes {
	inputNodes := TopologySelector(c.topology).Render(ctx, rpt).Nodes
	// Collect all the IPs we are trying to map to, and which ID they map from
	var ipNodes = map[string]string{}
	for _, n := range inputNodes {
		for _, ip := range c.toIPs(n) {
			if _, exists := ipNodes[ip]; exists {
				// If an IP is shared between multiple nodes, we can't reliably
				// attribute an connection based on its IP
				ipNodes[ip] = "" // blank out the mapping so we don't use it
			} else {
				ipNodes[ip] = n.ID
			}
		}
	}
	return MapEndpoints(
		func(m report.Node) string {
			scope, addr, port, ok := report.ParseEndpointNodeID(m.ID)
			if !ok {
				return ""
			}
			id, found := ipNodes[report.MakeScopedEndpointNodeID(scope, addr, "")]
			// We also allow for joining on ip:port pairs.  This is
			// useful for connections to the host IPs which have been
			// port mapped to a container can only be unambiguously
			// identified with the port.
			if !found {
				id, found = ipNodes[report.MakeScopedEndpointNodeID(scope, addr, port)]
			}
			if !found || id == "" {
				return ""
			}
			// Not an IP we blanked out earlier.
			//
			// MapEndpoints is guaranteed to find a node with this id
			// (and hence not have to create one), since we got the id
			// from ipNodes, which is populated from c.topology, which
			// is where MapEndpoints will look.
			return id
		}, c.topology).Render(ctx, rpt)
}

// FilterEmpty is a Renderer which filters out nodes which have no children
// from the specified topology.
func FilterEmpty(topology string, r Renderer) Renderer {
	return MakeFilter(HasChildren(topology), r)
}

// HasChildren returns true if the node has no children from the specified
// topology.
func HasChildren(topology string) FilterFunc {
	return func(n report.Node) bool {
		count := 0
		n.Children.ForEach(func(child report.Node) {
			if child.Topology == topology {
				count++
			}
		})
		return count > 0
	}
}

type containerWithImageNameRenderer struct{}

// Render produces a container graph where the the latest metadata contains the
// container image name, if found.
func (r containerWithImageNameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
	containers := ContainerRenderer.Render(ctx, rpt)
	images := SelectContainerImage.Render(ctx, rpt)

	outputs := make(report.Nodes, len(containers.Nodes))
	for id, c := range containers.Nodes {
		outputs[id] = c
		imageID, ok := c.Latest.Lookup(report.DockerImageID)
		if !ok {
			continue
		}
		image, ok := images.Nodes[report.MakeContainerImageNodeID(imageID)]
		if !ok {
			continue
		}
		imageNodeID := containerImageNodeID(image)
		if imageNodeID == "" {
			continue
		}

		c.Latest = c.Latest.Propagate(image.Latest, report.DockerImageName, report.DockerImageTag,
			report.DockerImageSize, report.DockerImageVirtualSize, report.DockerImageLabelPrefix+"works.weave.role")

		c.Parents = c.Parents.
			Delete(report.ContainerImage).
			AddString(report.ContainerImage, imageNodeID)
		outputs[id] = c
	}
	return Nodes{Nodes: outputs, Filtered: containers.Filtered}
}

// ContainerWithImageNameRenderer is a Renderer which produces a container
// graph where the ranks are the image names, not their IDs
var ContainerWithImageNameRenderer = Memoise(containerWithImageNameRenderer{})

// ContainerImageRenderer produces a graph where each node is a container image
// with the original containers as children
var ContainerImageRenderer = Memoise(FilterEmpty(report.Container,
	MakeMap(
		MapContainerImage2Name,
		containerImageRenderer{},
	),
))

// ContainerHostnameRenderer is a Renderer which produces a renderable container
// by hostname graph..
//
// not memoised
var ContainerHostnameRenderer = FilterEmpty(report.Container,
	containerHostnameRenderer{},
)

var portMappingMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):([0-9]+)->([0-9]+)/tcp`)

// MapContainer2IP maps container nodes to their IP addresses (outputs
// multiple nodes).  This allows container to be joined directly with
// the endpoint topology.
func MapContainer2IP(m report.Node) []string {
	// if this container doesn't make connections, we can ignore it
	_, doesntMakeConnections := m.Latest.Lookup(report.DoesNotMakeConnections)
	// if this container belongs to the host's networking namespace
	// we cannot use its IP to attribute connections
	// (they could come from any other process on the host or DNAT-ed IPs)
	_, isInHostNetwork := m.Latest.Lookup(report.DockerIsInHostNetwork)
	if doesntMakeConnections || isInHostNetwork {
		return nil
	}

	result := []string{}
	if addrs, ok := m.Sets.Lookup(report.DockerContainerIPsWithScopes); ok {
		for _, addr := range addrs {
			scope, addr, ok := report.ParseAddressNodeID(addr)
			if !ok {
				continue
			}
			// loopback addresses are shared among all namespaces
			// so we can't use them to attribute connections to a container
			if report.IsLoopback(addr) {
				continue
			}
			id := report.MakeScopedEndpointNodeID(scope, addr, "")
			result = append(result, id)
		}
	}

	// Also output all the host:port port mappings (see above comment).
	// In this case we assume this doesn't need a scope, as they are for host IPs.
	ports, _ := m.Sets.Lookup(report.DockerContainerPorts)
	for _, portMapping := range ports {
		if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
			ip, port := mapping[1], mapping[2]
			id := report.MakeScopedEndpointNodeID("", ip, port)
			result = append(result, id)
		}
	}

	return result
}

// MapProcess2Container maps process Nodes to container
// Nodes.
//
// Pseudo nodes are passed straight through.
//
// If this function is given a node without a docker_container_id, it
// will produce an "Uncontained" pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a container graph to get that info.
func MapProcess2Container(n report.Node) report.Node {
	// Propagate pseudo nodes
	if n.Topology == Pseudo {
		return n
	}

	// Otherwise, if the process is not in a container, group it into
	// a per-host "Uncontained" node.
	var (
		id   string
		node report.Node
	)
	if containerID, ok := n.Latest.Lookup(report.DockerContainerID); ok {
		id = report.MakeContainerNodeID(containerID)
		node = NewDerivedNode(id, n).WithTopology(report.Container)
	} else {
		hostID, _, _ := report.ParseProcessNodeID(n.ID)
		id = MakePseudoNodeID(UncontainedID, hostID)
		node = NewDerivedPseudoNode(id, n)
	}
	return node
}

// containerImageRenderer produces a graph where each node is a container image
// with the original containers as children
type containerImageRenderer struct{}

func (m containerImageRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
	containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
	ret := newJoinResults(rpt.ContainerImage.Nodes)

	for _, n := range containers.Nodes {
		if n.Topology == Pseudo {
			ret.passThrough(n)
			continue
		}
		// If some some reason the container doesn't have a image_id, just drop it
		imageID, ok := n.Latest.Lookup(report.DockerImageID)
		if !ok {
			continue
		}
		id := report.MakeContainerImageNodeID(imageID)
		ret.addChildAndChildren(n, id, report.ContainerImage)
	}
	return ret.result(ctx, containers)
}

func containerImageNodeID(n report.Node) string {
	imageName, ok := n.Latest.Lookup(report.DockerImageName)
	if !ok {
		return ""
	}

	parts := strings.SplitN(imageName, "/", 3)
	if len(parts) == 3 {
		imageName = strings.Join(parts[1:3], "/")
	}
	imageNameWithoutTag := strings.SplitN(imageName, ":", 2)[0]
	return report.MakeContainerImageNodeID(imageNameWithoutTag)
}

// MapContainerImage2Name ignores image versions
func MapContainerImage2Name(n report.Node) report.Node {
	// Propagate all pseudo nodes
	if n.Topology == Pseudo {
		return n
	}

	n.ID = containerImageNodeID(n)
	if n.ID == "" {
		return report.Node{}
	}

	return n
}

var containerHostnameTopology = MakeGroupNodeTopology(report.Container, report.DockerContainerHostname)

// containerHostnameRenderer collects containers by docker hostname
type containerHostnameRenderer struct{}

func (m containerHostnameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
	containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
	ret := newJoinResults(nil)

	for _, n := range containers.Nodes {
		if n.Topology == Pseudo {
			ret.passThrough(n)
			continue
		}
		// If some some reason the container doesn't have a hostname, just drop it
		id, ok := n.Latest.Lookup(report.DockerContainerHostname)
		if !ok {
			continue
		}
		ret.addChildAndChildren(n, id, containerHostnameTopology)
	}
	return ret.result(ctx, containers)
}
