package addrs

import (
	"fmt"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/gocty"

	"github.com/camptocamp/terraboard/internal/terraform/tfdiags"
)

// ModuleInstance is an address for a particular module instance within the
// dynamic module tree. This is an extension of the static traversals
// represented by type Module that deals with the possibility of a single
// module call producing multiple instances via the "count" and "for_each"
// arguments.
//
// Although ModuleInstance is a slice, it should be treated as immutable after
// creation.
type ModuleInstance []ModuleInstanceStep

var (
	_ Targetable = ModuleInstance(nil)
)

func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) {
	mi, remain, diags := parseModuleInstancePrefix(traversal)
	if len(remain) != 0 {
		if len(remain) == len(traversal) {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid module instance address",
				Detail:   "A module instance address must begin with \"module.\".",
				Subject:  remain.SourceRange().Ptr(),
			})
		} else {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid module instance address",
				Detail:   "The module instance address is followed by additional invalid content.",
				Subject:  remain.SourceRange().Ptr(),
			})
		}
	}
	return mi, diags
}

// ParseModuleInstanceStr is a helper wrapper around ParseModuleInstance
// that takes a string and parses it with the HCL native syntax traversal parser
// before interpreting it.
//
// This should be used only in specialized situations since it will cause the
// created references to not have any meaningful source location information.
// If a reference string is coming from a source that should be identified in
// error messages then the caller should instead parse it directly using a
// suitable function from the HCL API and pass the traversal itself to
// ParseModuleInstance.
//
// Error diagnostics are returned if either the parsing fails or the analysis
// of the traversal fails. There is no way for the caller to distinguish the
// two kinds of diagnostics programmatically. If error diagnostics are returned
// then the returned address is invalid.
func ParseModuleInstanceStr(str string) (ModuleInstance, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
	diags = diags.Append(parseDiags)
	if parseDiags.HasErrors() {
		return nil, diags
	}

	addr, addrDiags := ParseModuleInstance(traversal)
	diags = diags.Append(addrDiags)
	return addr, diags
}

func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) {
	remain := traversal
	var mi ModuleInstance
	var diags tfdiags.Diagnostics

LOOP:
	for len(remain) > 0 {
		var next string
		switch tt := remain[0].(type) {
		case hcl.TraverseRoot:
			next = tt.Name
		case hcl.TraverseAttr:
			next = tt.Name
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid address operator",
				Detail:   "Module address prefix must be followed by dot and then a name.",
				Subject:  remain[0].SourceRange().Ptr(),
			})
			break LOOP
		}

		if next != "module" {
			break
		}

		kwRange := remain[0].SourceRange()
		remain = remain[1:]
		// If we have the prefix "module" then we should be followed by an
		// module call name, as an attribute, and then optionally an index step
		// giving the instance key.
		if len(remain) == 0 {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid address operator",
				Detail:   "Prefix \"module.\" must be followed by a module name.",
				Subject:  &kwRange,
			})
			break
		}

		var moduleName string
		switch tt := remain[0].(type) {
		case hcl.TraverseAttr:
			moduleName = tt.Name
		default:
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid address operator",
				Detail:   "Prefix \"module.\" must be followed by a module name.",
				Subject:  remain[0].SourceRange().Ptr(),
			})
			break LOOP
		}
		remain = remain[1:]
		step := ModuleInstanceStep{
			Name: moduleName,
		}

		if len(remain) > 0 {
			if idx, ok := remain[0].(hcl.TraverseIndex); ok {
				remain = remain[1:]

				switch idx.Key.Type() {
				case cty.String:
					step.InstanceKey = StringKey(idx.Key.AsString())
				case cty.Number:
					var idxInt int
					err := gocty.FromCtyValue(idx.Key, &idxInt)
					if err == nil {
						step.InstanceKey = IntKey(idxInt)
					} else {
						diags = diags.Append(&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "Invalid address operator",
							Detail:   fmt.Sprintf("Invalid module index: %s.", err),
							Subject:  idx.SourceRange().Ptr(),
						})
					}
				default:
					// Should never happen, because no other types are allowed in traversal indices.
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid address operator",
						Detail:   "Invalid module key: must be either a string or an integer.",
						Subject:  idx.SourceRange().Ptr(),
					})
				}
			}
		}

		mi = append(mi, step)
	}

	var retRemain hcl.Traversal
	if len(remain) > 0 {
		retRemain = make(hcl.Traversal, len(remain))
		copy(retRemain, remain)
		// The first element here might be either a TraverseRoot or a
		// TraverseAttr, depending on whether we had a module address on the
		// front. To make life easier for callers, we'll normalize to always
		// start with a TraverseRoot.
		if tt, ok := retRemain[0].(hcl.TraverseAttr); ok {
			retRemain[0] = hcl.TraverseRoot{
				Name:     tt.Name,
				SrcRange: tt.SrcRange,
			}
		}
	}

	return mi, retRemain, diags
}

