package addrs

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/camptocamp/terraboard/internal/terraform/tfdiags"
	"github.com/zclconf/go-cty/cty"
)

// anyKeyImpl is the InstanceKey representation indicating a wildcard, which
// matches all possible keys. This is only used internally for matching
// combinations of address types, where only portions of the path contain key
// information.
type anyKeyImpl rune

func (k anyKeyImpl) instanceKeySigil() {
}

func (k anyKeyImpl) String() string {
	return fmt.Sprintf("[%s]", string(k))
}

func (k anyKeyImpl) Value() cty.Value {
	return cty.StringVal(string(k))
}

// anyKey is the only valid value of anyKeyImpl
var anyKey = anyKeyImpl('*')

// MoveEndpointInModule annotates a MoveEndpoint with the address of the
// module where it was declared, which is the form we use for resolving
// whether move statements chain from or are nested within other move
// statements.
type MoveEndpointInModule struct {
	// SourceRange is the location of the physical endpoint address
	// in configuration, if this MoveEndpoint was decoded from a
	// configuration expresson.
	SourceRange tfdiags.SourceRange

	// The internals are unexported here because, as with MoveEndpoint,
	// we're somewhat abusing AbsMoveable here to represent an address
	// relative to the module, rather than as an absolute address.
	// Conceptually, the following two fields represent a matching pattern
	// for AbsMoveables where the elements of "module" behave as
	// ModuleInstanceStep values with a wildcard instance key, because
	// a moved block in a module affects all instances of that module.
	// Unlike MoveEndpoint, relSubject in this case can be any of the
	// address types that implement AbsMoveable.
	module     Module
	relSubject AbsMoveable
}

// ImpliedMoveStatementEndpoint is a special constructor for MoveEndpointInModule
// which is suitable only for constructing "implied" move statements, which
// means that we inferred the statement automatically rather than building it
// from an explicit block in the configuration.
//
// Implied move endpoints, just as for the statements they are embedded in,
// have somewhat-related-but-imprecise source ranges, typically referring to
// some general configuration construct that implied the statement, because
// by definition there is no explicit move endpoint expression in this case.
func ImpliedMoveStatementEndpoint(addr AbsResourceInstance, rng tfdiags.SourceRange) *MoveEndpointInModule {
	// implied move endpoints always belong to the root module, because each
	// one refers to a single resource instance inside a specific module
	// instance, rather than all instances of the module where the resource
	// was declared.
	return &MoveEndpointInModule{
		SourceRange: rng,
		module:      RootModule,
		relSubject:  addr,
	}
}

func (e *MoveEndpointInModule) ObjectKind() MoveEndpointKind {
	return absMoveableEndpointKind(e.relSubject)
}

// String produces a string representation of the object matching pattern
// represented by the reciever.
//
// Since there is no direct syntax for representing such an object matching
// pattern, this function uses a splat-operator-like representation to stand
// in for the wildcard instance keys.
func (e *MoveEndpointInModule) String() string {
	if e == nil {
		return ""
	}
	var buf strings.Builder
	for _, name := range e.module {
		buf.WriteString("module.")
		buf.WriteString(name)
		buf.WriteString("[*].")
	}
	buf.WriteString(e.relSubject.String())

	// For consistency we'll also use the splat-like wildcard syntax to
	// represent the final step being either a resource or module call
	// rather than an instance, so we can more easily distinguish the two
	// in the string representation.
	switch e.relSubject.(type) {
	case AbsModuleCall, AbsResource:
		buf.WriteString("[*]")
	}

	return buf.String()
}

// Equal returns true if the reciever represents the same matching pattern
// as the other given endpoint, ignoring the source location information.
//
// This is not an optimized function and is here primarily to help with
// writing concise assertions in test code.
func (e *MoveEndpointInModule) Equal(other *MoveEndpointInModule) bool {
	if (e == nil) != (other == nil) {
		return false
	}
	if !e.module.Equal(other.module) {
		return false
	}
	// This assumes that all of our possible "movables" are trivially
	// comparable with reflect, which is true for all of them at the time
	// of writing.
	return reflect.DeepEqual(e.relSubject, other.relSubject)
}

