// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package stackmigrate

import (
	"fmt"
	"strings"

	"github.com/hashicorp/hcl/v2"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/collections"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackconfig"
	"github.com/hashicorp/terraform/internal/stacks/stackstate"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// stackResource represents a resource that was found in the terraform state.
// It contains the stack and component configuration for the resource.
type stackResource struct {
	// The fully qualified target address
	AbsResourceInstance stackaddrs.AbsResourceInstance

	// The stack and component configuration for the resource.
	StackConfig     *stackconfig.Stack
	ComponentConfig *stackconfig.Component

	// The source module configuration for the stack component.
	StackModuleConfig *configs.Config
}

// implement the UniqueKeyer interface for stackResource
// The key of a stackResource pointer is simply itself.
func (r *stackResource) UniqueKey() collections.UniqueKey[*stackResource] {
	return r
}

// implement the UniqueKey interface for stackResource
func (r *stackResource) IsUniqueKey(*stackResource) {}

func (m *migration) migrateResources(resources map[string]string, modules map[string]string) collections.Map[Instance, collections.Set[*stackResource]] {
	components := collections.NewMap[Instance, collections.Set[*stackResource]]()

	// for each resource in the config, we track the instances that belong to the
	// same component.
	trackComponent := func(resource *stackResource) {
		instance := resource.AbsResourceInstance.Component
		if !components.HasKey(instance) {
			components.Put(instance, collections.NewSet[*stackResource]())
		}
		components.Get(instance).Add(resource)
	}

	for _, resource := range m.stateResources() {
		for key, instance := range resource.Instances {
			source := resource.Addr.Instance(key)

			// check if the state resource has been requested for migration,
			// either by being in the resources map, or its module being in the modules map.
			// The returned target builds a new address for the resource within the
			// stack component where it will be migrated to.
			target, diags := m.search(source, resources, modules)
			if diags.HasErrors() {
				// if there are errors, we can't migrate this resource.
				m.emitDiags(diags)
				continue
			}

			// We have the component address, now load the stack and component configuration
			// for the resource.
			// If this is successful, we can now start adding source information
			// to diagnostics.
			diags = m.loadConfig(target)
			if diags.HasErrors() {
				m.emitDiags(diags)
				continue
			}
			trackComponent(target)

			// retrieve the provider that was uses to create the resource instance.
			providerAddr, provider, diags := m.getOwningProvider(source, target)
			if diags.HasErrors() {
				m.emitDiags(diags)
				continue
			}

			schema := provider.GetProviderSchema().SchemaForResourceType(resource.Addr.Resource.Mode, resource.Addr.Resource.Type)
			if schema.Body == nil {
				m.emitDiags(diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Invalid resource type",
					Detail:   fmt.Sprintf("Resource type %s not found in provider schema.", resource.Addr.Resource.Type),
					Subject:  target.StackModuleConfig.SourceAddrRange.Ptr(),
				}))
				continue
			}

			m.emit(&stackstate.AppliedChangeResourceInstanceObject{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: target.AbsResourceInstance.Component,
					Item:      target.AbsResourceInstance.Item.DeposedObject(addrs.NotDeposed),
				},
				NewStateSrc:        instance.Current,
				ProviderConfigAddr: providerAddr,
				Schema:             schema,
			})

			for deposedKey, deposed := range instance.Deposed {
				m.emit(&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
						Component: target.AbsResourceInstance.Component,
						Item:      target.AbsResourceInstance.Item.DeposedObject(deposedKey),
					},
					NewStateSrc:        deposed,
					ProviderConfigAddr: providerAddr,
					Schema:             schema,
				})
			}
		}

	}
	return components
}