// UnkeyedInstanceShim is a shim method for converting a Module address to the
// equivalent ModuleInstance address that assumes that no modules have
// keyed instances.
//
// This is a temporary allowance for the fact that Terraform does not presently
// support "count" and "for_each" on modules, and thus graph building code that
// derives graph nodes from configuration must just assume unkeyed modules
// in order to construct the graph. At a later time when "count" and "for_each"
// support is added for modules, all callers of this method will need to be
// reworked to allow for keyed module instances.
func (m Module) UnkeyedInstanceShim() ModuleInstance {
	path := make(ModuleInstance, len(m))
	for i, name := range m {
		path[i] = ModuleInstanceStep{Name: name}
	}
	return path
}

// ModuleInstanceStep is a single traversal step through the dynamic module
// tree. It is used only as part of ModuleInstance.
type ModuleInstanceStep struct {
	Name        string
	InstanceKey InstanceKey
}

// RootModuleInstance is the module instance address representing the root
// module, which is also the zero value of ModuleInstance.
var RootModuleInstance ModuleInstance

// IsRoot returns true if the receiver is the address of the root module instance,
// or false otherwise.
func (m ModuleInstance) IsRoot() bool {
	return len(m) == 0
}

// Child returns the address of a child module instance of the receiver,
// identified by the given name and key.
func (m ModuleInstance) Child(name string, key InstanceKey) ModuleInstance {
	ret := make(ModuleInstance, 0, len(m)+1)
	ret = append(ret, m...)
	return append(ret, ModuleInstanceStep{
		Name:        name,
		InstanceKey: key,
	})
}

// ChildCall returns the address of a module call within the receiver,
// identified by the given name.
func (m ModuleInstance) ChildCall(name string) AbsModuleCall {
	return AbsModuleCall{
		Module: m,
		Call:   ModuleCall{Name: name},
	}
}

// Parent returns the address of the parent module instance of the receiver, or
// the receiver itself if there is no parent (if it's the root module address).
func (m ModuleInstance) Parent() ModuleInstance {
	if len(m) == 0 {
		return m
	}
	return m[:len(m)-1]
}

// String returns a string representation of the receiver, in the format used
// within e.g. user-provided resource addresses.
//
// The address of the root module has the empty string as its representation.
func (m ModuleInstance) String() string {
	if len(m) == 0 {
		return ""
	}
	// Calculate minimal necessary space (no instance keys).
	l := 0
	for _, step := range m {
		l += len(step.Name)
	}
	buf := strings.Builder{}
	// 8 is len(".module.") which separates entries.
	buf.Grow(l + len(m)*8)
	sep := ""
	for _, step := range m {
		buf.WriteString(sep)
		buf.WriteString("module.")
		buf.WriteString(step.Name)
		if step.InstanceKey != NoKey {
			buf.WriteString(step.InstanceKey.String())
		}
		sep = "."
	}
	return buf.String()
}

type moduleInstanceKey string

func (m ModuleInstance) UniqueKey() UniqueKey {
	return moduleInstanceKey(m.String())
}

func (mk moduleInstanceKey) uniqueKeySigil() {}

// Equal returns true if the receiver and the given other value
// contains the exact same parts.
func (m ModuleInstance) Equal(o ModuleInstance) bool {
	if len(m) != len(o) {
		return false
	}

	for i := range m {
		if m[i] != o[i] {
			return false
		}
	}
	return true
}

// Less returns true if the receiver should sort before the given other value
// in a sorted list of addresses.
func (m ModuleInstance) Less(o ModuleInstance) bool {
	if len(m) != len(o) {
		// Shorter path sorts first.
		return len(m) < len(o)
	}

	for i := range m {
		mS, oS := m[i], o[i]
		switch {
		case mS.Name != oS.Name:
			return mS.Name < oS.Name
		case mS.InstanceKey != oS.InstanceKey:
			return InstanceKeyLess(mS.InstanceKey, oS.InstanceKey)
		}
	}

	return false
}

// Ancestors returns a slice containing the receiver and all of its ancestor
// module instances, all the way up to (and including) the root module.
// The result is ordered by depth, with the root module always first.
//
// Since the result always includes the root module, a caller may choose to
// ignore it by slicing the result with [1:].
func (m ModuleInstance) Ancestors() []ModuleInstance {
	ret := make([]ModuleInstance, 0, len(m)+1)
	for i := 0; i <= len(m); i++ {
		ret = append(ret, m[:i])
	}
	return ret
}

// IsAncestor returns true if the receiver is an ancestor of the given
// other value.
func (m ModuleInstance) IsAncestor(o ModuleInstance) bool {
	// Longer or equal sized paths means the receiver cannot
	// be an ancestor of the given module insatnce.
	if len(m) >= len(o) {
		return false
	}

	for i, ms := range m {
		if ms.Name != o[i].Name {
			return false
		}
		if ms.InstanceKey != NoKey && ms.InstanceKey != o[i].InstanceKey {
			return false
		}
	}

	return true
}