// Module returns the address of the module where the receiving address was
// declared.
func (e *MoveEndpointInModule) Module() Module {
	return e.module
}

// InModuleInstance returns an AbsMoveable address which concatenates the
// given module instance address with the receiver's relative object selection
// to produce one example of an instance that might be affected by this
// move statement.
//
// The result is meaningful only if the given module instance is an instance
// of the same module returned by the method Module. InModuleInstance doesn't
// fully verify that (aside from some cheap/easy checks), but it will produce
// meaningless garbage if not.
func (e *MoveEndpointInModule) InModuleInstance(modInst ModuleInstance) AbsMoveable {
	if len(modInst) != len(e.module) {
		// We don't check all of the steps to make sure that their names match,
		// because it would be expensive to do that repeatedly for every
		// instance of a module, but if the lengths don't match then that's
		// _obviously_ wrong.
		panic("given instance address does not match module address")
	}
	switch relSubject := e.relSubject.(type) {
	case ModuleInstance:
		ret := make(ModuleInstance, 0, len(modInst)+len(relSubject))
		ret = append(ret, modInst...)
		ret = append(ret, relSubject...)
		return ret
	case AbsModuleCall:
		retModAddr := make(ModuleInstance, 0, len(modInst)+len(relSubject.Module))
		retModAddr = append(retModAddr, modInst...)
		retModAddr = append(retModAddr, relSubject.Module...)
		return relSubject.Call.Absolute(retModAddr)
	case AbsResourceInstance:
		retModAddr := make(ModuleInstance, 0, len(modInst)+len(relSubject.Module))
		retModAddr = append(retModAddr, modInst...)
		retModAddr = append(retModAddr, relSubject.Module...)
		return relSubject.Resource.Absolute(retModAddr)
	case AbsResource:
		retModAddr := make(ModuleInstance, 0, len(modInst)+len(relSubject.Module))
		retModAddr = append(retModAddr, modInst...)
		retModAddr = append(retModAddr, relSubject.Module...)
		return relSubject.Resource.Absolute(retModAddr)
	default:
		panic(fmt.Sprintf("unexpected move subject type %T", relSubject))
	}
}

// ModuleCallTraversals returns both the address of the module where the
// receiver was declared and any other module calls it traverses through
// while selecting a particular object to move.
//
// This is a rather special-purpose function here mainly to support our
// validation rule that a module can only traverse down into child modules
// that belong to the same module package.
func (e *MoveEndpointInModule) ModuleCallTraversals() (Module, []ModuleCall) {
	// We're returning []ModuleCall rather than Module here to make it clearer
	// that this is a relative sequence of calls rather than an absolute
	// module path.

	var steps []ModuleInstanceStep
	switch relSubject := e.relSubject.(type) {
	case ModuleInstance:
		// We want all of the steps except the last one here, because the
		// last one is always selecting something declared in the same module
		// even though our address structure doesn't capture that.
		steps = []ModuleInstanceStep(relSubject[:len(relSubject)-1])
	case AbsModuleCall:
		steps = []ModuleInstanceStep(relSubject.Module)
	case AbsResourceInstance:
		steps = []ModuleInstanceStep(relSubject.Module)
	case AbsResource:
		steps = []ModuleInstanceStep(relSubject.Module)
	default:
		panic(fmt.Sprintf("unexpected move subject type %T", relSubject))
	}

	ret := make([]ModuleCall, len(steps))
	for i, step := range steps {
		ret[i] = ModuleCall{Name: step.Name}
	}
	return e.module, ret
}

// synthModuleInstance constructs a module instance out of the module path and
// any module portion of the relSubject, substituting Module and Call segments
// with ModuleInstanceStep using the anyKey value.
// This is only used internally for comparison of these complete paths, but
// does not represent how the individual parts are handled elsewhere in the
// code.
func (e *MoveEndpointInModule) synthModuleInstance() ModuleInstance {
	var inst ModuleInstance

	for _, mod := range e.module {
		inst = append(inst, ModuleInstanceStep{Name: mod, InstanceKey: anyKey})
	}

	switch sub := e.relSubject.(type) {
	case ModuleInstance:
		inst = append(inst, sub...)
	case AbsModuleCall:
		inst = append(inst, sub.Module...)
		inst = append(inst, ModuleInstanceStep{Name: sub.Call.Name, InstanceKey: anyKey})
	case AbsResource:
		inst = append(inst, sub.Module...)
	case AbsResourceInstance:
		inst = append(inst, sub.Module...)
	default:
		panic(fmt.Sprintf("unhandled relative address type %T", sub))
	}

	return inst
}

