package grapher

import (
	"strconv"
)

// Relation describes the relationship between k8s components. Type is one of CostrolRel, LabelRel, AnnotationsRel, SpecRel.
// Source and Target contains the ID of the k8s component that is either the giver or recipient of a relationship.
// All relations are bi-directional in that each object contains both the incoming and outbound relationships.
type Relation struct {
	Source int
	Target int
}

// ControlRel describes the relationship between a controller and its children pod.
type ControlRel struct {
	Relation
	Replicas int
	Template map[string]interface{}
}

// LabelRel connects objects with spec.selector with pods that have corresponding metadata.labels.
type LabelRel struct {
	Relation
}

// SpecRel connects objects via various spec properties.
type SpecRel struct {
	Relation
}

// ParsedObjs has methods GetControlRel and GetLabelRel that updates its objects array.
type ParsedObjs struct {
	Objects      []Object
	PodSelectors []string
}

// Relations is embedded into the Object struct and contains arrays of the three types of relationships.
type Relations struct {
	ControlRels []ControlRel
	LabelRels   []LabelRel
	SpecRels    []SpecRel
}

// MatchLabel is used to match Equality label selector.
type MatchLabel struct {
	key   string
	value string
}

// MatchExpression is used to match Set-based label selectors.
type MatchExpression struct {
	key      string
	operator string // In, NotIn, Exists, DoesNotExist are valid
	values   []string
}

// =============== helpers for parsing relationships from YAML ===============

// GetControlRel generates relationships and children objects for common k8s controller types.
// Note that this only includes controllers whose children are 1) pods and 2) do not have its own YAML.
// i.e. Children relies entirely on the parent's template. Controllers like CronJob are excluded because its children are not pods.
func (parsed *ParsedObjs) GetControlRel() {
	// First collect all children (Pods) that are not included in the yaml as top-level object.
	children := []Object{}
	selectors := []string{}
	for i, obj := range parsed.Objects {
		yaml := obj.RawYAML
		kind := getField(yaml, "kind")

		if kind == nil {
			kind = ""
		}

		switch kind.(string) {
		// Parse for all possible controller types
		case "Deployment", "StatefulSet", "ReplicaSet", "DaemonSet", "Job":
			rs := getField(yaml, "spec", "replicas")

			// replica defaults to 1 if unspecified
			if rs == nil {
				rs = 1
			}

			// Add Pods for controller objects
			template := getField(yaml, "spec", "template")
			if template == nil {
				continue
			}

			for j := 0; j < rs.(int); j++ {
				cid := len(parsed.Objects) + len(children)
				crel := ControlRel{
					Relation: Relation{
						Source: obj.ID,
						Target: cid,
					},
					Replicas: rs.(int),
				}

				pod := Object{
					ID:        cid,
					Kind:      "Pod",
					Name:      obj.Name + "-" + strconv.Itoa(j), // tentative name pre-deploy
					Namespace: obj.Namespace,
					RawYAML:   template.(map[string]interface{}),
					Relations: Relations{
						ControlRels: []ControlRel{
							crel,
						},
					},
				}

				children = append(children, pod)
				obj.Relations.ControlRels = append(obj.Relations.ControlRels, crel)
				parsed.Objects[i] = obj
			}

			// Get pod label selectors
			matchLabels, _ := aggregateLabelSelectors(yaml)

			// stringify selectors
			selector := ""
			for i, ml := range matchLabels {
				selector = selector + ml.key + "=" + ml.value
				if i != len(matchLabels)-1 {
					selector = selector + ","
				}
			}
			selectors = appendIfNotDuplicate(selectors, selector)
		}
	}

	// add children to the objects array at the end.
	parsed.Objects = append(parsed.Objects, children...)
	parsed.PodSelectors = selectors
}

