package terraform

import (
	"fmt"

	"github.com/hashicorp/terraform/config"
	"github.com/hashicorp/terraform/config/module"
	"github.com/hashicorp/terraform/dag"
)

// GraphNodeStateRepresentative is an interface that can be implemented by
// a node to say that it is representing a resource in the state.
type GraphNodeStateRepresentative interface {
	StateId() []string
}

// OrphanTransformer is a GraphTransformer that adds orphans to the
// graph. This transformer adds both resource and module orphans.
type OrphanTransformer struct {
	// Resource is resource configuration. This is only non-nil when
	// expanding a resource that is in the configuration. It can't be
	// dependend on.
	Resource *config.Resource

	// State is the global state. We require the global state to
	// properly find module orphans at our path.
	State *State

	// Module is the root module. We'll look up the proper configuration
	// using the graph path.
	Module *module.Tree

	// View, if non-nil will set a view on the module state.
	View string
}

func (t *OrphanTransformer) Transform(g *Graph) error {
	if t.State == nil {
		// If the entire state is nil, there can't be any orphans
		return nil
	}

	// Build up all our state representatives
	resourceRep := make(map[string]struct{})
	for _, v := range g.Vertices() {
		if sr, ok := v.(GraphNodeStateRepresentative); ok {
			for _, k := range sr.StateId() {
				resourceRep[k] = struct{}{}
			}
		}
	}

	var config *config.Config
	if t.Module != nil {
		if module := t.Module.Child(g.Path[1:]); module != nil {
			config = module.Config()
		}
	}

	var resourceVertexes []dag.Vertex
	if state := t.State.ModuleByPath(g.Path); state != nil {
		// If we have state, then we can have orphan resources

		// If we have a view, get the view
		if t.View != "" {
			state = state.View(t.View)
		}

		resourceOrphans := state.Orphans(config)

		resourceVertexes = make([]dag.Vertex, len(resourceOrphans))
		for i, k := range resourceOrphans {
			// If this orphan is represented by some other node somehow,
			// then ignore it.
			if _, ok := resourceRep[k]; ok {
				continue
			}

			rs := state.Resources[k]

			rsk, err := ParseResourceStateKey(k)
			if err != nil {
				return err
			}
			resourceVertexes[i] = g.Add(&graphNodeOrphanResource{
				Path:        g.Path,
				ResourceKey: rsk,
				Resource:    t.Resource,
				Provider:    rs.Provider,
				dependentOn: rs.Dependencies,
			})
		}
	}

	// Go over each module orphan and add it to the graph. We store the
	// vertexes and states outside so that we can connect dependencies later.
	moduleOrphans := t.State.ModuleOrphans(g.Path, config)
	moduleVertexes := make([]dag.Vertex, len(moduleOrphans))
	for i, path := range moduleOrphans {
		var deps []string
		if s := t.State.ModuleByPath(path); s != nil {
			deps = s.Dependencies
		}

		moduleVertexes[i] = g.Add(&graphNodeOrphanModule{
			Path:        path,
			dependentOn: deps,
		})
	}

	// Now do the dependencies. We do this _after_ adding all the orphan
	// nodes above because there are cases in which the orphans themselves
	// depend on other orphans.

	// Resource dependencies
	for _, v := range resourceVertexes {
		g.ConnectDependent(v)
	}

	// Module dependencies
	for _, v := range moduleVertexes {
		g.ConnectDependent(v)
	}

	return nil
}

// graphNodeOrphanModule is the graph vertex representing an orphan resource..
type graphNodeOrphanModule struct {
	Path []string

	dependentOn []string
}

func (n *graphNodeOrphanModule) DependableName() []string {
	return []string{n.dependableName()}
}

func (n *graphNodeOrphanModule) DependentOn() []string {
	return n.dependentOn
}

func (n *graphNodeOrphanModule) Name() string {
	return fmt.Sprintf("%s (orphan)", n.dependableName())
}