// SelectsModule returns true if the reciever directly selects either
// the given module or a resource nested directly inside that module.
//
// This is a good function to use to decide which modules in a state
// to consider when processing a particular move statement. For a
// module move the given module itself is what will move, while a
// resource move indicates that we should search each of the resources in
// the given module to see if they match.
func (e *MoveEndpointInModule) SelectsModule(addr ModuleInstance) bool {
	synthInst := e.synthModuleInstance()

	// In order to match the given module instance, our combined path must be
	// equal in length.
	if len(synthInst) != len(addr) {
		return false
	}

	for i, step := range synthInst {
		switch step.InstanceKey {
		case anyKey:
			// we can match any key as long as the name matches
			if step.Name != addr[i].Name {
				return false
			}
		default:
			if step != addr[i] {
				return false
			}
		}
	}
	return true
}

// SelectsResource returns true if the receiver directly selects either
// the given resource or one of its instances.
func (e *MoveEndpointInModule) SelectsResource(addr AbsResource) bool {
	// Only a subset of subject types can possibly select a resource, so
	// we'll take care of those quickly before we do anything more expensive.
	switch e.relSubject.(type) {
	case AbsResource, AbsResourceInstance:
		// okay
	default:
		return false // can't possibly match
	}

	if !e.SelectsModule(addr.Module) {
		return false
	}

	// If we get here then we know the module part matches, so we only need
	// to worry about the relative resource part.
	switch relSubject := e.relSubject.(type) {
	case AbsResource:
		return addr.Resource.Equal(relSubject.Resource)
	case AbsResourceInstance:
		// We intentionally ignore the instance key, because we consider
		// instances to be part of the resource they belong to.
		return addr.Resource.Equal(relSubject.Resource.Resource)
	default:
		// We should've filtered out all other types above
		panic(fmt.Sprintf("unsupported relSubject type %T", relSubject))
	}
}

// moduleInstanceCanMatch indicates that modA can match modB taking into
// account steps with an anyKey InstanceKey as wildcards. The comparison of
// wildcard steps is done symmetrically, because varying portions of either
// instance's path could have been derived from configuration vs evaluation.
// The length of modA must be equal or shorter than the length of modB.
func moduleInstanceCanMatch(modA, modB ModuleInstance) bool {
	for i, step := range modA {
		switch {
		case step.InstanceKey == anyKey || modB[i].InstanceKey == anyKey:
			// we can match any key as long as the names match
			if step.Name != modB[i].Name {
				return false
			}
		default:
			if step != modB[i] {
				return false
			}
		}
	}
	return true
}

// CanChainFrom returns true if the reciever describes an address that could
// potentially select an object that the other given address could select.
//
// In other words, this decides whether the move chaining rule applies, if
// the reciever is the "to" from one statement and the other given address
// is the "from" of another statement.
func (e *MoveEndpointInModule) CanChainFrom(other *MoveEndpointInModule) bool {
	eMod := e.synthModuleInstance()
	oMod := other.synthModuleInstance()

	// if the complete paths are different lengths, these cannot refer to the
	// same value.
	if len(eMod) != len(oMod) {
		return false
	}
	if !moduleInstanceCanMatch(oMod, eMod) {
		return false
	}

	eSub := e.relSubject
	oSub := other.relSubject

	switch oSub := oSub.(type) {
	case AbsModuleCall, ModuleInstance:
		switch eSub.(type) {
		case AbsModuleCall, ModuleInstance:
			// we already know the complete module path including any final
			// module call name is equal.
			return true
		}

	case AbsResource:
		switch eSub := eSub.(type) {
		case AbsResource:
			return eSub.Resource.Equal(oSub.Resource)
		}

	case AbsResourceInstance:
		switch eSub := eSub.(type) {
		case AbsResourceInstance:
			return eSub.Resource.Equal(oSub.Resource)
		}
	}

	return false
}