// GetLabelRel is generates relationships between objects connected by selector-label.
// It supports both Equality-based and Set-based operators with MatchLabels and MatchExpressions, respectively.
func (parsed *ParsedObjs) GetLabelRel() {
	for i, o := range parsed.Objects {

		// Skip Pods
		yaml := o.RawYAML
		matchLabels, matchExpressions := aggregateLabelSelectors(yaml)

		// Find ID's of targets that match the label selector
		targetID := parsed.findLabelsBySelector(o.ID, matchLabels, matchExpressions)
		lrels := o.Relations.LabelRels
		for _, tid := range targetID {
			newrel := LabelRel{
				Relation{
					Source: o.ID,
					Target: tid,
				},
			}
			lrels = append(lrels, newrel)
		}

		parsed.Objects[i].Relations.LabelRels = lrels
	}
}

// GetSpecRel draws relationships between two objects that are tied via various fields in their spec.
func (parsed *ParsedObjs) GetSpecRel() {
	for i, o := range parsed.Objects {
		tid := []int{}
		switch o.Kind {
		case "ClusterRoleBinding", "RoleBinding":
			tid = parsed.findRBACTargets(o.ID, o.RawYAML)
		case "Ingress":
			rules := getField(o.RawYAML, "spec", "rules")
			if rules == nil {
				rules = []interface{}{}
			}
			for _, r := range rules.([]interface{}) {
				paths := getField(r.(map[string]interface{}), "http", "paths")
				if paths == nil {
					paths = []interface{}{}
				}
				for _, p := range paths.([]interface{}) {
					// service and resource are mutually exclusive backend types.
					name := getField(p.(map[string]interface{}), "backend", "serviceName")
					kind := "Service"
					if name == nil {
						name = getField(p.(map[string]interface{}), "backend", "service", "name")
					}
					if name == nil {
						name = getField(p.(map[string]interface{}), "backend", "resource", "name")
						kind = getField(p.(map[string]interface{}), "backend", "resource", "kind").(string)
					}
					tid = parsed.findObjectByNameAndKind(o.ID, name, kind)
				}
			}
		case "StatefulSet":
			serviceName := getField(o.RawYAML, "spec", "serviceName")
			tid = append(tid, parsed.findObjectByNameAndKind(o.ID, serviceName, "Service")...)
		case "Pod":
			volume := getField(o.RawYAML, "spec", "volumes")
			imageSecrets := getField(o.RawYAML, "spec", "ImagePullSecrets")
			serviceAccount := getField(o.RawYAML, "spec", "serviceAccountName")

			if imageSecrets == nil {
				imageSecrets = []interface{}{}
			}

			if volume == nil {
				volume = []interface{}{}
			}

			for _, sec := range imageSecrets.([]interface{}) {
				tid = append(tid, parsed.findObjectByNameAndKind(o.ID, sec, "Secret")...)
			}
			tid = append(tid, parsed.findObjectByNameAndKind(o.ID, serviceAccount, "ServiceAccount")...)

			for _, v := range volume.([]interface{}) {
				vt := v.(map[string]interface{})
				configMap := getField(vt, "configMap", "name")
				pvc := getField(vt, "persistentVolumeClaim", "claimName")
				secret := getField(vt, "secret", "secretName")

				tid = append(tid, parsed.findObjectByNameAndKind(o.ID, configMap, "ConfigMap")...)
				tid = append(tid, parsed.findObjectByNameAndKind(o.ID, pvc, "PersistentVolumeClaim")...)
				tid = append(tid, parsed.findObjectByNameAndKind(o.ID, secret, "Secret")...)
			}
		}

		// Add edges to parent
		rels := o.Relations.SpecRels
		for _, id := range tid {
			newrel := SpecRel{
				Relation{
					Source: o.ID,
					Target: id,
				},
			}
			rels = append(rels, newrel)
		}
		parsed.Objects[i].Relations.SpecRels = rels

	}
}

// ControlRel helpers
func appendIfNotDuplicate(selectors []string, selector string) []string {
	for _, e := range selectors {
		if selector == e {
			return selectors
		}
	}
	selectors = append(selectors, selector)
	return selectors
}

