package render

import (
	"context"
	"strings"

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

const (
	k8sNamespaceLabel   = "io.kubernetes.pod.namespace"
	swarmNamespaceLabel = "com.docker.stack.namespace"
)

// CustomRenderer allow for mapping functions that received the entire topology
// in one call - useful for functions that need to consider the entire graph.
// We should minimise the use of this renderer type, as it is very inflexible.
type CustomRenderer struct {
	RenderFunc func(context.Context, Nodes) Nodes
	Renderer
}

// Render implements Renderer
func (c CustomRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
	if ctx.Err() != nil {
		return Nodes{}
	}
	return c.RenderFunc(ctx, c.Renderer.Render(ctx, rpt))
}

// FilterFunc is the function type used by Filters
type FilterFunc func(report.Node) bool

// AnyFilterFunc checks if any of the filterfuncs matches.
func AnyFilterFunc(fs ...FilterFunc) FilterFunc {
	return func(n report.Node) bool {
		for _, f := range fs {
			if f(n) {
				return true
			}
		}
		return false
	}
}

// ComposeFilterFuncs composes filterfuncs into a single FilterFunc checking all.
func ComposeFilterFuncs(fs ...FilterFunc) FilterFunc {
	return func(n report.Node) bool {
		for _, f := range fs {
			if !f(n) {
				return false
			}
		}
		return true
	}
}

// Complement takes a FilterFunc f and returns a FilterFunc that has the same
// effects, if any, and returns the opposite truth value.
func Complement(f FilterFunc) FilterFunc {
	return func(node report.Node) bool { return !f(node) }
}

// Transform applies the filter to all nodes
func (f FilterFunc) Transform(nodes Nodes) Nodes {
	output := report.Nodes{}
	filtered := nodes.Filtered
	for id, node := range nodes.Nodes {
		if f(node) {
			output[id] = node
		} else {
			filtered++
		}
	}

	// Deleted nodes also need to be cut as destinations in adjacency lists.
	for id, node := range output {
		newAdjacency := report.MakeIDList()
		for _, dstID := range node.Adjacency {
			if _, ok := output[dstID]; ok {
				newAdjacency = newAdjacency.Add(dstID)
			}
		}
		node.Adjacency = newAdjacency
		output[id] = node
	}

	return Nodes{Nodes: output, Filtered: filtered}
}

// Filter removes nodes from a view based on a predicate.
type Filter struct {
	Renderer
	FilterFunc FilterFunc
}

// MakeFilter makes a new Filter (that ignores pseudo nodes).
func MakeFilter(f FilterFunc, r Renderer) Renderer {
	return Filter{
		Renderer: r,
		FilterFunc: func(n report.Node) bool {
			return n.Topology == Pseudo || f(n)
		},
	}
}

// MakeFilterPseudo makes a new Filter that will not ignore pseudo nodes.
func MakeFilterPseudo(f FilterFunc, r Renderer) Renderer {
	return Filter{
		Renderer:   r,
		FilterFunc: f,
	}
}

// Render implements Renderer
func (f Filter) Render(ctx context.Context, rpt report.Report) Nodes {
	return f.FilterFunc.Transform(f.Renderer.Render(ctx, rpt))
}

// IsConnectedMark is the key added to Node.Metadata by
// ColorConnected to indicate a node has an edge pointing to it or
// from it
const IsConnectedMark = "is_connected"

// IsConnected checks whether the node has been marked with the
// IsConnectedMark.
func IsConnected(node report.Node) bool {
	_, ok := node.Latest.Lookup(IsConnectedMark)
	return ok
}

// IsPodComponent check whether given node is everything but PV, PVC, SC
func IsPodComponent(node report.Node) bool {
	var ok bool
	ok = true
	if node.Topology == "persistent_volume" || node.Topology == "persistent_volume_claim" || node.Topology == "storage_class" {
		ok = false
	}
	return ok
}

// IsNonSnapshotComponent checks whether given node is everything but Volume Snapshot, Volume Snapshot Data
func IsNonSnapshotComponent(node report.Node) bool {
	if node.Topology == "volume_snapshot" || node.Topology == "volume_snapshot_data" {
		return false
	}
	return true
}

// connected returns the node ids of nodes which have edges to/from
// them, excluding edges to/from themselves.
func connected(nodes report.Nodes) map[string]struct{} {
	res := map[string]struct{}{}
	void := struct{}{}
	for id, node := range nodes {
		for _, adj := range node.Adjacency {
			if adj != id {
				res[id] = void
				res[adj] = void
			}
		}
	}
	return res
}

// filterInternetAdjacencies filters out edges between the incoming
// and outgoing internet node. These are typically artifacts of
// imperfect connection tracking, e.g. when VIPs and NAT traversal are
// in use.
func filterInternetAdjacencies(nodes report.Nodes) {
	incomingInternet, ok := nodes[IncomingInternetID]
	if !ok {
		return
	}
	newAdjacency := report.MakeIDList()
	for _, dstID := range incomingInternet.Adjacency {
		if dstID != OutgoingInternetID {
			newAdjacency = newAdjacency.Add(dstID)
		}
	}
	incomingInternet.Adjacency = newAdjacency
	nodes[IncomingInternetID] = incomingInternet
}

// ColorConnected colors nodes with the IsConnectedMark key if they
// have edges to or from them.  Edges to/from yourself are not counted
// here (see #656).
func ColorConnected(r Renderer) Renderer {
	return CustomRenderer{
		Renderer: r,
		RenderFunc: func(ctx context.Context, input Nodes) Nodes {
			output := input.Copy()
			for id := range connected(input.Nodes) {
				output[id] = output[id].WithLatest(IsConnectedMark, mtime.Now(), "true")
			}
			return Nodes{Nodes: output, Filtered: input.Filtered}
		},
	}
}

type filterUnconnected struct {
	onlyPseudo bool
}

// Transform implements Transformer
func (f filterUnconnected) Transform(input Nodes) Nodes {
	output := input.Nodes.Copy()
	filterInternetAdjacencies(output)
	connected := connected(output)
	filtered := input.Filtered
	for id, node := range output {
		if _, ok := connected[id]; !ok && (!f.onlyPseudo || IsPseudoTopology(node)) {
			delete(output, id)
			filtered++
		}
	}
	return Nodes{Nodes: output, Filtered: filtered}
}

// FilterUnconnected is a transformer that filters unconnected nodes
var FilterUnconnected = filterUnconnected{onlyPseudo: false}

// FilterUnconnectedPseudo is a transformer that filters unconnected
// pseudo nodes
var FilterUnconnectedPseudo = filterUnconnected{onlyPseudo: true}

// Noop allows all nodes through
func Noop(_ report.Node) bool { return true }

// IsRunning checks if the node is a running docker container
func IsRunning(n report.Node) bool {
	state, ok := n.Latest.Lookup(report.DockerContainerState)
	return !ok || (state == report.StateRunning || state == report.StateRestarting || state == report.StatePaused)
}

// IsStopped checks if the node is *not* a running docker container
var IsStopped = Complement(IsRunning)

// IsApplication checks if the node is an "application" node
func IsApplication(n report.Node) bool {
	containerName, _ := n.Latest.Lookup(report.DockerContainerName)
	if _, ok := systemContainerNames[containerName]; ok {
		return false
	}
	imageName, _ := n.Latest.Lookup(report.DockerImageName)
	imagePrefix := strings.SplitN(imageName, ":", 2)[0] // :(
	if _, ok := systemImagePrefixes[imagePrefix]; ok || report.IsPauseImageName(imagePrefix) {
		return false
	}
	k8sContainerType, _ := n.Latest.Lookup(report.DockerLabelPrefix + "io.kubernetes.docker.type")
	if k8sContainerType == "podsandbox" { // another way to detect "pause container"
		return false
	}
	roleLabel, _ := n.Latest.Lookup(report.DockerLabelPrefix + "works.weave.role")
	if roleLabel == "system" {
		return false
	}
	roleLabel, _ = n.Latest.Lookup(report.DockerImageLabelPrefix + "works.weave.role")
	if roleLabel == "system" {
		return false
	}
	namespace, _ := n.Latest.Lookup(report.DockerLabelPrefix + k8sNamespaceLabel)
	if namespace == "kube-system" {
		return false
	}
	podName, _ := n.Latest.Lookup(report.DockerLabelPrefix + "io.kubernetes.pod.name")
	if strings.HasPrefix(podName, "kube-system/") {
		return false
	}
	return true
}

// IsSystem checks if the node is a "system" node
var IsSystem = Complement(IsApplication)

// HasLabel checks if the node has the desired docker label
func HasLabel(labelKey string, labelValue string) FilterFunc {
	return func(n report.Node) bool {
		value, _ := n.Latest.Lookup(report.DockerLabelPrefix + labelKey)
		if value == labelValue {
			return true
		}
		return false
	}
}

// DoesNotHaveLabel checks if the node does NOT have the specified docker label
func DoesNotHaveLabel(labelKey string, labelValue string) FilterFunc {
	return Complement(HasLabel(labelKey, labelValue))
}

// IsNotPseudo returns true if the node is not a pseudo node
// or internet/service nodes.
func IsNotPseudo(n report.Node) bool {
	return n.Topology != Pseudo || IsInternetNode(n) || strings.HasPrefix(n.ID, ServiceNodeIDPrefix)
}

// IsNamespace checks if the node is a pod/service in the specified namespace
func IsNamespace(namespace string) FilterFunc {
	return func(n report.Node) bool {
		tryKeys := []string{report.KubernetesNamespace, report.DockerLabelPrefix + k8sNamespaceLabel, report.DockerDefaultNamespace, report.DockerLabelPrefix + swarmNamespaceLabel}
		gotNamespace := ""
		for _, key := range tryKeys {
			if value, ok := n.Latest.Lookup(key); ok {
				gotNamespace = value
				break
			}
		}
		// Special case for docker
		if namespace == report.DockerDefaultNamespace && gotNamespace == "" {
			return true
		}
		return namespace == gotNamespace
	}
}

// IsTopology checks if the node is from a particular report topology
func IsTopology(topology string) FilterFunc {
	return func(n report.Node) bool {
		return n.Topology == topology
	}
}

// IsPseudoTopology returns true if the node is in a pseudo topology,
// mimicing the check performed by MakeFilter() instead of the more complex check in IsNotPseudo()
var IsPseudoTopology = IsTopology(Pseudo)

var systemContainerNames = map[string]struct{}{
	"weavescope": {},
	"weavedns":   {},
	"weave":      {},
	"weaveproxy": {},
	"weaveexec":  {},
	"ecs-agent":  {},
}

var systemImagePrefixes = map[string]struct{}{
	"swarm":                          {},
	"weaveworks/scope":               {},
	"weaveworks/weavedns":            {},
	"weaveworks/weave":               {},
	"weaveworks/weaveproxy":          {},
	"weaveworks/weaveexec":           {},
	"amazon/amazon-ecs-agent":        {},
	"openshift/origin-pod":           {},
	"docker.io/openshift/origin-pod": {},
}