// Call returns the module call address that corresponds to the given module
// instance, along with the address of the module instance that contains it.
//
// There is no call for the root module, so this method will panic if called
// on the root module address.
//
// A single module call can produce potentially many module instances, so the
// result discards any instance key that might be present on the last step
// of the instance. To retain this, use CallInstance instead.
//
// In practice, this just turns the last element of the receiver into a
// ModuleCall and then returns a slice of the receiever that excludes that
// last part. This is just a convenience for situations where a call address
// is required, such as when dealing with *Reference and Referencable values.
func (m ModuleInstance) Call() (ModuleInstance, ModuleCall) {
	if len(m) == 0 {
		panic("cannot produce ModuleCall for root module")
	}

	inst, lastStep := m[:len(m)-1], m[len(m)-1]
	return inst, ModuleCall{
		Name: lastStep.Name,
	}
}

// CallInstance returns the module call instance address that corresponds to
// the given module instance, along with the address of the module instance
// that contains it.
//
// There is no call for the root module, so this method will panic if called
// on the root module address.
//
// In practice, this just turns the last element of the receiver into a
// ModuleCallInstance and then returns a slice of the receiever that excludes
// that last part. This is just a convenience for situations where a call\
// address is required, such as when dealing with *Reference and Referencable
// values.
func (m ModuleInstance) CallInstance() (ModuleInstance, ModuleCallInstance) {
	if len(m) == 0 {
		panic("cannot produce ModuleCallInstance for root module")
	}

	inst, lastStep := m[:len(m)-1], m[len(m)-1]
	return inst, ModuleCallInstance{
		Call: ModuleCall{
			Name: lastStep.Name,
		},
		Key: lastStep.InstanceKey,
	}
}

// TargetContains implements Targetable by returning true if the given other
// address either matches the receiver, is a sub-module-instance of the
// receiver, or is a targetable absolute address within a module that
// is contained within the reciever.
func (m ModuleInstance) TargetContains(other Targetable) bool {
	switch to := other.(type) {
	case Module:
		if len(to) < len(m) {
			// Can't be contained if the path is shorter
			return false
		}
		// Other is contained if its steps match for the length of our own path.
		for i, ourStep := range m {
			otherStep := to[i]

			// We can't contain an entire module if we have a specific instance
			// key. The case of NoKey is OK because this address is either
			// meant to address an unexpanded module, or a single instance of
			// that module, and both of those are a covered in-full by the
			// Module address.
			if ourStep.InstanceKey != NoKey {
				return false
			}

			if ourStep.Name != otherStep {
				return false
			}
		}
		// If we fall out here then the prefixed matched, so it's contained.
		return true

	case ModuleInstance:
		if len(to) < len(m) {
			return false
		}
		for i, ourStep := range m {
			otherStep := to[i]

			if ourStep.Name != otherStep.Name {
				return false
			}

			// if this is our last step, because all targets are parsed as
			// instances, this may be a ModuleInstance intended to be used as a
			// Module.
			if i == len(m)-1 {
				if ourStep.InstanceKey == NoKey {
					// If the other step is a keyed instance, then we contain that
					// step, and if it isn't it's a match, which is true either way
					return true
				}
			}

			if ourStep.InstanceKey != otherStep.InstanceKey {
				return false
			}

		}
		return true

	case ConfigResource:
		return m.TargetContains(to.Module)

	case AbsResource:
		return m.TargetContains(to.Module)

	case AbsResourceInstance:
		return m.TargetContains(to.Module)

	default:
		return false
	}
}

// Module returns the address of the module that this instance is an instance
// of.
func (m ModuleInstance) Module() Module {
	if len(m) == 0 {
		return nil
	}
	ret := make(Module, len(m))
	for i, step := range m {
		ret[i] = step.Name
	}
	return ret
}

func (m ModuleInstance) AddrType() TargetableAddrType {
	return ModuleInstanceAddrType
}

func (m ModuleInstance) targetableSigil() {
	// ModuleInstance is targetable
}

func (m ModuleInstance) absMoveableSigil() {
	// ModuleInstance is moveable
}

// IsDeclaredByCall returns true if the receiver is an instance of the given
// AbsModuleCall.
func (m ModuleInstance) IsDeclaredByCall(other AbsModuleCall) bool {
	// Compare len(m) to len(other.Module+1) because the final module instance
	// step in other is stored in the AbsModuleCall.Call
	if len(m) > len(other.Module)+1 || len(m) == 0 && len(other.Module) == 0 {
		return false
	}

	// Verify that the other's ModuleInstance matches the receiver.
	inst, lastStep := other.Module, other.Call
	for i := range inst {
		if inst[i] != m[i] {
			return false
		}
	}

	// Now compare the final step of the received with the other Call, where
	// only the name needs to match.
	return lastStep.Name == m[len(m)-1].Name
}

func (s ModuleInstanceStep) String() string {
	if s.InstanceKey != NoKey {
		return s.Name + s.InstanceKey.String()
	}
	return s.Name
}