// LabelRel helpers
func aggregateLabelSelectors(yaml map[string]interface{}) ([]MatchLabel, []MatchExpression) {
	matchLabels := []MatchLabel{}
	matchExpressions := []MatchExpression{}

	// First check for the outdated syntax (matchLabels were added in recent k8s version)
	if l := getField(yaml, "spec", "selector"); l != nil {
		simple := true
		if ml := getField(yaml, "spec", "selector", "matchLabels"); ml != nil {
			matchLabels = addMatchLabels(matchLabels, ml.(map[string]interface{}))
			simple = false
		}

		if me := getField(yaml, "spec", "selector", "matchExpressions"); me != nil {
			for _, o := range me.([]interface{}) {
				ot := o.(map[string]interface{})
				values := []string{}
				for _, arg := range ot["values"].([]interface{}) {
					values = append(values, arg.(string))
				}
				matchExpressions = append(matchExpressions, MatchExpression{
					key:      ot["key"].(string),
					operator: ot["operator"].(string),
					values:   values,
				})
			}
			simple = false
		}

		if simple {
			matchLabels = addMatchLabels(matchLabels, l.(map[string]interface{}))
		}
	}
	return matchLabels, matchExpressions
}

// SpecRel helpers
func (parsed *ParsedObjs) findObjectByNameAndKind(parentID int, name interface{}, kind string) []int {
	targets := []int{}

	if name == nil {
		return targets
	}

	name = name.(string)
	for i, o := range parsed.Objects {
		newrel := SpecRel{
			Relation{
				Source: parentID,
				Target: o.ID,
			},
		}

		if o.Name == name && o.Kind == kind {
			// Add bidirectional link from children as well.
			parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
			targets = append(targets, o.ID)
			return targets
		}
	}
	return targets
}

func (parsed *ParsedObjs) findRBACTargets(parentID int, yaml map[string]interface{}) []int {
	roleRef := getField(yaml, "roleRef")
	subjects := getField(yaml, "subjects")
	rules := append(subjects.([]interface{}), roleRef)
	targets := []int{}
	for i, o := range parsed.Objects {
		for _, r := range rules {
			tr := r.(map[string]interface{})

			newrel := SpecRel{
				Relation{
					Source: parentID,
					Target: o.ID,
				},
			}

			// first consider case of targets added via subjects, which are namespace scoped.
			if tr["namespace"] != nil && o.Kind == tr["kind"] && o.Name == tr["name"] &&
				(o.Namespace == tr["namespace"] || o.Namespace == "default") {

				// Add bidirectional link from children as well.
				parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
				targets = append(targets, o.ID)

			} else if tr["namespace"] == nil && o.Kind == tr["kind"] && o.Name == tr["name"] {
				parsed.Objects[i].Relations.SpecRels = append(parsed.Objects[i].Relations.SpecRels, newrel)
				targets = append(targets, o.ID)
			}
		}
	}
	return targets
}

func addMatchLabels(matchLabels []MatchLabel, ml map[string]interface{}) []MatchLabel {
	for k, v := range ml {
		matchLabels = append(matchLabels, MatchLabel{
			key:   k,
			value: v.(string),
		})
	}
	return matchLabels
}

// TODO: Implement MatchExpression for set based operations.
func (parsed *ParsedObjs) findLabelsBySelector(parentID int, ml []MatchLabel, me []MatchExpression) []int {
	matchedObjs := []int{}
	for i, o := range parsed.Objects {

		// Only Pods can be selected by spec.selector
		if o.Kind != "Pod" {
			continue
		}

		// find Pods that match labels
		labels := getField(o.RawYAML, "metadata", "labels")
		match := 0
		for _, l := range ml {
			if labels.(map[string]interface{})[l.key] == l.value {
				match++
			}
		}

		// Returns only if labels meet all conditions of the selector.
		if match == len(ml) && match > 0 {
			newrel := LabelRel{
				Relation{
					Source: parentID,
					Target: o.ID,
				},
			}

			// Add bidirectional link from children as well.
			parsed.Objects[i].Relations.LabelRels = append(parsed.Objects[i].Relations.LabelRels, newrel)
			matchedObjs = append(matchedObjs, o.ID)
		}
	}
	return matchedObjs
}