// search searches for the state resource in the resource mappings and when found, converts and returns the relevant
// stackResource.
//
// If the resource or module is nested within the root module, they will be migrated to the component with the address structure retained.
// For example, a resource with the address module.my_module.module.child.aws_instance.foo will be migrated to
// component.my_component.module.child.aws_instance.foo if the corresponding map key is found.
// E.g module.child.aws_instance.foo will be replaced with component.child.aws_instance.foo
func (m *migration) search(resource addrs.AbsResourceInstance, resources map[string]string, modules map[string]string) (*stackResource, tfdiags.Diagnostics) {
	target, ok := resources[resource.String()]
	if ok {
		// Then we have an exact mapping for this resource.
		component, rest, diags := stackaddrs.ParseAbsComponentInstanceStrOnly(target)
		if diags.HasErrors() {
			return nil, diags
		}
		if !component.Stack.IsRoot() {
			// we only support root components at the moment
			diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid component instance", fmt.Sprintf("Only root component instances are allowed, got %q", target)))
			return nil, diags
		}

		if len(rest) > 0 {
			// Then we should have an exact mapping to a new resource.
			inst, moreDiags := addrs.ParseAbsResourceInstance(rest)
			diags = diags.Append(moreDiags)
			if moreDiags.HasErrors() {
				return nil, diags
			}

			return &stackResource{
				AbsResourceInstance: stackaddrs.AbsResourceInstance{
					Component: component,
					Item:      inst,
				},
			}, diags
		}

		// otherwise, we'll just put this resource into the specified component
		// at the same address.

		return &stackResource{
			AbsResourceInstance: stackaddrs.AbsResourceInstance{
				Component: component,
				Item:      resource,
			},
		}, diags
	} else {
		// no exact mapping, but if this isn't a resource in the root module
		// then we might have a mapping for the module it is in below.
		if resource.Module.IsRoot() {
			var diags tfdiags.Diagnostics
			diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource not found", fmt.Sprintf("Resource %q exists in state, but was not included in any provided mapping.", resource.Resource.String())))
			return nil, diags
		}
	}

	// The resource is in a child module, so we need to find the component.
	// When found, we replace the module with the component instance, i.e
	// a resource of module.child.aws_instance.foo will be replaced with
	// component.child.aws_instance.foo
	if targetComponent, ok := modules[resource.Module[0].Name]; ok {
		inst, rest, diags := stackaddrs.ParseAbsComponentInstanceStrOnly("component." + strings.TrimPrefix(targetComponent, "component."))
		if diags.HasErrors() {
			return nil, diags
		}
		if len(rest) > 0 || !inst.Stack.IsRoot() {
			// the module mapping should point directly to a root stack instance
			diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid component instance", fmt.Sprintf("Only root component instances are allowed, got %q", target)))
			return nil, diags
		}

		inst.Item.Key = resource.Module[0].InstanceKey // retain the instance key
		return &stackResource{
			AbsResourceInstance: stackaddrs.AbsResourceInstance{
				Component: inst,
				Item: addrs.AbsResourceInstance{
					Module:   resource.Module[1:], // the first module instance is replaced by the component instance
					Resource: resource.Resource,
				},
			},
		}, diags
	} else {
		var diags tfdiags.Diagnostics
		diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Module not found", fmt.Sprintf("Module %q exists in state, but was not included in any provided mapping.", resource.Module[0].Name)))
		return nil, diags
	}
}

// getOwningProvider returns the address of the provider configuration,
// as well as the provider instance, that was used to create the given resource instance.
func (m *migration) getOwningProvider(source addrs.AbsResourceInstance, resource *stackResource) (addrs.AbsProviderConfig, providers.Interface, tfdiags.Diagnostics) {
	var ret addrs.AbsProviderConfig
	// At this point, we already worked out the stack component where we are migrating
	// the resource to. Now we need to look into the module configuration of the stack component,
	// and ensure that it has a provider configuration that matches the one used to create
	// the resource instance.

	moduleAddr := resource.AbsResourceInstance.Item.Module.Module() // the module address within the stack component's module configuration
	providerConfig, diags := m.findProviderConfig(source, moduleAddr, resource.AbsResourceInstance.Item.Resource.Resource, resource.StackModuleConfig, resource.AbsResourceInstance.Component)
	if diags.HasErrors() {
		return ret, nil, diags
	}
	component := resource.ComponentConfig
	stackCfg := resource.StackConfig

	// we found the provider configuration within the module configuration,
	// now look it up in the stack configuration.
	expr, ok := component.ProviderConfigs[providerConfig]
	if !ok {
		// Then the module uses a provider not referenced in the component.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Provider not found for component",
			Detail:   fmt.Sprintf("Provider %q not found in component %q.", providerConfig.LocalName, resource.AbsResourceInstance.Component.Item.Component.Name),
			Subject:  component.SourceAddrRange.ToHCL().Ptr(),
		})
		return ret, nil, diags
	}

	vars := expr.Variables()
	if len(vars) != 1 {
		// This should be an exact reference to a single provider, if it's not
		// we can't really do anything.
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid provider reference",
			Detail:   "Provider references should be a simple reference to a single provider.",
			Subject:  expr.Range().Ptr(),
		})
		return ret, nil, diags
	}

	ref, _, moreDiags := stackaddrs.ParseReference(vars[0])
	diags = diags.Append(moreDiags)

	switch ref := ref.Target.(type) {
	case stackaddrs.ProviderConfigRef:
		providerAddr, ok := stackCfg.RequiredProviders.ProviderForLocalName(ref.ProviderLocalName)
		if !ok {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Provider not found for component",
				Detail:   fmt.Sprintf("Provider %s was needed by the resource %s but was not found in the stack configuration.", ref.ProviderLocalName, resource.AbsResourceInstance.String()),
				Subject:  component.SourceAddrRange.ToHCL().Ptr(),
			})
			return ret, nil, diags
		}

		addr := addrs.AbsProviderConfig{
			Module:   addrs.RootModule,
			Provider: providerAddr,
			Alias:    providerConfig.Alias, // we still use the alias from the module provider as this is referenced as if from within the module.
		}

		provider, pDiags := m.provider(providerAddr)
		// pull in source information for diagnostics if available.
		for _, diag := range pDiags {
			if diag.Source().Subject == nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: diag.Severity().ToHCL(),
					Summary:  diag.Description().Summary,
					Detail:   diag.Description().Detail,
					Subject:  resource.ComponentConfig.SourceAddrRange.ToHCL().Ptr(),
				})
			}
		}

		return addr, provider, diags
	default:
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid reference",
			Detail:   "Non-provider reference found in provider configuration.",
			Subject:  expr.Range().Ptr(),
		})
		return ret, nil, diags
	}
}

