package config

import (
	"context"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"github.com/gruntwork-io/terragrunt/internal/remotestate"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/pkg/log"
	"github.com/huandu/go-clone"

	"github.com/gruntwork-io/terragrunt/internal/cache"
	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"

	"errors"

	"github.com/gruntwork-io/terragrunt/pkg/config/hclparse"
)

// PartialDecodeSectionType is an enum that is used to list out which blocks/sections of the terragrunt config should be
// decoded in a partial decode.
type PartialDecodeSectionType int

const (
	DependenciesBlock PartialDecodeSectionType = iota
	DependencyBlock
	TerraformBlock
	TerraformSource
	TerragruntFlags
	TerragruntVersionConstraints
	RemoteStateBlock
	FeatureFlagsBlock
	EngineBlock
	ExcludeBlock
	ErrorsBlock
	TerraformExtraArgs
)

// terragruntIncludeMultiple is a struct that can be used to only decode the include block with labels.
type terragruntIncludeMultiple struct {
	Remain  hcl.Body       `hcl:",remain"`
	Include IncludeConfigs `hcl:"include,block"`
}

// terragruntDependencies is a struct that can be used to only decode the dependencies block.
type terragruntDependencies struct {
	Dependencies *ModuleDependencies `hcl:"dependencies,block"`
	Remain       hcl.Body            `hcl:",remain"`
}

// terragruntFeatureFlags is a struct that can be used to store decoded feature flags.
type terragruntFeatureFlags struct {
	Remain       hcl.Body     `hcl:",remain"`
	FeatureFlags FeatureFlags `hcl:"feature,block"`
}

// terragruntErrors struct to decode errors block
type terragruntErrors struct {
	Errors *ErrorsConfig `hcl:"errors,block"`
	Remain hcl.Body      `hcl:",remain"`
}

// terragruntTerraform is a struct that can be used to only decode the terraform block.
type terragruntTerraform struct {
	Terraform *TerraformConfig `hcl:"terraform,block"`
	Remain    hcl.Body         `hcl:",remain"`
}

// terragruntTerraformSource is a struct that can be used to only decode the terraform block, and only the source
// attribute.
type terragruntTerraformSource struct {
	Terraform *terraformConfigSourceOnly `hcl:"terraform,block"`
	Remain    hcl.Body                   `hcl:",remain"`
}

// terraformConfigSourceOnly is a struct that can be used to decode only the source attribute of the terraform block.
type terraformConfigSourceOnly struct {
	Source *string  `hcl:"source,attr"`
	Remain hcl.Body `hcl:",remain"`
}

// terragruntTerraformExtraArgs decodes only the terraform source and extra_arguments blocks.
type terragruntTerraformExtraArgs struct {
	Terraform *terraformConfigExtraArgs `hcl:"terraform,block"`
	Remain    hcl.Body                  `hcl:",remain"`
}

// terraformConfigExtraArgs decodes source and extra_arguments only, leaving hooks unevaluated in Remain.
type terraformConfigExtraArgs struct {
	Remain    hcl.Body                  `hcl:",remain"`
	Source    *string                   `hcl:"source,attr"`
	ExtraArgs []TerraformExtraArguments `hcl:"extra_arguments,block"`
}

// terragruntFlags is a struct that can be used to only decode the flag attributes (prevent_destroy)
type terragruntFlags struct {
	IamRole             *string  `hcl:"iam_role,attr"`
	IamWebIdentityToken *string  `hcl:"iam_web_identity_token,attr"`
	PreventDestroy      *bool    `hcl:"prevent_destroy,attr"`
	Remain              hcl.Body `hcl:",remain"`
}

// terragruntVersionConstraints is a struct that can be used to only decode the attributes related to constraining the
// versions of terragrunt and terraform.
type terragruntVersionConstraints struct {
	TerragruntVersionConstraint *string  `hcl:"terragrunt_version_constraint,attr"`
	TerraformVersionConstraint  *string  `hcl:"terraform_version_constraint,attr"`
	TerraformBinary             *string  `hcl:"terraform_binary,attr"`
	Remain                      hcl.Body `hcl:",remain"`
}