// NestedWithin returns true if the receiver describes an address that is
// contained within one of the objects that the given other address could
// select.
func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool {
	eMod := e.synthModuleInstance()
	oMod := other.synthModuleInstance()

	// In order to be nested within the given endpoint, the module path must be
	// shorter or equal.
	if len(oMod) > len(eMod) {
		return false
	}

	if !moduleInstanceCanMatch(oMod, eMod) {
		return false
	}

	eSub := e.relSubject
	oSub := other.relSubject

	switch oSub := oSub.(type) {
	case AbsModuleCall:
		switch eSub.(type) {
		case AbsModuleCall:
			// we know the other endpoint selects our module, but if we are
			// also a module call our path must be longer to be nested.
			return len(eMod) > len(oMod)
		}

		return true

	case ModuleInstance:
		switch eSub.(type) {
		case ModuleInstance, AbsModuleCall:
			// a nested module must have a longer path
			return len(eMod) > len(oMod)
		}

		return true

	case AbsResource:
		if len(eMod) != len(oMod) {
			// these resources are from different modules
			return false
		}

		// A resource can only contain a resource instance.
		switch eSub := eSub.(type) {
		case AbsResourceInstance:
			return eSub.Resource.Resource.Equal(oSub.Resource)
		}
	}

	return false
}

// matchModuleInstancePrefix is an internal helper to decide whether the given
// module instance address refers to either the module where the move endpoint
// was declared or some descendent of that module.
//
// If so, it will split the given address into two parts: the "prefix" part
// which corresponds with the module where the statement was declared, and
// the "relative" part which is the remainder that the relSubject of the
// statement might match against.
//
// The second return value is another example of our light abuse of
// ModuleInstance to represent _relative_ module references rather than
// absolute: it's a module instance address relative to the same return value.
// Because the exported idea of ModuleInstance represents only _absolute_
// module instance addresses, we mustn't expose that value through any exported
// API.
func (e *MoveEndpointInModule) matchModuleInstancePrefix(instAddr ModuleInstance) (ModuleInstance, ModuleInstance, bool) {
	if len(e.module) > len(instAddr) {
		return nil, nil, false // to short to possibly match
	}
	for i := range e.module {
		if e.module[i] != instAddr[i].Name {
			return nil, nil, false
		}
	}
	// If we get here then we have a match, so we'll slice up the input
	// to produce the prefix and match segments.
	return instAddr[:len(e.module)], instAddr[len(e.module):], true
}