// findProviderConfig recursively searches through the stack module configuration to find the provider
// that was used to create the resource instance.
func (m *migration) findProviderConfig(source addrs.AbsResourceInstance, module addrs.Module, resource addrs.Resource, config *configs.Config, component stackaddrs.AbsComponentInstance) (addrs.LocalProviderConfig, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	if module.IsRoot() {
		r := config.Module.ResourceByAddr(resource)
		if r == nil {
			diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Target resource %q not found in component %q.", source, resource.InModule(module), component)))
			return addrs.LocalProviderConfig{}, diags
		}

		return r.ProviderConfigAddr(), diags
	}

	next, ok := config.Children[module[0]]
	if !ok {
		diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Module %q not found in component %q.", source, module, component)))
		return addrs.LocalProviderConfig{}, diags
	}

	// the address points to a nested module, so we continue the search
	// within the next module's configuration.
	provider, moreDiags := m.findProviderConfig(source, module[1:], resource, next, component)
	diags = diags.Append(moreDiags)
	if diags.HasErrors() {
		return addrs.LocalProviderConfig{}, diags
	}

	call, ok := config.Module.ModuleCalls[module[0]]
	if !ok {
		diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource mapped to non-existent target", fmt.Sprintf("Could not migrate resource %q. Module %q not found in component %q.", source, module, component)))
		return addrs.LocalProviderConfig{}, diags
	}

	for _, p := range call.Providers {
		if p.InChild.Name == provider.LocalName && p.InChild.Alias == provider.Alias {
			return p.InParent.Addr(), diags
		}
	}

	// if we reach here, then the provider was not explicitly passed to the module call
	// so we can just return the provider config determined by the resource.
	return provider, diags
}

// loadConfig loads the module and component configuration from the stack directory.
func (m *migration) loadConfig(resource *stackResource) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	instance := resource.AbsResourceInstance.Component
	stack := m.Config.Stack(instance.Stack.ConfigAddr())
	if stack == nil {
		return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Stack not found", fmt.Sprintf("Stack %q not found in configuration.", instance.Stack.ConfigAddr())))
	}
	resource.StackConfig = stack

	component := m.Config.Component(stackaddrs.ConfigComponentForAbsInstance(instance))
	if component == nil {
		return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Component not found", fmt.Sprintf("Component %q not found in stack %q.", instance.Item.Component.Name, instance.Stack.ConfigAddr())))
	}

	resource.ComponentConfig = component

	moduleConfig, diags := m.moduleConfig(component)
	if diags.HasErrors() {
		return diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Module configuration not found",
			Detail:   fmt.Sprintf("Module configuration for component %q not found.", instance.Item.Component.Name),
			Subject:  component.SourceAddrRange.ToHCL().Ptr(),
		})
	}
	resource.StackModuleConfig = moduleConfig
	return diags
}
