package addrs

import (
	"fmt"

	"github.com/camptocamp/terraboard/internal/terraform/tfdiags"
	"github.com/hashicorp/hcl/v2"
)

// MoveEndpoint is to AbsMoveable and ConfigMoveable what Target is to
// Targetable: a wrapping struct that captures the result of decoding an HCL
// traversal representing a relative path from the current module to
// a moveable object.
//
// Its name reflects that its primary purpose is for the "from" and "to"
// addresses in a "moved" statement in the configuration, but it's also
// valid to use MoveEndpoint for other similar mechanisms that give
// Terraform hints about historical configuration changes that might
// prompt creating a different plan than Terraform would by default.
//
// To obtain a full address from a MoveEndpoint you must use
// either the package function UnifyMoveEndpoints (to get an AbsMoveable) or
// the method ConfigMoveable (to get a ConfigMoveable).
type MoveEndpoint struct {
	// SourceRange is the location of the physical endpoint address
	// in configuration, if this MoveEndpoint was decoded from a
	// configuration expresson.
	SourceRange tfdiags.SourceRange

	// Internally we (ab)use AbsMoveable as the representation of our
	// relative address, even though everywhere else in Terraform
	// AbsMoveable always represents a fully-absolute address.
	// In practice, due to the implementation of ParseMoveEndpoint,
	// this is always either a ModuleInstance or an AbsResourceInstance,
	// and we only consider the possibility of interpreting it as
	// a AbsModuleCall or an AbsResource in UnifyMoveEndpoints.
	// This is intentionally unexported to encapsulate this unusual
	// meaning of AbsMoveable.
	relSubject AbsMoveable
}

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

func (e *MoveEndpoint) String() string {
	// Our internal pseudo-AbsMoveable representing the relative
	// address (either ModuleInstance or AbsResourceInstance) is
	// a good enough proxy for the relative move endpoint address
	// serialization.
	return e.relSubject.String()
}

func (e *MoveEndpoint) Equal(other *MoveEndpoint) bool {
	switch {
	case (e == nil) != (other == nil):
		return false
	case e == nil:
		return true
	default:
		// Since we only use ModuleInstance and AbsResourceInstance in our
		// string representation, we have no ambiguity between address types
		// and can safely just compare the string representations to
		// compare the relSubject values.
		return e.String() == other.String() && e.SourceRange == other.SourceRange
	}
}

// MightUnifyWith returns true if it is possible that a later call to
// UnifyMoveEndpoints might succeed if given the reciever and the other
// given endpoint.
//
// This is intended for early static validation of obviously-wrong situations,
// although there are still various semantic errors that this cannot catch.
func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool {
	// For our purposes here we'll just do a unify without a base module
	// address, because the rules for whether unify can succeed depend
	// only on the relative part of the addresses, not on which module
	// they were declared in.
	from, to := UnifyMoveEndpoints(RootModule, e, other)
	return from != nil && to != nil
}

// ConfigMovable transforms the reciever into a ConfigMovable by resolving it
// relative to the given base module, which should be the module where
// the MoveEndpoint expression was found.
//
// The result is useful for finding the target object in the configuration,
// but it's not sufficient for fully interpreting a move statement because
// it lacks the specific module and resource instance keys.
func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable {
	addr := e.relSubject
	switch addr := addr.(type) {
	case ModuleInstance:
		ret := make(Module, 0, len(baseModule)+len(addr))
		ret = append(ret, baseModule...)
		ret = append(ret, addr.Module()...)
		return ret
	case AbsResourceInstance:
		moduleAddr := make(Module, 0, len(baseModule)+len(addr.Module))
		moduleAddr = append(moduleAddr, baseModule...)
		moduleAddr = append(moduleAddr, addr.Module.Module()...)
		return ConfigResource{
			Module:   moduleAddr,
			Resource: addr.Resource.Resource,
		}
	default:
		// The above should be exhaustive for all of the types
		// that ParseMoveEndpoint produces as our intermediate
		// address representation.
		panic(fmt.Sprintf("unsupported address type %T", addr))
	}

}

// ParseMoveEndpoint attempts to interpret the given traversal as a
// "move endpoint" address, which is a relative path from the module containing
// the traversal to a movable object in either the same module or in some
// child module.
//
// This deals only with the syntactic element of a move endpoint expression
// in configuration. Before the result will be useful you'll need to combine
// it with the address of the module where it was declared in order to get
// an absolute address relative to the root module.
func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) {
	path, remain, diags := parseModuleInstancePrefix(traversal)
	if diags.HasErrors() {
		return nil, diags
	}

	rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange())

	if len(remain) == 0 {
		return &MoveEndpoint{
			relSubject:  path,
			SourceRange: rng,
		}, diags
	}

	riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain)
	diags = diags.Append(moreDiags)
	if diags.HasErrors() {
		return nil, diags
	}

	return &MoveEndpoint{
		relSubject:  riAddr,
		SourceRange: rng,
	}, diags
}

// UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the
// "from" and "to" addresses in a moved block, and returns a pair of
// MoveEndpointInModule addresses guaranteed to be of the same dynamic type
// that represent what the two MoveEndpoint addresses refer to.
//
// moduleAddr must be the address of the module where the move was declared.
//
// This function deals both with the conversion from relative to absolute
// addresses and with resolving the ambiguity between no-key instance
// addresses and whole-object addresses, returning the least specific
// address type possible.
//
// Not all combinations of addresses are unifyable: the two addresses must
// either both include resources or both just be modules. If the two
// given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil),
// in which case the caller should typically report an error to the user
// stating the unification constraints.
func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) {

	// First we'll make a decision about which address type we're
	// ultimately trying to unify to. For our internal purposes
	// here we're going to borrow TargetableAddrType just as a
	// convenient way to talk about our address types, even though
	// targetable address types are not 100% aligned with moveable
	// address types.
	fromType := relFrom.internalAddrType()
	toType := relTo.internalAddrType()
	var wantType TargetableAddrType

	// Our goal here is to choose the whole-resource or whole-module-call
	// addresses if both agree on it, but to use specific instance addresses
	// otherwise. This is a somewhat-arbitrary way to resolve syntactic
	// ambiguity between the two situations which allows both for renaming
	// whole resources and for switching from a single-instance object to
	// a multi-instance object.
	switch {
	case fromType == AbsResourceInstanceAddrType || toType == AbsResourceInstanceAddrType:
		wantType = AbsResourceInstanceAddrType
	case fromType == AbsResourceAddrType || toType == AbsResourceAddrType:
		wantType = AbsResourceAddrType
	case fromType == ModuleInstanceAddrType || toType == ModuleInstanceAddrType:
		wantType = ModuleInstanceAddrType
	case fromType == ModuleAddrType || toType == ModuleAddrType:
		// NOTE: We're fudging a little here and using
		// ModuleAddrType to represent AbsModuleCall rather
		// than Module.
		wantType = ModuleAddrType
	default:
		panic("unhandled move address types")
	}

	modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType)
	modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType)
	if modFrom == nil || modTo == nil {
		// if either of them failed then they both failed, to make the
		// caller's life a little easier.
		return nil, nil
	}
	return modFrom, modTo
}

func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule {
	// relAddr can only be either AbsResourceInstance or ModuleInstance, the
	// internal intermediate representation produced by ParseMoveEndpoint.
	relAddr := e.relSubject

	switch relAddr := relAddr.(type) {
	case ModuleInstance:
		switch wantType {
		case ModuleInstanceAddrType:
			// Since our internal representation is already a module instance,
			// we can just rewrap this one.
			return &MoveEndpointInModule{
				SourceRange: e.SourceRange,
				module:      moduleAddr,
				relSubject:  relAddr,
			}
		case ModuleAddrType:
			// NOTE: We're fudging a little here and using
			// ModuleAddrType to represent AbsModuleCall rather
			// than Module.
			callerAddr, callAddr := relAddr.Call()
			absCallAddr := AbsModuleCall{
				Module: callerAddr,
				Call:   callAddr,
			}
			return &MoveEndpointInModule{
				SourceRange: e.SourceRange,
				module:      moduleAddr,
				relSubject:  absCallAddr,
			}
		default:
			return nil // can't make any other types from a ModuleInstance
		}
	case AbsResourceInstance:
		switch wantType {
		case AbsResourceInstanceAddrType:
			return &MoveEndpointInModule{
				SourceRange: e.SourceRange,
				module:      moduleAddr,
				relSubject:  relAddr,
			}
		case AbsResourceAddrType:
			return &MoveEndpointInModule{
				SourceRange: e.SourceRange,
				module:      moduleAddr,
				relSubject:  relAddr.ContainingResource(),
			}
		default:
			return nil // can't make any other types from an AbsResourceInstance
		}
	default:
		panic(fmt.Sprintf("unhandled address type %T", relAddr))
	}
}

// internalAddrType helps facilitate our slight abuse of TargetableAddrType
// as a way to talk about our different possible result address types in
// UnifyMoveEndpoints.
//
// It's not really correct to use TargetableAddrType in this way, because
// it's for Targetable rather than for AbsMoveable, but as long as the two
// remain aligned enough it saves introducing yet another enumeration with
// similar members that would be for internal use only anyway.
func (e *MoveEndpoint) internalAddrType() TargetableAddrType {
	switch addr := e.relSubject.(type) {
	case ModuleInstance:
		if !addr.IsRoot() && addr[len(addr)-1].InstanceKey == NoKey {
			// NOTE: We're fudging a little here and using
			// ModuleAddrType to represent AbsModuleCall rather
			// than Module.
			return ModuleAddrType
		}
		return ModuleInstanceAddrType
	case AbsResourceInstance:
		if addr.Resource.Key == NoKey {
			return AbsResourceAddrType
		}
		return AbsResourceInstanceAddrType
	default:
		// The above should cover all of the address types produced
		// by ParseMoveEndpoint.
		panic(fmt.Sprintf("unsupported address type %T", addr))
	}
}