// TerragruntDependency is a struct that can be used to only decode the dependency blocks in the terragrunt config
type TerragruntDependency struct {
	Remain       hcl.Body     `hcl:",remain"`
	Dependencies Dependencies `hcl:"dependency,block"`
}

// terragruntRemoteState is a struct that can be used to only decode the remote_state blocks in the terragrunt config
type terragruntRemoteState struct {
	RemoteState *remotestate.ConfigFile `hcl:"remote_state,block"`
	Remain      hcl.Body                `hcl:",remain"`
}

// terragruntEngine is a struct that can only be used to decode the engine block.
type terragruntEngine struct {
	Engine *EngineConfig `hcl:"engine,block"`
	Remain hcl.Body      `hcl:",remain"`
}

// DecodeBaseBlocks takes in a parsed HCL2 file and decodes the base blocks. Base blocks are blocks that should always
// be decoded even in partial decoding, because they provide bindings that are necessary for parsing any block in the
// file. Currently base blocks are:
// - locals
// - features
// - include
func DecodeBaseBlocks(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*DecodedBaseBlocks, error) {
	var errs []error

	evalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)
	if err != nil {
		return nil, err
	}

	// Decode just the `include` and `import` blocks, and verify that it's allowed here
	terragruntIncludeList, err := decodeAsTerragruntInclude(
		file,
		evalParsingContext,
	)
	if err != nil {
		errs = append(errs, err)
	}

	trackInclude, err := getTrackInclude(pctx, terragruntIncludeList, includeFromChild)
	if err != nil {
		errs = append(errs, err)
	}

	registerSiblingAutoInclude(pctx, file.ConfigPath, trackInclude)

	// set feature flags
	tgFlags := terragruntFeatureFlags{}
	// load default feature flags
	if err := file.Decode(&tgFlags, evalParsingContext); err != nil {
		return nil, err
	}
	// validate flags to have default value, collect errors
	var flagErrs []error

	for _, flag := range tgFlags.FeatureFlags {
		if flag.Default == nil {
			flagErr := fmt.Errorf("feature flag %s does not have a default value in %s", flag.Name, file.ConfigPath)
			flagErrs = append(flagErrs, flagErr)
		}
	}

	errs = append(errs, flagErrs...)

	flagsAsCtyVal, err := flagsAsCty(pctx, tgFlags.FeatureFlags)
	if err != nil {
		errs = append(errs, err)
	}

	// Evaluate all the expressions in the locals block separately and generate the variables list to use in the
	// evaluation ctx.
	locals, err := EvaluateLocalsBlock(ctx, pctx.WithTrackInclude(trackInclude).WithFeatures(&flagsAsCtyVal), l, file)
	if err != nil {
		errs = append(errs, err)
	}

	localsAsCtyVal, err := ConvertValuesMapToCtyVal(locals)
	if err != nil {
		return nil, err
	}

	return &DecodedBaseBlocks{
		TrackInclude: trackInclude,
		Locals:       &localsAsCtyVal,
		FeatureFlags: &flagsAsCtyVal,
	}, errors.Join(errs...)
}

func flagsAsCty(ctx *ParsingContext, tgFlags FeatureFlags) (cty.Value, error) {
	// extract all flags in map by name
	flagByName := map[string]*FeatureFlag{}
	for _, flag := range tgFlags {
		flagByName[flag.Name] = flag
	}

	evaluatedFlags, err := cliFlagsToCty(ctx, flagByName)
	if err != nil {
		return cty.NilVal, err
	}

	var errs []error

	for _, flag := range tgFlags {
		if _, exists := evaluatedFlags[flag.Name]; !exists {
			if flag.Default == nil {
				errs = append(errs, fmt.Errorf("feature flag %s does not have a default value in %s", flag.Name, ctx.TerragruntConfigPath))
				continue
			}

			contextFlag, err := flagToCtyValue(flag.Name, *flag.Default)
			if err != nil {
				return cty.NilVal, err
			}

			evaluatedFlags[flag.Name] = contextFlag
		}
	}

	flagsAsCtyVal, err := ConvertValuesMapToCtyVal(evaluatedFlags)
	if err != nil {
		return cty.NilVal, err
	}

	return flagsAsCtyVal, errors.Join(errs...)
}

