package config

import (
	"context"
	"io"
	"maps"
	"os"
	"path/filepath"
	"slices"

	"github.com/puzpuzpuz/xsync/v4"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"

	"github.com/gruntwork-io/terragrunt/internal/engine"
	"github.com/gruntwork-io/terragrunt/internal/errors"
	"github.com/gruntwork-io/terragrunt/internal/experiment"
	"github.com/gruntwork-io/terragrunt/internal/iacargs"
	"github.com/gruntwork-io/terragrunt/internal/iam"
	pcoptions "github.com/gruntwork-io/terragrunt/internal/providercache/options"
	"github.com/gruntwork-io/terragrunt/internal/strict"
	"github.com/gruntwork-io/terragrunt/internal/telemetry"
	"github.com/gruntwork-io/terragrunt/internal/tfimpl"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/internal/writer"
	"github.com/gruntwork-io/terragrunt/pkg/config/hclparse"
	"github.com/gruntwork-io/terragrunt/pkg/log"
	"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders"
)

const (
	// MaxParseDepth limits nested parsing to prevent stack overflow
	// from deeply recursive config structures (includes, dependencies, etc.).
	MaxParseDepth = 1000
)

// ParsingContext provides various variables that are used throughout all funcs and passed from function to function.
// Using `ParsingContext` makes the code more readable.
// Note: context.Context should be passed explicitly as the first parameter to functions, not embedded in this struct.
type ParsingContext struct {
	Writers writer.Writers

	TerraformCliArgs *iacargs.IacArgs
	TrackInclude     *TrackInclude
	EngineConfig     *engine.EngineConfig
	EngineOptions    *engine.EngineOptions
	FeatureFlags     *xsync.Map[string, string]
	FilesRead        *[]string
	Telemetry        *telemetry.Options

	DecodedDependencies *cty.Value
	Values              *cty.Value
	Features            *cty.Value
	Locals              *cty.Value

	Env                 map[string]string
	SourceMap           map[string]string
	PredefinedFunctions map[string]function.Function

	ConvertToTerragruntConfigFunc func(ctx context.Context, pctx *ParsingContext, configPath string, terragruntConfigFromFile *terragruntConfigFile) (cfg *TerragruntConfig, err error)

	TerragruntConfigPath         string
	OriginalTerragruntConfigPath string
	WorkingDir                   string
	RootWorkingDir               string
	DownloadDir                  string
	Source                       string
	TerraformCommand             string
	OriginalTerraformCommand     string
	AuthProviderCmd              string
	TFPath                       string
	ScaffoldRootFileName         string
	TerragruntStackConfigPath    string
	TofuImplementation           tfimpl.Type

	IAMRoleOptions         iam.RoleOptions
	OriginalIAMRoleOptions iam.RoleOptions

	Experiments            experiment.Experiments
	StrictControls         strict.Controls
	PartialParseDecodeList []PartialDecodeSectionType
	ParserOptions          []hclparse.Option

	ProviderCacheOptions pcoptions.ProviderCacheOptions

	MaxFoldersToCheck int
	ParseDepth        int
	CASCloneDepth     int

	TFPathExplicitlySet bool
	SkipOutput          bool
	ForwardTFStdout     bool
	JSONLogFormat       bool
	Debug               bool
	AutoInit            bool
	Headless            bool
	BackendBootstrap    bool
	CheckDependentUnits bool

	NoDependencyFetchOutputFromState bool
	UsePartialParseConfigCache       bool
	SkipOutputsResolution            bool
	NoStackValidate                  bool
	NoCAS                            bool
}

func NewParsingContext(ctx context.Context, l log.Logger, opts ...Option) (context.Context, *ParsingContext) {
	filesRead := make([]string, 0)

	pctx := &ParsingContext{
		TerraformCliArgs: iacargs.New(),
		FilesRead:        &filesRead,
	}

	for _, opt := range opts {
		opt(pctx)
	}

	pctx.ParserOptions = DefaultParserOptions(l, pctx.StrictControls)

	return ctx, pctx
}

// Clone returns a copy of the ParsingContext.
// Maps are deep-copied so that mutations (e.g. credential injection into Env)
// on a clone do not affect the original or other clones.
func (ctx *ParsingContext) Clone() *ParsingContext {
	clone := *ctx

	if ctx.Env != nil {
		clone.Env = maps.Clone(ctx.Env)
	}

	if ctx.SourceMap != nil {
		clone.SourceMap = maps.Clone(ctx.SourceMap)
	}

	if ctx.EngineOptions != nil {
		eo := *ctx.EngineOptions
		clone.EngineOptions = &eo
	}

	clone.ProviderCacheOptions.RegistryNames = slices.Clone(ctx.ProviderCacheOptions.RegistryNames)

	return &clone
}

func (ctx *ParsingContext) WithDecodeList(decodeList ...PartialDecodeSectionType) *ParsingContext {
	c := ctx.Clone()
	c.PartialParseDecodeList = decodeList

	return c
}

