package report

// Backwards-compatibility: code to read older reports and convert

import (
	"strings"
	"time"

	"github.com/ugorji/go/codec"
)

// For backwards-compatibility with probes that sent a map of latestControls data
type bcNode struct {
	Node
	LatestControls map[string]nodeControlDataLatestEntry `json:"latestControls,omitempty"`
	Counters       map[string]int                        `json:"counters,omitempty"`
}

type nodeControlDataLatestEntry struct {
	Timestamp time.Time       `json:"timestamp"`
	Value     nodeControlData `json:"value"`
}

type nodeControlData struct {
	Dead bool `json:"dead"`
}

// CodecDecodeSelf implements codec.Selfer
func (n *Node) CodecDecodeSelf(decoder *codec.Decoder) {
	var in bcNode
	decoder.Decode(&in)
	*n = in.Node
	if len(in.LatestControls) > 0 {
		// Convert the map into a delimited string
		cs := make([]string, 0, len(in.LatestControls))
		var ts time.Time
		for name, v := range in.LatestControls {
			if !v.Value.Dead {
				cs = append(cs, name)
				// Pull out the newest timestamp to use for the whole set
				if ts.Before(v.Timestamp) {
					ts = v.Timestamp
				}
			}
		}
		n.Latest = n.Latest.Set(NodeActiveControls, ts, strings.Join(cs, ScopeDelim))
	}
	// Counters were not generated in the Scope probe, but decode them here in case a plugin used them.
	for k, v := range in.Counters {
		*n = n.AddCounter(k, v)
	}
}

type _Node Node // just so we don't recurse inside CodecEncodeSelf

// CodecEncodeSelf implements codec.Selfer
func (n *Node) CodecEncodeSelf(encoder *codec.Encoder) {
	encoder.Encode((*_Node)(n))
}

// Upgrade returns a new report based on a report received from the old probe.
//
func (r Report) Upgrade() Report {
	return r.upgradePodNodes().upgradeNamespaces().upgradeDNSRecords()
}

func (r Report) upgradePodNodes() Report {
	// At the same time the probe stopped reporting replicasets,
	// it also started reporting deployments as pods' parents
	if len(r.ReplicaSet.Nodes) == 0 {
		return r
	}

	// For each pod, we check for any replica sets, and merge any deployments they point to
	// into a replacement Parents value.
	nodes := Nodes{}
	for podID, pod := range r.Pod.Nodes {
		if replicaSetIDs, ok := pod.Parents.Lookup(ReplicaSet); ok {
			newParents := pod.Parents.Delete(ReplicaSet)
			for _, replicaSetID := range replicaSetIDs {
				if replicaSet, ok := r.ReplicaSet.Nodes[replicaSetID]; ok {
					if deploymentIDs, ok := replicaSet.Parents.Lookup(Deployment); ok {
						newParents = newParents.Add(Deployment, deploymentIDs)
					}
				}
			}
			// newParents contains a copy of the current parents without replicasets,
			// PruneParents().WithParents() ensures replicasets are actually deleted
			pod = pod.PruneParents().WithParents(newParents)
		}
		nodes[podID] = pod
	}
	r.Pod.Nodes = nodes

	return r
}

func (r Report) upgradeNamespaces() Report {
	if len(r.Namespace.Nodes) > 0 {
		return r
	}

	namespaces := map[string]struct{}{}
	for _, t := range []Topology{r.Pod, r.Service, r.Deployment, r.DaemonSet, r.StatefulSet, r.CronJob} {
		for _, n := range t.Nodes {
			if state, ok := n.Latest.Lookup(KubernetesState); ok && state == "deleted" {
				continue
			}
			if namespace, ok := n.Latest.Lookup(KubernetesNamespace); ok {
				namespaces[namespace] = struct{}{}
			}
		}
	}

	nodes := make(Nodes, len(namespaces))
	for ns := range namespaces {
		// Namespace ID:
		// Probes did not use to report namespace ids, but since creating a report node requires an id,
		// the namespace name, which is unique, is passed to `MakeNamespaceNodeID`
		namespaceID := MakeNamespaceNodeID(ns)
		nodes[namespaceID] = MakeNodeWith(namespaceID, map[string]string{KubernetesName: ns})
	}
	r.Namespace.Nodes = nodes

	return r
}

func (r Report) upgradeDNSRecords() Report {
	// For release 1.11.6, probes accidentally sent DNS records labeled "nodes".
	// Translate the incorrect version here. Accident was in commit 951629a.
	if len(r.BugDNS) > 0 {
		r.DNS = r.BugDNS
		r.BugDNS = nil
	}
	if len(r.DNS) > 0 {
		return r
	}
	dns := make(DNSRecords)
	for endpointID, endpoint := range r.Endpoint.Nodes {
		_, addr, _, ok := ParseEndpointNodeID(endpointID)
		snoopedNames, foundS := endpoint.Sets.Lookup(SnoopedDNSNames)
		reverseNames, foundR := endpoint.Sets.Lookup(ReverseDNSNames)
		if ok && (foundS || foundR) {
			// Add address and names to report-level map
			if existing, found := dns[addr]; found {
				var sUnchanged, rUnchanged bool
				snoopedNames, sUnchanged = snoopedNames.Merge(existing.Forward)
				reverseNames, rUnchanged = reverseNames.Merge(existing.Reverse)
				if sUnchanged && rUnchanged {
					continue
				}
			}
			dns[addr] = DNSRecord{Forward: snoopedNames, Reverse: reverseNames}
		}
	}
	r.DNS = dns
	return r
}