// cliFlagsToCty converts CLI feature flags to Cty values. It returns a map of flag names
// to their corresponding Cty values and any error encountered during conversion.
func cliFlagsToCty(ctx *ParsingContext, flagByName map[string]*FeatureFlag) (map[string]cty.Value, error) {
	if ctx.FeatureFlags == nil {
		return make(map[string]cty.Value), nil
	}

	evaluatedFlags := make(map[string]cty.Value)

	var conversionErr error

	ctx.FeatureFlags.Range(func(name, value string) bool {
		var flag cty.Value

		var err error

		if existingFlag, ok := flagByName[name]; ok {
			flag, err = flagToTypedCtyValue(name, existingFlag.Default.Type(), value)
		} else {
			flag, err = flagToCtyValue(name, value)
		}

		if err != nil {
			conversionErr = err

			return false
		}

		evaluatedFlags[name] = flag

		return true
	})

	if conversionErr != nil {
		return nil, conversionErr
	}

	return evaluatedFlags, nil
}

func PartialParseConfigFile(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string, include *IncludeConfig) (*TerragruntConfig, error) {
	hclCache := cache.ContextCache[*hclparse.File](ctx, HclCacheContextKey)

	fileInfo, err := os.Stat(configPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, TerragruntConfigNotFoundError{Path: configPath}
		}

		return nil, err
	}

	cacheKey := fmt.Sprintf("configPath-%v-modTime-%v", configPath, fileInfo.ModTime().UnixMicro())

	// Check cache hit status before tracing
	_, cacheHit := hclCache.Get(ctx, cacheKey)

	var config *TerragruntConfig

	err = TraceParseConfigFile(
		ctx,
		configPath,
		pctx.WorkingDir,
		true, // isPartial
		pctx.PartialParseDecodeList,
		include,
		cacheHit,
		func(ctx context.Context) error {
			var file *hclparse.File

			if cacheConfig, found := hclCache.Get(ctx, cacheKey); found {
				file = cacheConfig.Rebind(hclparse.NewParser(pctx.ParserOptions...))
			} else {
				var parseErr error

				file, parseErr = hclparse.NewParser(pctx.ParserOptions...).ParseFromFile(configPath)
				if parseErr != nil {
					return parseErr
				}

				hclCache.Put(ctx, cacheKey, file)
			}

			var parseErr error

			config, parseErr = TerragruntConfigFromPartialConfig(ctx, pctx, l, file, include)

			return parseErr
		})

	return config, err
}

