// Copyright © 2016 Asteris, LLC
//
// 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 load

import (
	"bytes"
	"fmt"

	"github.com/asteris-llc/converge/fetch"
	"github.com/asteris-llc/converge/graph"
	"github.com/asteris-llc/converge/graph/node"
	"github.com/asteris-llc/converge/graph/node/conditional"
	"github.com/asteris-llc/converge/helpers/logging"
	"github.com/asteris-llc/converge/keystore"
	"github.com/asteris-llc/converge/parse"
	"github.com/asteris-llc/converge/parse/preprocessor/switch"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

type source struct {
	Parent       string
	ParentSource string
	Source       string
}

func (s *source) String() string {
	return fmt.Sprintf("%s (%s)", s.Source, s.Parent)
}

// Nodes loads and parses all resources referred to by the provided url
func Nodes(ctx context.Context, root string, verify bool) (*graph.Graph, error) {
	logger := logging.GetLogger(ctx).WithField("function", "Nodes")

	toLoad := []*source{{"root", root, root}}

	out := graph.New()
	out.Add(node.New("root", nil))

	for len(toLoad) > 0 {
		select {
		case <-ctx.Done():
			return nil, errors.New("interrupted")
		default:
		}

		current := toLoad[0]
		toLoad = toLoad[1:]

		url, err := fetch.ResolveInContext(current.Source, current.ParentSource)
		if err != nil {
			return nil, err
		}

		logger.WithField("url", url).Debug("fetching")
		content, err := fetch.Any(ctx, url)
		if err != nil {
			return nil, errors.Wrap(err, url)
		}

		if verify {
			signatureURL := url + ".asc"

			logger.WithField("signatureUrl", signatureURL).Debug("fetching")
			signature, sigErr := fetch.Any(ctx, signatureURL)
			if sigErr != nil {
				return nil, errors.Wrap(sigErr, signatureURL)
			}

			err = keystore.Default().CheckSignature(bytes.NewBuffer(content), bytes.NewBuffer(signature))
			if err != nil {
				return nil, errors.Wrap(err, signatureURL)
			}
		}

		resources, err := parse.Parse(content)
		if err != nil {
			return nil, errors.Wrap(err, url)
		}

		for _, resource := range resources {
			if control.IsSwitchNode(resource) {
				out, err = expandSwitchMacro(content, current, resource, out)
				if err != nil {
					return out, errors.Wrap(err, "unable to load resource")
				}
				continue
			}
			newID := graph.ID(current.Parent, resource.ID())
			out.Add(node.New(newID, resource))
			out.ConnectParent(current.Parent, newID)

			if resource.IsModule() {
				toLoad = append(
					toLoad,
					&source{
						Parent:       newID,
						ParentSource: url,
						Source:       resource.Source(),
					},
				)
			}
		}
	}
	return out, out.Validate()
}

// expandSwitchMacro is responsible for adding the generated switch nodes into
// the graph.  Nodes inside of the switch macro are added as children to the
// case statements, who are parents of the outer switch statement.  Actual node
// generation happens in parse/preprocessor/switch and we add the nodes into the
// graph here.
func expandSwitchMacro(data []byte, current *source, n *parse.Node, g *graph.Graph) (*graph.Graph, error) {
	if !control.IsSwitchNode(n) {
		return g, nil
	}
	switchObj, err := control.NewSwitch(n, data)
	if err != nil {
		return g, err
	}
	switchNode, err := switchObj.GenerateNode()
	if err != nil {
		return g, err
	}
	switchID := graph.ID(current.Parent, switchNode.ID())
	switchGrNode := node.New(switchID, switchNode)
	g.Add(switchGrNode)
	g.ConnectParent(current.Parent, switchID)

	switchGrNode.AddMetadata(conditional.MetaSwitchName, switchObj.Name)
	switchGrNode.AddMetadata(conditional.MetaType, conditional.NodeCatSwitch)

	var peerList []string
	for _, branch := range switchObj.BranchNames() {
		peerList = append(peerList, "macro.case."+branch)
	}

	for idx, branch := range switchObj.Branches {
		branchNode, err := branch.GenerateNode()
		if err != nil {
			return g, err
		}

		branchID := graph.ID(switchID, branchNode.ID())
		branchGrNode := node.New(branchID, branchNode)
		g.Add(branchGrNode)
		g.ConnectParent(switchID, branchID)

		branchGrNode.AddMetadata(conditional.MetaSwitchName, switchObj.Name)
		branchGrNode.AddMetadata(conditional.MetaUnrenderedPredicate, branch.Predicate)
		branchGrNode.AddMetadata(conditional.MetaBranchName, branch.Name)
		branchGrNode.AddMetadata(conditional.MetaPeers, peerList)
		branchGrNode.AddMetadata(conditional.MetaType, conditional.NodeCatBranch)

		for _, innerNode := range branch.InnerNodes {
			if err := validateInnerNode(innerNode); err != nil {
				return g, err
			}
			innerID := graph.ID(branchID, innerNode.ID())

			condNode := node.New(innerID, innerNode)
			condNode.AddMetadata(conditional.MetaSwitchName, switchObj.Name)
			condNode.AddMetadata(conditional.MetaUnrenderedPredicate, branch.Predicate)
			condNode.AddMetadata(conditional.MetaBranchName, branch.Name)
			condNode.AddMetadata(conditional.MetaPeers, peerList)
			condNode.AddMetadata(conditional.MetaType, conditional.NodeCatResource)

			g.Add(condNode)
			g.ConnectParent(branchID, innerID)
		}
		if idx > 0 {
			parent, _ := switchObj.Branches[idx-1].GenerateNode()

			g.Connect(branchID, graph.ID(switchID, parent.ID()))
		}
	}
	return g, nil
}

// validateInnerNode ensures that we do not nest control statements nor attempt
// to add modules under a switch statement.
func validateInnerNode(node *parse.Node) error {
	switch node.Kind() {
	case "module":
		return errors.New("modules not supported in conditionals")
	case "switch":
		return errors.New("nested conditionals are not supported")
	case "case":
		return errors.New("nested branches are not supported")
	}
	return nil
}