func (n *graphNodeOrphanModule) dependableName() string {
	return fmt.Sprintf("module.%s", n.Path[len(n.Path)-1])
}

// GraphNodeExpandable
func (n *graphNodeOrphanModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) {
	g, err := b.Build(n.Path)
	if err != nil {
		return nil, err
	}

	return &GraphNodeBasicSubgraph{
		NameValue: n.Name(),
		Graph:     g,
	}, nil
}

// graphNodeOrphanResource is the graph vertex representing an orphan resource..
type graphNodeOrphanResource struct {
	Path        []string
	ResourceKey *ResourceStateKey
	Resource    *config.Resource
	Provider    string

	dependentOn []string
}

func (n *graphNodeOrphanResource) ConfigType() GraphNodeConfigType {
	return GraphNodeConfigTypeResource
}

func (n *graphNodeOrphanResource) ResourceAddress() *ResourceAddress {
	return &ResourceAddress{
		Index:        n.ResourceKey.Index,
		InstanceType: TypePrimary,
		Name:         n.ResourceKey.Name,
		Path:         n.Path[1:],
		Type:         n.ResourceKey.Type,
		Mode:         n.ResourceKey.Mode,
	}
}

func (n *graphNodeOrphanResource) DependableName() []string {
	return []string{n.dependableName()}
}

func (n *graphNodeOrphanResource) DependentOn() []string {
	return n.dependentOn
}

func (n *graphNodeOrphanResource) Flatten(p []string) (dag.Vertex, error) {
	return &graphNodeOrphanResourceFlat{
		graphNodeOrphanResource: n,
		PathValue:               p,
	}, nil
}

func (n *graphNodeOrphanResource) Name() string {
	return fmt.Sprintf("%s (orphan)", n.ResourceKey)
}

func (n *graphNodeOrphanResource) ProvidedBy() []string {
	return []string{resourceProvider(n.ResourceKey.Type, n.Provider)}
}

// GraphNodeEvalable impl.
func (n *graphNodeOrphanResource) EvalTree() EvalNode {

	seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}

	// Build instance info
	info := &InstanceInfo{Id: n.ResourceKey.String(), Type: n.ResourceKey.Type}
	info.uniqueExtra = "destroy"

	seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info})

	// Each resource mode has its own lifecycle
	switch n.ResourceKey.Mode {
	case config.ManagedResourceMode:
		seq.Nodes = append(
			seq.Nodes,
			n.managedResourceEvalNodes(info)...,
		)
	case config.DataResourceMode:
		seq.Nodes = append(
			seq.Nodes,
			n.dataResourceEvalNodes(info)...,
		)
	default:
		panic(fmt.Errorf("unsupported resource mode %s", n.ResourceKey.Mode))
	}

	return seq
}