// MoveDestination considers a an address representing a module
// instance in the context of source and destination move endpoints and then,
// if the module address matches the from endpoint, returns the corresponding
// new module address that the object should move to.
//
// MoveDestination will return false in its second return value if the receiver
// doesn't match fromMatch, indicating that the given move statement doesn't
// apply to this object.
//
// Both of the given endpoints must be from the same move statement and thus
// must have matching object types. If not, MoveDestination will panic.
func (m ModuleInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (ModuleInstance, bool) {
	// NOTE: This implementation assumes the invariant that fromMatch and
	// toMatch both belong to the same configuration statement, and thus they
	// will both have the same address type and the same declaration module.

	// The root module instance is not itself moveable.
	if m.IsRoot() {
		return nil, false
	}

	// The two endpoints must either be module call or module instance
	// addresses, or else this statement can never match.
	if fromMatch.ObjectKind() != MoveEndpointModule {
		return nil, false
	}

	// The rest of our work will be against the part of the reciever that's
	// relative to the declaration module. mRel is a weird abuse of
	// ModuleInstance that represents a relative module address, similar to
	// what we do for MoveEndpointInModule.relSubject.
	mPrefix, mRel, match := fromMatch.matchModuleInstancePrefix(m)
	if !match {
		return nil, false
	}

	// Our next goal is to split mRel into two parts: the match (if any) and
	// the suffix. Our result will then replace the match with the replacement
	// in toMatch while preserving the prefix and suffix.
	var mSuffix, mNewMatch ModuleInstance

	switch relSubject := fromMatch.relSubject.(type) {
	case ModuleInstance:
		if len(relSubject) > len(mRel) {
			return nil, false // too short to possibly match
		}
		for i := range relSubject {
			if relSubject[i] != mRel[i] {
				return nil, false // this step doesn't match
			}
		}
		// If we get to here then we've found a match. Since the statement
		// addresses are already themselves ModuleInstance fragments we can
		// just slice out the relevant parts.
		mNewMatch = toMatch.relSubject.(ModuleInstance)
		mSuffix = mRel[len(relSubject):]
	case AbsModuleCall:
		// The module instance part of relSubject must be a prefix of
		// mRel, and mRel must be at least one step longer to account for
		// the call step itself.
		if len(relSubject.Module) > len(mRel)-1 {
			return nil, false
		}
		for i := range relSubject.Module {
			if relSubject.Module[i] != mRel[i] {
				return nil, false // this step doesn't match
			}
		}
		// The call name must also match the next step of mRel, after
		// the relSubject.Module prefix.
		callStep := mRel[len(relSubject.Module)]
		if callStep.Name != relSubject.Call.Name {
			return nil, false
		}
		// If we get to here then we've found a match. We need to construct
		// a new mNewMatch that's an instance of the "new" relSubject with
		// the same key as our call.
		mNewMatch = toMatch.relSubject.(AbsModuleCall).Instance(callStep.InstanceKey)
		mSuffix = mRel[len(relSubject.Module)+1:]
	default:
		panic("invalid address type for module-kind move endpoint")
	}

	ret := make(ModuleInstance, 0, len(mPrefix)+len(mNewMatch)+len(mSuffix))
	ret = append(ret, mPrefix...)
	ret = append(ret, mNewMatch...)
	ret = append(ret, mSuffix...)
	return ret, true
}

// MoveDestination considers a an address representing a resource
// in the context of source and destination move endpoints and then,
// if the resource address matches the from endpoint, returns the corresponding
// new resource address that the object should move to.
//
// MoveDestination will return false in its second return value if the receiver
// doesn't match fromMatch, indicating that the given move statement doesn't
// apply to this object.
//
// Both of the given endpoints must be from the same move statement and thus
// must have matching object types. If not, MoveDestination will panic.
func (r AbsResource) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResource, bool) {
	switch fromMatch.ObjectKind() {
	case MoveEndpointModule:
		// If we've moving a module then any resource inside that module
		// moves too.
		fromMod := r.Module
		toMod, match := fromMod.MoveDestination(fromMatch, toMatch)
		if !match {
			return AbsResource{}, false
		}
		return r.Resource.Absolute(toMod), true

	case MoveEndpointResource:
		fromRelSubject, ok := fromMatch.relSubject.(AbsResource)
		if !ok {
			// The only other possible type for a resource move is
			// AbsResourceInstance, and that can never match an AbsResource.
			return AbsResource{}, false
		}

		// fromMatch can only possibly match the reciever if the resource
		// portions are identical, regardless of the module paths.
		if fromRelSubject.Resource != r.Resource {
			return AbsResource{}, false
		}

		// The module path portion of relSubject must have a prefix that
		// matches the module where our endpoints were declared.
		mPrefix, mRel, match := fromMatch.matchModuleInstancePrefix(r.Module)
		if !match {
			return AbsResource{}, false
		}

		// The remaining steps of the module path must _exactly_ match
		// the relative module path in the "fromMatch" address.
		if len(mRel) != len(fromRelSubject.Module) {
			return AbsResource{}, false // can't match if lengths are different
		}
		for i := range mRel {
			if mRel[i] != fromRelSubject.Module[i] {
				return AbsResource{}, false // all of the steps must match
			}
		}

		// If we got here then we have a match, and so our result is the
		// module instance where the statement was declared (mPrefix) followed
		// by the "to" relative address in toMatch.
		toRelSubject := toMatch.relSubject.(AbsResource)
		var mNew ModuleInstance
		if len(mPrefix) > 0 || len(toRelSubject.Module) > 0 {
			mNew = make(ModuleInstance, 0, len(mPrefix)+len(toRelSubject.Module))
			mNew = append(mNew, mPrefix...)
			mNew = append(mNew, toRelSubject.Module...)
		}
		ret := toRelSubject.Resource.Absolute(mNew)
		return ret, true

	default:
		panic("unexpected object kind")
	}
}

