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

package moduleref

import (
	"maps"
	"strings"

	"github.com/hashicorp/go-version"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/modsdir"
)

// Resolver is the struct responsible for finding all modules references in
// Terraform configuration for a given internal module manifest.
type Resolver struct {
	manifest         *Manifest
	internalManifest modsdir.Manifest
}

// NewResolver creates a new Resolver, storing a copy of the internal manifest
// that is passed.
func NewResolver(internalManifest modsdir.Manifest) *Resolver {
	// Since maps are pointers, create a copy of the internal manifest to
	// prevent introducing side effects to the original
	internalManifestCopy := maps.Clone(internalManifest)

	// Remove the root module entry from the internal manifest as it is
	// never directly referenced.
	delete(internalManifestCopy, "")

	return &Resolver{
		internalManifest: internalManifestCopy,
		manifest: &Manifest{
			FormatVersion: FormatVersion,
			Records:       Records{},
		},
	}
}

// Resolve will attempt to find all module references for the passed configuration
// and return a new manifest encapsulating this information.
func (r *Resolver) Resolve(cfg *configs.Config) *Manifest {
	// First find all the referenced modules.
	r.findAndTrimReferencedEntries(cfg, nil, nil)

	return r.manifest
}

// findAndTrimReferencedEntries will traverse a given Terraform configuration
// and attempt find a caller for every entry in the internal module manifest.
// If an entry is found, it will be removed from the internal manifest and
// appended to the manifest that records this new information in a nested heirarchy.
func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config, parentRecord *Record, parentKey *string) {
	var name string
	var versionConstraints version.Constraints
	if parentKey != nil {
		for key, child := range cfg.Parent.Children {
			if key == *parentKey {
				name = key
				versionConstraints = child.VersionConstraint.Required
				break
			}
		}
	}

	childRecord := &Record{
		Key:                name,
		Source:             cfg.SourceAddr,
		VersionConstraints: versionConstraints,
	}
	key := strings.Join(cfg.Path, ".")

	for entryKey, entry := range r.internalManifest {
		if entryKey == key {
			// Use resolved version from manifest
			childRecord.Version = entry.Version
			if parentRecord.Source != nil {
				parentRecord.addChild(childRecord)
			} else {
				r.manifest.addModuleEntry(childRecord)
			}
			// "Trim" the entry from the internal manifest, saving us cycles
			// as we descend into the module tree.
			delete(r.internalManifest, entryKey)
			break
		}
	}

	// Traverse the child configurations
	for childKey, childCfg := range cfg.Children {
		r.findAndTrimReferencedEntries(childCfg, childRecord, &childKey)
	}
}
