// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

// Package langpuppet implements an integration entrypoint that combines lang
// and Puppet.
package langpuppet

import (
	"fmt"
	"strings"

	"github.com/purpleidea/mgmt/engine"
	"github.com/purpleidea/mgmt/pgraph"
)

const (
	// MergePrefixLang is how a mergeable vertex name starts in mcl code.
	MergePrefixLang = "puppet_"
	// MergePrefixPuppet is how a mergeable Puppet class name starts.
	MergePrefixPuppet = "mgmt_"
)

// mergeGraph returns the merged graph containing all vertices and edges found
// in the graphs produced by the lang and Puppet GAPIs associated with the
// wrapping GAPI. Vertices are merged if they adhere to the following rules (for
// any given value of POSTFIX): (1) The graph from lang contains a noop vertex
// named puppet_POSTFIX. (2) The graph from Puppet contains an empty class
// mgmt_POSTFIX. (3) The resulting graph will contain one noop vertex named
// POSTFIX that replaces all nodes mentioned in (1) and (2). All edges
// connecting to any of the vertices merged this way will be present in the
// merged graph.
func mergeGraphs(graphFromLang, graphFromPuppet *pgraph.Graph) (*pgraph.Graph, error) {
	if graphFromLang == nil || graphFromPuppet == nil {
		return nil, fmt.Errorf("cannot merge graphs until both child graphs are loaded")
	}

	result, err := pgraph.NewGraph(graphFromLang.Name + "+" + graphFromPuppet.Name)
	if err != nil {
		return nil, err
	}

	mergeTargets := make(map[string]pgraph.Vertex)

	// first add all vertices from the lang graph
	for _, vertex := range graphFromLang.Vertices() {
		if strings.Index(vertex.String(), "noop["+MergePrefixLang) == 0 {
			resource, ok := vertex.(engine.Res)
			if !ok {
				return nil, fmt.Errorf("vertex %s is not a named resource", vertex.String())
			}
			basename := strings.TrimPrefix(resource.Name(), MergePrefixLang)
			resource.SetName(basename)
			mergeTargets[basename] = vertex
		}
		result.AddVertex(vertex)
		for _, neighbor := range graphFromLang.OutgoingGraphVertices(vertex) {
			result.AddVertex(neighbor)
			result.AddEdge(vertex, neighbor, graphFromLang.FindEdge(vertex, neighbor))
		}
	}

	var anchor pgraph.Vertex
	mergePairs := make(map[pgraph.Vertex]pgraph.Vertex)

	// do a scan through the Puppet graph, and mark all vertices that will be
	// subject to a merge, so it will be easier do generate the new edges
	// in the final pass
	for _, vertex := range graphFromPuppet.Vertices() {
		if vertex.String() == "noop[admissible_Stage[main]]" {
			// we can start a depth first search here
			anchor = vertex
			continue
		}
		// at this stage we don't distinguis between class start and end
		if strings.Index(vertex.String(), "noop[admissible_Class["+strings.Title(MergePrefixPuppet)) != 0 &&
			strings.Index(vertex.String(), "noop[completed_Class["+strings.Title(MergePrefixPuppet)) != 0 {
			continue
		}

		resource, ok := vertex.(engine.Res)
		if !ok {
			return nil, fmt.Errorf("vertex %s is not a named resource", vertex.String())
		}
		// strip either prefix (plus the closing bracket)
		basename := strings.TrimSuffix(
			strings.TrimPrefix(
				strings.TrimPrefix(resource.Name(),
					"admissible_Class["+strings.Title(MergePrefixPuppet)),
				"completed_Class["+strings.Title(MergePrefixPuppet)),
			"]")

		if _, found := mergeTargets[basename]; !found {
			// FIXME: should be a warning not an error?
			return nil, fmt.Errorf("puppet graph has unmatched class %s%s", MergePrefixPuppet, basename)
		}

		mergePairs[vertex] = mergeTargets[basename]

		if strings.Index(resource.Name(), "admissible_Class["+strings.Title(MergePrefixPuppet)) != 0 {
			continue
		}

		// is there more than one edge outgoing from the class start?
		if graphFromPuppet.OutDegree()[vertex] > 1 {
			return nil, fmt.Errorf("class %s is not empty", basename)
		}

		// does this edge not lead to the class end?
		next := graphFromPuppet.OutgoingGraphVertices(vertex)[0]
		if next.String() != "noop[completed_Class["+strings.Title(MergePrefixPuppet)+basename+"]]" {
			return nil, fmt.Errorf("class %s%s is not empty, start is followed by %s", MergePrefixPuppet, basename, next.String())
		}
	}

	merged := make(map[pgraph.Vertex]bool)
	result.AddVertex(anchor)
	// traverse the puppet graph, add all vertices and perform merges
	// using DFS so we can be sure the "admissible" is visited before the "completed" vertex
	for _, vertex := range graphFromPuppet.DFS(anchor) {
		source := vertex

		// when adding edges, the source might be a different vertex
		// than the current one, if this is a merged vertex
		if _, found := mergePairs[vertex]; found {
			source = mergePairs[vertex]
		}

		// the current vertex has been added by previous iterations,
		// we only add neighbors here
		for _, neighbor := range graphFromPuppet.OutgoingGraphVertices(vertex) {
			if strings.Index(neighbor.String(), "noop[admissible_Class["+strings.Title(MergePrefixPuppet)) == 0 {
				result.AddEdge(source, mergePairs[neighbor], graphFromPuppet.FindEdge(vertex, neighbor))
				continue
			}
			if strings.Index(neighbor.String(), "noop[completed_Class["+strings.Title(MergePrefixPuppet)) == 0 {
				// mark target vertex as merged
				merged[mergePairs[neighbor]] = true
				continue
			}
			// if we reach here, this neighbor is a regular vertex
			result.AddVertex(neighbor)
			result.AddEdge(source, neighbor, graphFromPuppet.FindEdge(vertex, neighbor))
		}
	}

	for _, vertex := range mergeTargets {
		if !merged[vertex] {
			// FIXME: should be a warning not an error?
			return nil, fmt.Errorf("lang graph has unmatched %s", vertex.String())
		}
	}

	return result, nil
}