// MoveDestination considers a an address representing a resource
// instance in the context of source and destination move endpoints and then,
// if the instance address matches the from endpoint, returns the corresponding
// new instance address that the object should move to.
//
// MoveDestination will return false in its second return value if the receiver
// doesn't match fromMatch, indicating that the given move statement doesn't
// apply to this object.
//
// Both of the given endpoints must be from the same move statement and thus
// must have matching object types. If not, MoveDestination will panic.
func (r AbsResourceInstance) MoveDestination(fromMatch, toMatch *MoveEndpointInModule) (AbsResourceInstance, bool) {
	switch fromMatch.ObjectKind() {
	case MoveEndpointModule:
		// If we've moving a module then any resource inside that module
		// moves too.
		fromMod := r.Module
		toMod, match := fromMod.MoveDestination(fromMatch, toMatch)
		if !match {
			return AbsResourceInstance{}, false
		}
		return r.Resource.Absolute(toMod), true

	case MoveEndpointResource:
		switch fromMatch.relSubject.(type) {
		case AbsResource:
			oldResource := r.ContainingResource()
			newResource, match := oldResource.MoveDestination(fromMatch, toMatch)
			if !match {
				return AbsResourceInstance{}, false
			}
			return newResource.Instance(r.Resource.Key), true
		case AbsResourceInstance:
			fromRelSubject, ok := fromMatch.relSubject.(AbsResourceInstance)
			if !ok {
				// The only other possible type for a resource move is
				// AbsResourceInstance, and that can never match an AbsResource.
				return AbsResourceInstance{}, false
			}

			// fromMatch can only possibly match the reciever if the resource
			// portions are identical, regardless of the module paths.
			if fromRelSubject.Resource != r.Resource {
				return AbsResourceInstance{}, false
			}

			// The module path portion of relSubject must have a prefix that
			// matches the module where our endpoints were declared.
			mPrefix, mRel, match := fromMatch.matchModuleInstancePrefix(r.Module)
			if !match {
				return AbsResourceInstance{}, false
			}

			// The remaining steps of the module path must _exactly_ match
			// the relative module path in the "fromMatch" address.
			if len(mRel) != len(fromRelSubject.Module) {
				return AbsResourceInstance{}, false // can't match if lengths are different
			}
			for i := range mRel {
				if mRel[i] != fromRelSubject.Module[i] {
					return AbsResourceInstance{}, false // all of the steps must match
				}
			}

			// If we got here then we have a match, and so our result is the
			// module instance where the statement was declared (mPrefix) followed
			// by the "to" relative address in toMatch.
			toRelSubject := toMatch.relSubject.(AbsResourceInstance)
			var mNew ModuleInstance
			if len(mPrefix) > 0 || len(toRelSubject.Module) > 0 {
				mNew = make(ModuleInstance, 0, len(mPrefix)+len(toRelSubject.Module))
				mNew = append(mNew, mPrefix...)
				mNew = append(mNew, toRelSubject.Module...)
			}
			ret := toRelSubject.Resource.Absolute(mNew)
			return ret, true
		default:
			panic("invalid address type for resource-kind move endpoint")
		}
	default:
		panic("unexpected object kind")
	}
}

// IsModuleReIndex takes the From and To endpoints from a single move
// statement, and returns true if the only changes are to module indexes, and
// all non-absolute paths remain the same.
func (from *MoveEndpointInModule) IsModuleReIndex(to *MoveEndpointInModule) bool {
	// The statements must originate from the same module.
	if !from.module.Equal(to.module) {
		panic("cannot compare move expressions from different modules")
	}

	switch f := from.relSubject.(type) {
	case AbsModuleCall:
		switch t := to.relSubject.(type) {
		case ModuleInstance:
			// Generate a synthetic module to represent the full address of
			// the module call. We're not actually comparing indexes, so the
			// instance doesn't matter.
			callAddr := f.Instance(NoKey).Module()
			return callAddr.Equal(t.Module())
		}

	case ModuleInstance:
		switch t := to.relSubject.(type) {
		case AbsModuleCall:
			callAddr := t.Instance(NoKey).Module()
			return callAddr.Equal(f.Module())

		case ModuleInstance:
			return t.Module().Equal(f.Module())
		}
	}

	return false
}
