package terraform

import "fmt"

// Add adds the item in the state at the given address.
//
// The item can be a ModuleState, ResourceState, or InstanceState. Depending
// on the item type, the address may or may not be valid. For example, a
// module cannot be moved to a resource address, however a resource can be
// moved to a module address (it retains the same name, under that resource).
//
// The item can also be a []*ModuleState, which is the case for nested
// modules. In this case, Add will expect the zero-index to be the top-most
// module to add and will only nest children from there. For semantics, this
// is equivalent to module => module.
//
// The full semantics of Add:
//
//                      ┌───────────────────┬───────────────────┬───────────────────┐
//                      │  Module Address   │ Resource Address  │ Instance Address  │
//    ┌─────────────────┼───────────────────┼───────────────────┼───────────────────┤
//    │   ModuleState   │         ✓         │         x         │         x         │
//    ├─────────────────┼───────────────────┼───────────────────┼───────────────────┤
//    │  ResourceState  │         ✓         │         ✓         │      maybe*       │
//    ├─────────────────┼───────────────────┼───────────────────┼───────────────────┤
//    │ Instance State  │         ✓         │         ✓         │         ✓         │
//    └─────────────────┴───────────────────┴───────────────────┴───────────────────┘
//
// *maybe - Resources can be added at an instance address only if the resource
//          represents a single instance (primary). Example:
//          "aws_instance.foo" can be moved to "aws_instance.bar.tainted"
//
func (s *State) Add(fromAddrRaw string, toAddrRaw string, raw interface{}) error {
	// Parse the address

	toAddr, err := ParseResourceAddress(toAddrRaw)
	if err != nil {
		return err
	}

	// Parse the from address
	fromAddr, err := ParseResourceAddress(fromAddrRaw)
	if err != nil {
		return err
	}

	// Determine the types
	from := detectValueAddLoc(raw)
	to := detectAddrAddLoc(toAddr)

	// Find the function to do this
	fromMap, ok := stateAddFuncs[from]
	if !ok {
		return fmt.Errorf("invalid source to add to state: %T", raw)
	}
	f, ok := fromMap[to]
	if !ok {
		return fmt.Errorf("invalid destination: %s (%d)", toAddr, to)
	}

	// Call the migrator
	if err := f(s, fromAddr, toAddr, raw); err != nil {
		return err
	}

	// Prune the state
	s.prune()
	return nil
}

