/*
Copyright 2018 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package garbagecollector

import (
	"fmt"
	"net/http"
	"strings"

	"gonum.org/v1/gonum/graph"
	"gonum.org/v1/gonum/graph/encoding"
	"gonum.org/v1/gonum/graph/encoding/dot"
	"gonum.org/v1/gonum/graph/simple"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)

type gonumVertex struct {
	uid                types.UID
	gvk                schema.GroupVersionKind
	namespace          string
	name               string
	missingFromGraph   bool
	beingDeleted       bool
	deletingDependents bool
	virtual            bool
	vertexID           int64
}

func (v *gonumVertex) ID() int64 {
	return v.vertexID
}

func (v *gonumVertex) String() string {
	kind := v.gvk.Kind + "." + v.gvk.Version
	if len(v.gvk.Group) > 0 {
		kind = kind + "." + v.gvk.Group
	}
	missing := ""
	if v.missingFromGraph {
		missing = "(missing)"
	}
	deleting := ""
	if v.beingDeleted {
		deleting = "(deleting)"
	}
	deletingDependents := ""
	if v.deletingDependents {
		deleting = "(deletingDependents)"
	}
	virtual := ""
	if v.virtual {
		virtual = "(virtual)"
	}
	return fmt.Sprintf(`%s/%s[%s]-%v%s%s%s%s`, kind, v.name, v.namespace, v.uid, missing, deleting, deletingDependents, virtual)
}

func (v *gonumVertex) Attributes() []encoding.Attribute {
	kubectlString := v.gvk.Kind + "." + v.gvk.Version
	if len(v.gvk.Group) > 0 {
		kubectlString = kubectlString + "." + v.gvk.Group
	}
	kubectlString = kubectlString + "/" + v.name

	label := fmt.Sprintf(`uid=%v
namespace=%v
%v
`,
		v.uid,
		v.namespace,
		kubectlString,
	)

	conditionStrings := []string{}
	if v.beingDeleted {
		conditionStrings = append(conditionStrings, "beingDeleted")
	}
	if v.deletingDependents {
		conditionStrings = append(conditionStrings, "deletingDependents")
	}
	if v.virtual {
		conditionStrings = append(conditionStrings, "virtual")
	}
	if v.missingFromGraph {
		conditionStrings = append(conditionStrings, "missingFromGraph")
	}
	conditionString := strings.Join(conditionStrings, ",")
	if len(conditionString) > 0 {
		label = label + conditionString + "\n"
	}

	return []encoding.Attribute{
		{Key: "label", Value: fmt.Sprintf(`"%v"`, label)},
		// these place metadata in the correct location, but don't conform to any normal attribute for rendering
		{Key: "group", Value: fmt.Sprintf(`"%v"`, v.gvk.Group)},
		{Key: "version", Value: fmt.Sprintf(`"%v"`, v.gvk.Version)},
		{Key: "kind", Value: fmt.Sprintf(`"%v"`, v.gvk.Kind)},
		{Key: "namespace", Value: fmt.Sprintf(`"%v"`, v.namespace)},
		{Key: "name", Value: fmt.Sprintf(`"%v"`, v.name)},
		{Key: "uid", Value: fmt.Sprintf(`"%v"`, v.uid)},
		{Key: "missing", Value: fmt.Sprintf(`"%v"`, v.missingFromGraph)},
		{Key: "beingDeleted", Value: fmt.Sprintf(`"%v"`, v.beingDeleted)},
		{Key: "deletingDependents", Value: fmt.Sprintf(`"%v"`, v.deletingDependents)},
		{Key: "virtual", Value: fmt.Sprintf(`"%v"`, v.virtual)},
	}
}

// NewGonumVertex creates a new gonumVertex.
func NewGonumVertex(node *node, nodeID int64) *gonumVertex {
	gv, err := schema.ParseGroupVersion(node.identity.APIVersion)
	if err != nil {
		// this indicates a bad data serialization that should be prevented during storage of the API
		utilruntime.HandleError(err)
	}
	return &gonumVertex{
		uid:                node.identity.UID,
		gvk:                gv.WithKind(node.identity.Kind),
		namespace:          node.identity.Namespace,
		name:               node.identity.Name,
		beingDeleted:       node.beingDeleted,
		deletingDependents: node.deletingDependents,
		virtual:            node.virtual,
		vertexID:           nodeID,
	}
}

// NewMissingGonumVertex creates a new gonumVertex.
func NewMissingGonumVertex(ownerRef metav1.OwnerReference, nodeID int64) *gonumVertex {
	gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
	if err != nil {
		// this indicates a bad data serialization that should be prevented during storage of the API
		utilruntime.HandleError(err)
	}
	return &gonumVertex{
		uid:              ownerRef.UID,
		gvk:              gv.WithKind(ownerRef.Kind),
		name:             ownerRef.Name,
		missingFromGraph: true,
		vertexID:         nodeID,
	}
}

func (m *concurrentUIDToNode) ToGonumGraph() graph.Directed {
	m.uidToNodeLock.Lock()
	defer m.uidToNodeLock.Unlock()

	return toGonumGraph(m.uidToNode)
}

func toGonumGraph(uidToNode map[types.UID]*node) graph.Directed {
	uidToVertex := map[types.UID]*gonumVertex{}
	graphBuilder := simple.NewDirectedGraph()

	// add the vertices first, then edges.  That avoids having to deal with missing refs.
	for _, node := range uidToNode {
		// skip adding objects that don't have owner references and aren't referred to.
		if len(node.dependents) == 0 && len(node.owners) == 0 {
			continue
		}
		vertex := NewGonumVertex(node, graphBuilder.NewNode().ID())
		uidToVertex[node.identity.UID] = vertex
		graphBuilder.AddNode(vertex)
	}
	for _, node := range uidToNode {
		currVertex := uidToVertex[node.identity.UID]
		for _, ownerRef := range node.owners {
			currOwnerVertex, ok := uidToVertex[ownerRef.UID]
			if !ok {
				currOwnerVertex = NewMissingGonumVertex(ownerRef, graphBuilder.NewNode().ID())
				uidToVertex[node.identity.UID] = currOwnerVertex
				graphBuilder.AddNode(currOwnerVertex)
			}
			graphBuilder.SetEdge(simple.Edge{
				F: currVertex,
				T: currOwnerVertex,
			})

		}
	}

	return graphBuilder
}

func (m *concurrentUIDToNode) ToGonumGraphForObj(uids ...types.UID) graph.Directed {
	m.uidToNodeLock.Lock()
	defer m.uidToNodeLock.Unlock()

	return toGonumGraphForObj(m.uidToNode, uids...)
}

func toGonumGraphForObj(uidToNode map[types.UID]*node, uids ...types.UID) graph.Directed {
	uidsToCheck := append([]types.UID{}, uids...)
	interestingNodes := map[types.UID]*node{}

	// build the set of nodes to inspect first, then use the normal construction on the subset
	for i := 0; i < len(uidsToCheck); i++ {
		uid := uidsToCheck[i]
		// if we've already been observed, there was a bug, but skip it so we don't loop forever
		if _, ok := interestingNodes[uid]; ok {
			continue
		}
		node, ok := uidToNode[uid]
		// if there is no node for the UID, skip over it.  We may add it to the list multiple times
		// but we won't loop forever and hopefully the condition doesn't happen very often
		if !ok {
			continue
		}

		interestingNodes[node.identity.UID] = node

		for _, ownerRef := range node.owners {
			// if we've already inspected this UID, don't add it to be inspected again
			if _, ok := interestingNodes[ownerRef.UID]; ok {
				continue
			}
			uidsToCheck = append(uidsToCheck, ownerRef.UID)
		}
		for dependent := range node.dependents {
			// if we've already inspected this UID, don't add it to be inspected again
			if _, ok := interestingNodes[dependent.identity.UID]; ok {
				continue
			}
			uidsToCheck = append(uidsToCheck, dependent.identity.UID)
		}
	}

	return toGonumGraph(interestingNodes)
}

// NewDebugHandler creates a new debugHTTPHandler.
func NewDebugHandler(controller *GarbageCollector) http.Handler {
	return &debugHTTPHandler{controller: controller}
}

type debugHTTPHandler struct {
	controller *GarbageCollector
}

func (h *debugHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.URL.Path != "/graph" {
		http.Error(w, "", http.StatusNotFound)
		return
	}

	var graph graph.Directed
	if uidStrings := req.URL.Query()["uid"]; len(uidStrings) > 0 {
		uids := []types.UID{}
		for _, uidString := range uidStrings {
			uids = append(uids, types.UID(uidString))
		}
		graph = h.controller.dependencyGraphBuilder.uidToNode.ToGonumGraphForObj(uids...)

	} else {
		graph = h.controller.dependencyGraphBuilder.uidToNode.ToGonumGraph()
	}

	data, err := dot.Marshal(graph, "full", "", "  ")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/vnd.graphviz")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.Write(data)
	w.WriteHeader(http.StatusOK)
}