func (ctx *ParsingContext) WithLocals(locals *cty.Value) *ParsingContext {
	c := ctx.Clone()
	c.Locals = locals

	return c
}

func (ctx *ParsingContext) WithValues(values *cty.Value) *ParsingContext {
	c := ctx.Clone()
	c.Values = values

	return c
}

// WithFeatures sets the feature flags to be used in evaluation context.
func (ctx *ParsingContext) WithFeatures(features *cty.Value) *ParsingContext {
	c := ctx.Clone()
	c.Features = features

	return c
}

func (ctx *ParsingContext) WithTrackInclude(trackInclude *TrackInclude) *ParsingContext {
	c := ctx.Clone()
	c.TrackInclude = trackInclude

	return c
}

func (ctx *ParsingContext) WithParseOption(parserOptions []hclparse.Option) *ParsingContext {
	c := ctx.Clone()
	c.ParserOptions = parserOptions

	return c
}

// WithDiagnosticsSuppressed returns a new ParsingContext with diagnostics suppressed.
// Diagnostics are written to stderr in debug mode for troubleshooting, otherwise discarded.
// This avoids false positive "There is no variable named dependency" errors during parsing
// when dependency outputs haven't been resolved yet.
func (ctx *ParsingContext) WithDiagnosticsSuppressed(l log.Logger) *ParsingContext {
	var diagWriter = io.Discard
	if l.Level() >= log.DebugLevel {
		diagWriter = os.Stderr
	}

	c := ctx.Clone()
	c.ParserOptions = slices.Concat(ctx.ParserOptions, []hclparse.Option{hclparse.WithDiagnosticsWriter(diagWriter, true)})

	return c
}

func (ctx *ParsingContext) WithSkipOutputsResolution() *ParsingContext {
	c := ctx.Clone()
	c.SkipOutputsResolution = true

	return c
}

// WithIncrementedDepth returns a new ParsingContext with incremented parse depth.
// Returns an error if the maximum depth would be exceeded.
func (ctx *ParsingContext) WithIncrementedDepth() (*ParsingContext, error) {
	if ctx.ParseDepth > MaxParseDepth {
		return nil, errors.New(MaxParseDepthError{
			Depth: ctx.ParseDepth,
			Max:   MaxParseDepth,
		})
	}

	c := ctx.Clone()
	c.ParseDepth = ctx.ParseDepth + 1

	return c, nil
}

// WithConfigPath returns a new ParsingContext targeting a different config file.
//
// It normalizes configPath to an absolute path, sets TerragruntConfigPath and
// WorkingDir accordingly, and updates the logger when the working directory changes.
//
// OriginalTerragruntConfigPath is preserved so that get_original_terragrunt_dir()
// continues to resolve to the caller. This is the correct behavior when one config
// reads another via read_terragrunt_config.
//
// To parse a dependency as an independent unit, use [ParsingContext.WithDependencyConfigPath].
func (ctx *ParsingContext) WithConfigPath(l log.Logger, configPath string) (log.Logger, *ParsingContext, error) {
	configPath = filepath.Clean(configPath)
	if !filepath.IsAbs(configPath) {
		configPath = filepath.Clean(filepath.Join(ctx.WorkingDir, configPath))
	}

	workingDir := filepath.Dir(configPath)

	if workingDir != ctx.WorkingDir {
		l = l.WithField(placeholders.WorkDirKeyName, workingDir)
	}

	c := ctx.Clone()

	// Keep DownloadDir in sync with TerragruntConfigPath: if the current context was
	// using the default download dir for the old config, update it to the default for
	// the new config. This ensures that when read_terragrunt_config() or dependency
	// processing switches to a different module, DownloadDir reflects the new module's
	// default rather than an inherited stale default from an ancestor. User-set custom
	// dirs (which won't match any module's default) are preserved unchanged.
	_, defaultDir := util.DefaultWorkingAndDownloadDirs(ctx.TerragruntConfigPath)
	if filepath.Clean(c.DownloadDir) == filepath.Clean(defaultDir) {
		_, c.DownloadDir = util.DefaultWorkingAndDownloadDirs(configPath)
	}

	c.TerragruntConfigPath = configPath
	c.WorkingDir = workingDir

	return l, c, nil
}

// WithDependencyConfigPath returns a new ParsingContext for parsing a dependency
// as an independent unit.
//
// It performs the same path and logger updates as [ParsingContext.WithConfigPath],
// and additionally resets OriginalTerragruntConfigPath to the dependency's path.
// This ensures that get_original_terragrunt_dir() resolves to the dependency's
// own directory rather than the caller's.
func (ctx *ParsingContext) WithDependencyConfigPath(l log.Logger, configPath string) (log.Logger, *ParsingContext, error) {
	l, c, err := ctx.WithConfigPath(l, configPath)
	if err != nil {
		return l, nil, err
	}

	c.OriginalTerragruntConfigPath = c.TerragruntConfigPath

	return l, c, nil
}