func (n *graphNodeOrphanResource) managedResourceEvalNodes(info *InstanceInfo) []EvalNode {
	var provider ResourceProvider
	var state *InstanceState

	nodes := make([]EvalNode, 0, 3)

	// Refresh the resource
	nodes = append(nodes, &EvalOpFilter{
		Ops: []walkOperation{walkRefresh},
		Node: &EvalSequence{
			Nodes: []EvalNode{
				&EvalGetProvider{
					Name:   n.ProvidedBy()[0],
					Output: &provider,
				},
				&EvalReadState{
					Name:   n.ResourceKey.String(),
					Output: &state,
				},
				&EvalRefresh{
					Info:     info,
					Provider: &provider,
					State:    &state,
					Output:   &state,
				},
				&EvalWriteState{
					Name:         n.ResourceKey.String(),
					ResourceType: n.ResourceKey.Type,
					Provider:     n.Provider,
					Dependencies: n.DependentOn(),
					State:        &state,
				},
			},
		},
	})

	// Diff the resource
	var diff *InstanceDiff
	nodes = append(nodes, &EvalOpFilter{
		Ops: []walkOperation{walkPlan, walkPlanDestroy},
		Node: &EvalSequence{
			Nodes: []EvalNode{
				&EvalReadState{
					Name:   n.ResourceKey.String(),
					Output: &state,
				},
				&EvalDiffDestroy{
					Info:   info,
					State:  &state,
					Output: &diff,
				},
				&EvalCheckPreventDestroy{
					Resource:   n.Resource,
					ResourceId: n.ResourceKey.String(),
					Diff:       &diff,
				},
				&EvalWriteDiff{
					Name: n.ResourceKey.String(),
					Diff: &diff,
				},
			},
		},
	})

	// Apply
	var err error
	nodes = append(nodes, &EvalOpFilter{
		Ops: []walkOperation{walkApply, walkDestroy},
		Node: &EvalSequence{
			Nodes: []EvalNode{
				&EvalReadDiff{
					Name: n.ResourceKey.String(),
					Diff: &diff,
				},
				&EvalGetProvider{
					Name:   n.ProvidedBy()[0],
					Output: &provider,
				},
				&EvalReadState{
					Name:   n.ResourceKey.String(),
					Output: &state,
				},
				&EvalApply{
					Info:     info,
					State:    &state,
					Diff:     &diff,
					Provider: &provider,
					Output:   &state,
					Error:    &err,
				},
				&EvalWriteState{
					Name:         n.ResourceKey.String(),
					ResourceType: n.ResourceKey.Type,
					Provider:     n.Provider,
					Dependencies: n.DependentOn(),
					State:        &state,
				},
				&EvalApplyPost{
					Info:  info,
					State: &state,
					Error: &err,
				},
				&EvalUpdateStateHook{},
			},
		},
	})

	return nodes
}

func (n *graphNodeOrphanResource) dataResourceEvalNodes(info *InstanceInfo) []EvalNode {
	nodes := make([]EvalNode, 0, 3)

	// This will remain nil, since we don't retain states for orphaned
	// data resources.
	var state *InstanceState

	// On both refresh and apply we just drop our state altogether,
	// since the config resource validation pass will have proven that the
	// resources remaining in the configuration don't need it.
	nodes = append(nodes, &EvalOpFilter{
		Ops: []walkOperation{walkRefresh, walkApply},
		Node: &EvalSequence{
			Nodes: []EvalNode{
				&EvalWriteState{
					Name:         n.ResourceKey.String(),
					ResourceType: n.ResourceKey.Type,
					Provider:     n.Provider,
					Dependencies: n.DependentOn(),
					State:        &state, // state is nil
				},
			},
		},
	})

	return nodes
}

func (n *graphNodeOrphanResource) dependableName() string {
	return n.ResourceKey.String()
}

// GraphNodeDestroyable impl.
func (n *graphNodeOrphanResource) DestroyNode() GraphNodeDestroy {
	return n
}

// GraphNodeDestroy impl.
func (n *graphNodeOrphanResource) CreateBeforeDestroy() bool {
	return false
}

func (n *graphNodeOrphanResource) CreateNode() dag.Vertex {
	return n
}

// Same as graphNodeOrphanResource, but for flattening
type graphNodeOrphanResourceFlat struct {
	*graphNodeOrphanResource

	PathValue []string
}

func (n *graphNodeOrphanResourceFlat) Name() string {
	return fmt.Sprintf(
		"%s.%s", modulePrefixStr(n.PathValue), n.graphNodeOrphanResource.Name())
}

func (n *graphNodeOrphanResourceFlat) Path() []string {
	return n.PathValue
}

// GraphNodeDestroyable impl.
func (n *graphNodeOrphanResourceFlat) DestroyNode() GraphNodeDestroy {
	return n
}

// GraphNodeDestroy impl.
func (n *graphNodeOrphanResourceFlat) CreateBeforeDestroy() bool {
	return false
}

func (n *graphNodeOrphanResourceFlat) CreateNode() dag.Vertex {
	return n
}

func (n *graphNodeOrphanResourceFlat) ProvidedBy() []string {
	return modulePrefixList(
		n.graphNodeOrphanResource.ProvidedBy(),
		modulePrefixStr(n.PathValue))
}