// TerragruntConfigFromPartialConfig is a wrapper of PartialParseConfigString which checks for cached configs.
// filename, configString, includeFromChild and decodeList are used for the cache key,
// by getting the default value (%#v) through fmt.
func TerragruntConfigFromPartialConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
	cacheKey := fmt.Sprintf("%#v-%#v-%#v-%#v-%#v", file.ConfigPath, file.Content(), includeFromChild, pctx.PartialParseDecodeList, pctx.TerragruntConfigPath)

	// Fold the sibling autoinclude existence and content into the key so an in-process create, remove, or edit cannot return a stale entry, with the merge experiment-gated so the experiment-off key is byte-for-byte unchanged.
	cacheKey += autoIncludeCacheKeySuffix(ctx, pctx, file.ConfigPath)

	terragruntConfigCache := cache.ContextCache[*TerragruntConfig](ctx, TerragruntConfigCacheContextKey)
	if pctx.UsePartialParseConfigCache {
		if config, found := terragruntConfigCache.Get(ctx, cacheKey); found {
			l.Debugf("Cache hit for '%s' (partial parsing), decodeList: '%v'.", pctx.TerragruntConfigPath, pctx.PartialParseDecodeList)

			deepCopy := clone.Clone(config).(*TerragruntConfig)

			return deepCopy, nil
		}

		l.Debugf("Cache miss for '%s' (partial parsing), decodeList: '%v'.", pctx.TerragruntConfigPath, pctx.PartialParseDecodeList)
	}

	config, err := PartialParseConfig(ctx, pctx, l, file, includeFromChild)
	if err != nil {
		return config, err
	}

	if pctx.UsePartialParseConfigCache {
		putConfig := clone.Clone(config).(*TerragruntConfig)
		terragruntConfigCache.Put(ctx, cacheKey, putConfig)
	}

	return config, nil
}

// PartialParseConfigString partially parses and decodes the provided string. Which blocks/attributes to decode is
// controlled by the function parameter decodeList. These blocks/attributes are parsed and set on the output
// TerragruntConfig. Valid values are:
//   - DependenciesBlock: Parses the `dependencies` block in the config
//   - DependencyBlock: Parses the `dependency` block in the config
//   - TerraformBlock: Parses the `terraform` block in the config
//   - TerragruntFlags: Parses the boolean flags `prevent_destroy` and `skip` in the config
//   - TerragruntVersionConstraints: Parses the attributes related to constraining terragrunt and terraform versions in
//     the config.
//   - RemoteStateBlock: Parses the `remote_state` block in the config
//   - FeatureFlagsBlock: Parses the `feature` block in the config
//   - EngineBlock: Parses the `engine` block in the config
//   - ExcludeBlock : Parses the `exclude` block in the config
//
// Note that the following blocks are always decoded:
// - locals
// - include
// Note also that the following blocks are never decoded in a partial parse:
// - inputs
func PartialParseConfigString(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath, configString string, include *IncludeConfig) (*TerragruntConfig, error) {
	file, err := hclparse.NewParser(pctx.ParserOptions...).ParseFromString(configString, configPath)
	if err != nil {
		return nil, err
	}

	return PartialParseConfig(ctx, pctx, l, file, include)
}

func PartialParseConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
	var errs []error

	// Detect and block deprecated configurations early, before attempting to parse.
	// This ensures included configs with deprecated syntax get clear error messages
	// instead of cryptic "Could not find Terragrunt configuration settings" errors.
	// See: https://github.com/gruntwork-io/terragrunt/issues/4983
	if err := DetectDeprecatedConfigurations(ctx, pctx, l, file); err != nil {
		return nil, err
	}

	if includeFromChild != nil && includeFromChild.Path != "" && !filepath.IsAbs(includeFromChild.Path) {
		includeFromChild.Path = filepath.Clean(filepath.Join(filepath.Dir(pctx.TerragruntConfigPath), includeFromChild.Path))
	}

	pctx = pctx.WithTrackInclude(nil)

	// read unit files and add to context
	unitValues, err := ReadValues(ctx, pctx, l, filepath.Dir(file.ConfigPath))
	if err != nil {
		return nil, err
	}

	pctx = pctx.WithValues(unitValues)

	// Decode just the Base blocks. See the function docs for DecodeBaseBlocks for more info on what base blocks are.
	// Initialize evaluation ctx extensions from base blocks.
	baseBlocks, err := DecodeBaseBlocks(ctx, pctx, l, file, includeFromChild)
	if err != nil {
		errs = append(errs, err)
	}

	if baseBlocks != nil {
		pctx = pctx.WithTrackInclude(baseBlocks.TrackInclude)
		pctx = pctx.WithFeatures(baseBlocks.FeatureFlags)
		pctx = pctx.WithLocals(baseBlocks.Locals)
	}

	// Set parsed Locals on the parsed config
	output, err := convertToTerragruntConfig(ctx, pctx, file.ConfigPath, &terragruntConfigFile{})
	if err != nil {
		return nil, err
	}

	output.IsPartial = true

	// Provide a dependency placeholder so remote_state can reference dependency outputs during partial decode.
	if pctx.DecodedDependencies == nil && pctx.SkipOutputsResolution {
		pctx = pctx.Clone()
		dynamicVal := cty.DynamicVal
		pctx.DecodedDependencies = &dynamicVal
	}

	evalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)
	if err != nil {
		return nil, err
	}

	// Now loop through each requested block / component to decode from the terragrunt config, decode them, and merge
	// them into the output TerragruntConfig struct.
	hasExcludeBlock := false

	for _, decode := range pctx.PartialParseDecodeList {
		switch decode {
		case DependenciesBlock:
			decoded := terragruntDependencies{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			// If we already decoded some dependencies, merge them in. Otherwise, set as the new list.
			if output.Dependencies != nil {
				output.Dependencies.Merge(decoded.Dependencies)
			} else {
				output.Dependencies = decoded.Dependencies
			}

		case TerraformBlock:
			decoded := terragruntTerraform{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			output.Terraform = decoded.Terraform

		case TerraformSource:
			decoded := terragruntTerraformSource{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if decoded.Terraform != nil {
				output.Terraform = &TerraformConfig{Source: decoded.Terraform.Source}
			}

		case TerraformExtraArgs:
			decoded := terragruntTerraformExtraArgs{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if decoded.Terraform != nil {
				output.Terraform = &TerraformConfig{
					Source:    decoded.Terraform.Source,
					ExtraArgs: decoded.Terraform.ExtraArgs,
				}
			}

		case DependencyBlock:
			decoded := TerragruntDependency{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			// In normal operation, if a dependency block does not have a `config_path` attribute, decoding returns an error since this attribute is required, but the `hclvalidate` command suppresses decoding errors and this causes a cycle between modules, so we need to filter out dependencies without a defined `config_path`.
			decoded.Dependencies = decoded.Dependencies.FilteredWithoutConfigPath()

			output.TerragruntDependencies = decoded.Dependencies
			// Convert dependency blocks into module dependency lists. If we already decoded some dependencies,
			// merge them in. Otherwise, set as the new list.
			dependencies := dependencyBlocksToModuleDependencies(l, decoded.Dependencies)
			if output.Dependencies != nil {
				output.Dependencies.Merge(dependencies)
			} else {
				output.Dependencies = dependencies
			}

		case EngineBlock:
			decoded := terragruntEngine{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			output.Engine = decoded.Engine

		case TerragruntFlags:
			decoded := terragruntFlags{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if decoded.PreventDestroy != nil {
				output.PreventDestroy = decoded.PreventDestroy
			}

			if decoded.IamRole != nil {
				output.IamRole = *decoded.IamRole
			}

			if decoded.IamWebIdentityToken != nil {
				output.IamWebIdentityToken = *decoded.IamWebIdentityToken
			}
		case TerragruntVersionConstraints:
			decoded := terragruntVersionConstraints{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if decoded.TerragruntVersionConstraint != nil {
				output.TerragruntVersionConstraint = *decoded.TerragruntVersionConstraint
			}

			if decoded.TerraformVersionConstraint != nil {
				output.TerraformVersionConstraint = *decoded.TerraformVersionConstraint
			}

			// If the TFPath is not explicitly set, use the TFPath from the config if it is set.
			if !pctx.TFPathExplicitlySet && decoded.TerraformBinary != nil {
				output.TerraformBinary = *decoded.TerraformBinary
			}

		case RemoteStateBlock:
			decoded := terragruntRemoteState{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if decoded.RemoteState != nil {
				config, err := decoded.RemoteState.Config()
				if err != nil {
					return nil, err
				}

				output.RemoteState = remotestate.New(config)
			}
		case FeatureFlagsBlock:
			decoded := terragruntFeatureFlags{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if output.FeatureFlags != nil {
				flags, err := deepMergeFeatureBlocks(output.FeatureFlags, decoded.FeatureFlags)
				if err != nil {
					return nil, err
				}

				output.FeatureFlags = flags
			} else {
				output.FeatureFlags = decoded.FeatureFlags
			}

		case ExcludeBlock:
			hasExcludeBlock = true

		case ErrorsBlock:
			decoded := terragruntErrors{}

			err := file.Decode(&decoded, evalParsingContext)
			if err != nil {
				return nil, err
			}

			if output.Errors != nil {
				output.Errors.Merge(decoded.Errors)
			} else {
				output.Errors = decoded.Errors
			}

		default:
			return nil, InvalidPartialBlockName{decode}
		}
	}

	errsContainsIncludeErr := false

	for _, err := range errs {
		if errors.As(err, &TooManyLevelsOfInheritanceError{}) {
			errsContainsIncludeErr = true
		}
	}

	if output.Terraform != nil && output.Terraform.Source != nil {
		markLocalModuleSourceAsRead(pctx, file.ConfigPath, *output.Terraform.Source)
	}

	// If this file includes another, parse and merge the partial blocks. Otherwise, just return this config.
	// If there have been errors during this parse, don't attempt to parse the included config.
	// TrackInclude is nil when DecodeBaseBlocks returned (nil, err), e.g. an invalid feature
	// default. Skip the merge in that case and let the error surface at the return below.
	if pctx.TrackInclude != nil && len(pctx.TrackInclude.CurrentList) > 0 && !errsContainsIncludeErr {
		config, err := handleInclude(ctx, pctx, l, output, true)
		if err != nil {
			errs = append(errs, err)
		}

		// handleInclude returns nil when a Merge/DeepMerge fails; keep the pre-include output so the
		// appended error surfaces below instead of panicking on a nil ProcessedIncludes assignment.
		if config != nil {
			config.ProcessedIncludes = pctx.TrackInclude.CurrentMap
			output = config
		}
	}

	if joined := errors.Join(errs...); joined != nil {
		return output, joined
	}

	// Materialize the current file's exclude after includes merge so included feature flags resolve, and before the autoinclude merge so the autoinclude wins on top.
	if hasExcludeBlock {
		excludeOutput, err := processExcludes(ctx, pctx, l, output, file)
		if err != nil {
			return nil, err
		}

		output = excludeOutput
	}

	// Merge the sibling autoinclude into this partial output so discovery/run-queue agrees with the full parse.
	if err := mergeAutoIncludePartialIfPresent(ctx, pctx, l, output); err != nil {
		return nil, err
	}

	return output, nil
}

// processExcludes evaluate exclude blocks and merge them into the config.
func processExcludes(ctx context.Context, pctx *ParsingContext, l log.Logger, config *TerragruntConfig, file *hclparse.File) (*TerragruntConfig, error) {
	flagsAsCtyVal, err := flagsAsCty(pctx, config.FeatureFlags)
	if err != nil {
		return nil, err
	}

	excludeConfig, err := evaluateExcludeBlocks(ctx, pctx.WithFeatures(&flagsAsCtyVal), l, file)
	if err != nil {
		return nil, err
	}

	if excludeConfig == nil {
		return config, nil
	}

	if config.Exclude != nil {
		config.Exclude.Merge(excludeConfig)
	} else {
		config.Exclude = excludeConfig
	}

	return config, nil
}

func partialParseIncludedConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, includedConfig *IncludeConfig) (*TerragruntConfig, error) {
	if includedConfig.Path == "" {
		return nil, IncludedConfigMissingPathError(pctx.TerragruntConfigPath)
	}

	includePath := includedConfig.Path

	config, err := PartialParseConfigFile(
		ctx,
		pctx,
		l,
		includePath,
		includedConfig,
	)
	if err != nil {
		// Convert generic config not found error to include-specific error
		var configNotFoundError TerragruntConfigNotFoundError
		if errors.As(err, &configNotFoundError) {
			return nil, IncludeConfigNotFoundError{IncludePath: includePath, SourcePath: pctx.TerragruntConfigPath}
		}

		return nil, err
	}

	return config, nil
}

// This decodes only the `include` blocks of a terragrunt config, so its value can be used while decoding the rest of
// the config.
// For consistency, `include` in the call to `file.Decode` is always assumed to be nil. Either it really is nil (parsing
// the child config), or it shouldn't be used anyway (the parent config shouldn't have an include block).
func decodeAsTerragruntInclude(file *hclparse.File, evalParsingContext *hcl.EvalContext) (IncludeConfigs, error) {
	tgInc := terragruntIncludeMultiple{}
	if err := file.Decode(&tgInc, evalParsingContext); err != nil {
		return nil, err
	}

	return tgInc.Include, nil
}

// registerSiblingAutoInclude records the sibling terragrunt.autoinclude.hcl on trackInclude as a high-priority override when it is in scope and exists, so the merge consumers read one registered entry instead of recomputing the gate.
func registerSiblingAutoInclude(pctx *ParsingContext, configPath string, trackInclude *TrackInclude) {
	if trackInclude == nil {
		return
	}

	autoIncludePath, ok := siblingAutoIncludePath(pctx, configPath)
	if !ok {
		return
	}

	if !util.FileExists(autoIncludePath) {
		return
	}

	trackInclude.AutoIncludeOverride = &IncludeConfig{Path: autoIncludePath}
}

// mergeAutoIncludePartialIfPresent merges the registered sibling autoinclude override into a partial output using the same decode list, shallow, with the autoinclude winning, matching a regular include's default merge strategy.
func mergeAutoIncludePartialIfPresent(ctx context.Context, pctx *ParsingContext, l log.Logger, output *TerragruntConfig) error {
	if pctx.TrackInclude == nil || pctx.TrackInclude.AutoIncludeOverride == nil {
		return nil
	}

	autoIncludePath := pctx.TrackInclude.AutoIncludeOverride.Path

	// Reset DecodedDependencies so the autoinclude file gets its own dependency resolution pass; reuse the same decode list.
	clonedPctx := pctx.Clone()
	clonedPctx.DecodedDependencies = nil
	clonedPctx.skipAutoIncludeMerge = true
	clonedPctx = clonedPctx.WithDecodeList(pctx.PartialParseDecodeList...)

	autoIncludeConfig, err := PartialParseConfigFile(ctx, clonedPctx, l, autoIncludePath, nil)
	if err != nil {
		return fmt.Errorf("failed to parse %s: %w", autoIncludePath, err)
	}

	// Capture paths from each side's dependencies block before the merge flattens block derived paths together.
	unitDependenciesBlockPaths := dependenciesBlockOnlyPaths(l, output)
	autoDependenciesBlockPaths := dependenciesBlockOnlyPaths(l, autoIncludeConfig)

	// Shallow merge with the autoinclude as the winning source, matching a regular include's default strategy.
	if err := output.Merge(l, autoIncludeConfig); err != nil {
		return fmt.Errorf("failed to merge %s: %w", autoIncludePath, err)
	}

	// Merge only appends the module dependency path list so rebuild it from the by name merged blocks to drop any overridden stale path.
	reconcileAutoIncludeModulePaths(l, output, unitDependenciesBlockPaths, autoDependenciesBlockPaths)

	return nil
}

// dependenciesBlockOnlyPaths returns the module dependency paths that came from a dependencies block rather than being derived from a dependency block.
func dependenciesBlockOnlyPaths(l log.Logger, cfg *TerragruntConfig) []string {
	if cfg == nil || cfg.Dependencies == nil {
		return nil
	}

	blockPaths := make(map[string]struct{})

	// Classify block derived paths through the same helper the rebuild uses so disabled and unresolved blocks are treated identically and a dependencies block edge overlapping a disabled block is not dropped.
	blockDerived := dependencyBlocksToModuleDependencies(l, cfg.TerragruntDependencies)
	if blockDerived != nil {
		for _, path := range blockDerived.Paths {
			blockPaths[path] = struct{}{}
		}
	}

	blockOnly := make([]string, 0, len(cfg.Dependencies.Paths))

	for _, path := range cfg.Dependencies.Paths {
		if _, ok := blockPaths[path]; ok {
			continue
		}

		blockOnly = append(blockOnly, path)
	}

	return blockOnly
}

// reconcileAutoIncludeModulePaths rebuilds the module dependency path list from the by name merged dependency blocks plus the preserved literal dependencies paths.
func reconcileAutoIncludeModulePaths(l log.Logger, output *TerragruntConfig, preservedPaths ...[]string) {
	rebuilt := dependencyBlocksToModuleDependencies(l, output.TerragruntDependencies)
	if rebuilt == nil {
		rebuilt = &ModuleDependencies{}
	}

	for _, set := range preservedPaths {
		rebuilt.Paths = append(rebuilt.Paths, set...)
	}

	seen := make(map[string]struct{}, len(rebuilt.Paths))
	deduped := make([]string, 0, len(rebuilt.Paths))

	for _, path := range rebuilt.Paths {
		if _, ok := seen[path]; ok {
			continue
		}

		seen[path] = struct{}{}
		deduped = append(deduped, path)
	}

	if len(deduped) == 0 {
		output.Dependencies = nil
		return
	}

	rebuilt.Paths = deduped
	output.Dependencies = rebuilt
}

// autoIncludeCacheKeySuffix folds the sibling autoinclude existence and content into the partial-parse cache key when the StackDependencies experiment is on, returning the empty string with the experiment off so the existing key stays byte-for-byte unchanged.
func autoIncludeCacheKeySuffix(ctx context.Context, pctx *ParsingContext, configPath string) string {
	autoIncludePath, ok := siblingAutoIncludePath(pctx, configPath)
	if !ok {
		return ""
	}

	memo := cache.ContextCache[string](ctx, AutoIncludeSuffixCacheContextKey)

	// Fingerprint the sibling cheaply so a cache hit reuses the content based suffix without re-reading the file.
	fingerprint := autoIncludePath
	if info, statErr := os.Stat(autoIncludePath); statErr == nil {
		fingerprint = fmt.Sprintf("%s-%d-%d", autoIncludePath, info.ModTime().UnixMicro(), info.Size())
	}

	if suffix, found := memo.Get(ctx, fingerprint); found {
		return suffix
	}

	suffix := autoIncludeContentSuffix(autoIncludePath)
	memo.Put(ctx, fingerprint, suffix)

	return suffix
}

// autoIncludeContentSuffix folds the sibling autoinclude existence and content into the cache key, with distinct sentinels for absent and unreadable.
func autoIncludeContentSuffix(autoIncludePath string) string {
	if !util.FileExists(autoIncludePath) {
		return fmt.Sprintf("-autoinclude:%#v", false)
	}

	content, err := util.ReadFileAsString(autoIncludePath)
	if err != nil {
		// An unreadable autoinclude must not collide with the absent sentinel; key on the error text.
		return fmt.Sprintf("-autoinclude:%#v-err:%#v", true, err.Error())
	}

	return fmt.Sprintf("-autoinclude:%#v-%#v", true, content)
}

// Custom error types

type InvalidPartialBlockName struct {
	sectionCode PartialDecodeSectionType
}

func (err InvalidPartialBlockName) Error() string {
	return fmt.Sprintf("Unrecognized partial block code %d. This is most likely an error in terragrunt. Please file a bug report on the project repository.", err.sectionCode)
}