func stateAddFunc_Module_Module(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error {
	// raw can be either *ModuleState or []*ModuleState. The former means
	// we're moving just one module. The latter means we're moving a module
	// and children.
	root := raw
	var rest []*ModuleState
	if list, ok := raw.([]*ModuleState); ok {
		// We need at least one item
		if len(list) == 0 {
			return fmt.Errorf("module move with no value to: %s", addr)
		}

		// The first item is always the root
		root = list[0]
		if len(list) > 1 {
			rest = list[1:]
		}
	}

	// Get the actual module state
	src := root.(*ModuleState).deepcopy()

	// If the target module exists, it is an error
	path := append([]string{"root"}, addr.Path...)
	if s.ModuleByPath(path) != nil {
		return fmt.Errorf("module target is not empty: %s", addr)
	}

	// Create it and copy our outputs and dependencies
	mod := s.AddModule(path)
	mod.Outputs = src.Outputs
	mod.Dependencies = src.Dependencies

	// Go through the resources perform an add for each of those
	for k, v := range src.Resources {
		resourceKey, err := ParseResourceStateKey(k)
		if err != nil {
			return err
		}

		// Update the resource address for this
		addrCopy := *addr
		addrCopy.Type = resourceKey.Type
		addrCopy.Name = resourceKey.Name
		addrCopy.Index = resourceKey.Index
		addrCopy.Mode = resourceKey.Mode

		// Perform an add
		if err := s.Add(fromAddr.String(), addrCopy.String(), v); err != nil {
			return err
		}
	}

	// Add all the children if we have them
	for _, item := range rest {
		// If item isn't a descendent of our root, then ignore it
		if !src.IsDescendent(item) {
			continue
		}

		// It is! Strip the leading prefix and attach that to our address
		extra := item.Path[len(src.Path):]
		addrCopy := addr.Copy()
		addrCopy.Path = append(addrCopy.Path, extra...)

		// Add it
		s.Add(fromAddr.String(), addrCopy.String(), item)
	}

	return nil
}

func stateAddFunc_Resource_Module(
	s *State, from, to *ResourceAddress, raw interface{}) error {
	// Build the more specific to addr
	addr := *to
	addr.Type = from.Type
	addr.Name = from.Name

	return s.Add(from.String(), addr.String(), raw)
}

func stateAddFunc_Resource_Resource(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error {
	// raw can be either *ResourceState or []*ResourceState. The former means
	// we're moving just one resource. The latter means we're moving a count
	// of resources.
	if list, ok := raw.([]*ResourceState); ok {
		// We need at least one item
		if len(list) == 0 {
			return fmt.Errorf("resource move with no value to: %s", addr)
		}

		// If there is an index, this is an error since we can't assign
		// a set of resources to a single index
		if addr.Index >= 0 && len(list) > 1 {
			return fmt.Errorf(
				"multiple resources can't be moved to a single index: "+
					"%s => %s", fromAddr, addr)
		}

		// Add each with a specific index
		for i, rs := range list {
			addrCopy := addr.Copy()
			addrCopy.Index = i

			if err := s.Add(fromAddr.String(), addrCopy.String(), rs); err != nil {
				return err
			}
		}

		return nil
	}

	src := raw.(*ResourceState).deepcopy()

	// Initialize the resource
	resourceRaw, exists := stateAddInitAddr(s, addr)
	if exists {
		return fmt.Errorf("resource exists and not empty: %s", addr)
	}
	resource := resourceRaw.(*ResourceState)
	resource.Type = src.Type
	resource.Dependencies = src.Dependencies
	resource.Provider = src.Provider

	// Move the primary
	if src.Primary != nil {
		addrCopy := *addr
		addrCopy.InstanceType = TypePrimary
		addrCopy.InstanceTypeSet = true
		if err := s.Add(fromAddr.String(), addrCopy.String(), src.Primary); err != nil {
			return err
		}
	}

	// Move all deposed
	if len(src.Deposed) > 0 {
		resource.Deposed = src.Deposed
	}

	return nil
}

func stateAddFunc_Instance_Instance(s *State, fromAddr, addr *ResourceAddress, raw interface{}) error {
	src := raw.(*InstanceState).DeepCopy()

	// Create the instance
	instanceRaw, _ := stateAddInitAddr(s, addr)
	instance := instanceRaw.(*InstanceState)

	// Set it
	instance.Set(src)

	return nil
}

func stateAddFunc_Instance_Module(
	s *State, from, to *ResourceAddress, raw interface{}) error {
	addr := *to
	addr.Type = from.Type
	addr.Name = from.Name

	return s.Add(from.String(), addr.String(), raw)
}

func stateAddFunc_Instance_Resource(
	s *State, from, to *ResourceAddress, raw interface{}) error {
	addr := *to
	addr.InstanceType = TypePrimary
	addr.InstanceTypeSet = true

	return s.Add(from.String(), addr.String(), raw)
}

// stateAddFunc is the type of function for adding an item to a state
type stateAddFunc func(s *State, from, to *ResourceAddress, item interface{}) error

// stateAddFuncs has the full matrix mapping of the state adders.
var stateAddFuncs map[stateAddLoc]map[stateAddLoc]stateAddFunc

func init() {
	stateAddFuncs = map[stateAddLoc]map[stateAddLoc]stateAddFunc{
		stateAddModule: {
			stateAddModule: stateAddFunc_Module_Module,
		},
		stateAddResource: {
			stateAddModule:   stateAddFunc_Resource_Module,
			stateAddResource: stateAddFunc_Resource_Resource,
		},
		stateAddInstance: {
			stateAddInstance: stateAddFunc_Instance_Instance,
			stateAddModule:   stateAddFunc_Instance_Module,
			stateAddResource: stateAddFunc_Instance_Resource,
		},
	}
}

// stateAddLoc is an enum to represent the location where state is being
// moved from/to. We use this for quick lookups in a function map.
type stateAddLoc uint

const (
	stateAddInvalid stateAddLoc = iota
	stateAddModule
	stateAddResource
	stateAddInstance
)

// detectAddrAddLoc detects the state type for the given address. This
// function is specifically not unit tested since we consider the State.Add
// functionality to be comprehensive enough to cover this.
func detectAddrAddLoc(addr *ResourceAddress) stateAddLoc {
	if addr.Name == "" {
		return stateAddModule
	}

	if !addr.InstanceTypeSet {
		return stateAddResource
	}

	return stateAddInstance
}

// detectValueAddLoc determines the stateAddLoc value from the raw value
// that is some State structure.
func detectValueAddLoc(raw interface{}) stateAddLoc {
	switch raw.(type) {
	case *ModuleState:
		return stateAddModule
	case []*ModuleState:
		return stateAddModule
	case *ResourceState:
		return stateAddResource
	case []*ResourceState:
		return stateAddResource
	case *InstanceState:
		return stateAddInstance
	default:
		return stateAddInvalid
	}
}

// stateAddInitAddr takes a ResourceAddress and creates the non-existing
// resources up to that point, returning the empty (or existing) interface
// at that address.
func stateAddInitAddr(s *State, addr *ResourceAddress) (interface{}, bool) {
	addType := detectAddrAddLoc(addr)

	// Get the module
	path := append([]string{"root"}, addr.Path...)
	exists := true
	mod := s.ModuleByPath(path)
	if mod == nil {
		mod = s.AddModule(path)
		exists = false
	}
	if addType == stateAddModule {
		return mod, exists
	}

	// Add the resource
	resourceKey := (&ResourceStateKey{
		Name:  addr.Name,
		Type:  addr.Type,
		Index: addr.Index,
		Mode:  addr.Mode,
	}).String()
	exists = true
	resource, ok := mod.Resources[resourceKey]
	if !ok {
		resource = &ResourceState{Type: addr.Type}
		resource.init()
		mod.Resources[resourceKey] = resource
		exists = false
	}
	if addType == stateAddResource {
		return resource, exists
	}

	// Get the instance
	exists = true
	instance := &InstanceState{}
	switch addr.InstanceType {
	case TypePrimary, TypeTainted:
		if v := resource.Primary; v != nil {
			instance = resource.Primary
		} else {
			exists = false
		}
	case TypeDeposed:
		idx := addr.Index
		if addr.Index < 0 {
			idx = 0
		}
		if len(resource.Deposed) > idx {
			instance = resource.Deposed[idx]
		} else {
			resource.Deposed = append(resource.Deposed, instance)
			exists = false
		}
	}

	return instance, exists
}
