// Package run provides Terragrunt command flags.
package run

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"

	"github.com/gruntwork-io/terragrunt/internal/cli/flags"
	"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared"
	"github.com/gruntwork-io/terragrunt/internal/clihelper"
	"github.com/gruntwork-io/terragrunt/internal/experiment"
	"github.com/gruntwork-io/terragrunt/internal/filter"
	"github.com/gruntwork-io/terragrunt/internal/report"
	"github.com/gruntwork-io/terragrunt/internal/strict/controls"
	"github.com/gruntwork-io/terragrunt/internal/util"
	"github.com/gruntwork-io/terragrunt/pkg/log"
	"github.com/gruntwork-io/terragrunt/pkg/options"
)

const (
	NoAutoInitFlagName                       = "no-auto-init"
	NoAutoRetryFlagName                      = "no-auto-retry"
	NoAutoApproveFlagName                    = "no-auto-approve"
	NoAutoProviderCacheDirFlagName           = "no-auto-provider-cache-dir"
	NoEngineFlagName                         = "no-engine"
	NoDependencyFetchOutputFromStateFlagName = "no-dependency-fetch-output-from-state"
	NoHooksFlagName                          = "no-hooks"
	TFForwardStdoutFlagName                  = "tf-forward-stdout"
	UnitsThatIncludeFlagName                 = "units-that-include"
	DependencyFetchOutputFromStateFlagName   = "dependency-fetch-output-from-state"
	UsePartialParseConfigCacheFlagName       = "use-partial-parse-config-cache"
	SummaryPerUnitFlagName                   = "summary-per-unit"
	VersionManagerFileNameFlagName           = "version-manager-file-name"

	DisableCommandValidationFlagName   = "disable-command-validation"
	NoDestroyDependenciesCheckFlagName = "no-destroy-dependencies-check"
	DestroyDependenciesCheckFlagName   = "destroy-dependencies-check"

	SourceFlagName       = "source"
	SourceMapFlagName    = "source-map"
	SourceUpdateFlagName = "source-update"

	NoStackGenerate = "no-stack-generate"

	// Terragrunt Provider Cache related flags.

	ProviderCacheFlagName              = "provider-cache"
	ProviderCacheDirFlagName           = "provider-cache-dir"
	ProviderCacheHostnameFlagName      = "provider-cache-hostname"
	ProviderCachePortFlagName          = "provider-cache-port"
	ProviderCacheTokenFlagName         = "provider-cache-token"
	ProviderCacheRegistryNamesFlagName = "provider-cache-registry-names"

	// Engine related environment variables.

	EngineEnableFlagName    = "experimental-engine"
	EngineCachePathFlagName = "engine-cache-path"
	EngineSkipCheckFlagName = "engine-skip-check"
	EngineLogLevelFlagName  = "engine-log-level"

	// Report related flags.

	SummaryDisableFlagName = "summary-disable"
	ReportFileFlagName     = "report-file"
	ReportFormatFlagName   = "report-format"
	ReportSchemaFlagName   = "report-schema-file"

	// `--all` related flags.

	OutDirFlagName     = "out-dir"
	JSONOutDirFlagName = "json-out-dir"

	// `--graph` related flags.
	GraphRootFlagName = "graph-root"

	// Config and download flags - use shared package constants
	ConfigFlagName = shared.ConfigFlagName

	// Auth and IAM flags - use shared package constants
	InputsDebugFlagName                   = shared.InputsDebugFlagName
	IAMAssumeRoleFlagName                 = shared.IAMAssumeRoleFlagName
	IAMAssumeRoleDurationFlagName         = shared.IAMAssumeRoleDurationFlagName
	IAMAssumeRoleSessionNameFlagName      = shared.IAMAssumeRoleSessionNameFlagName
	IAMAssumeRoleWebIdentityTokenFlagName = shared.IAMAssumeRoleWebIdentityTokenFlagName
)

var ErrNoHooksRequiresExperiment = errors.New(
	"--no-hooks requires the 'optional-hooks' experiment to be enabled (e.g., --experiment=optional-hooks)",
)

// NewFlags creates and returns global flags.
func NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {
	tgPrefix := flags.Prefix{flags.TgPrefix}
	terragruntPrefix := flags.Prefix{flags.TerragruntPrefix}
	cmdFlags := clihelper.Flags{
		// `--all` related flags.
		flags.NewFlag(&clihelper.GenericFlag[string]{
			Name:        OutDirFlagName,
			EnvVars:     tgPrefix.EnvVars(OutDirFlagName),
			Destination: &opts.OutputFolder,
			Usage:       "Directory to store plan files.",
		},
			flags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars("out-dir"), opts.StrictControls)),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        JSONOutDirFlagName,
				EnvVars:     tgPrefix.EnvVars(JSONOutDirFlagName),
				Destination: &opts.JSONOutputFolder,
				Usage:       "Directory to store json plan files.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("json-out-dir"),
				opts.StrictControls,
			),
		),

		// `graph/-graph` related flags.

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        GraphRootFlagName,
				EnvVars:     tgPrefix.EnvVars(GraphRootFlagName),
				Destination: &opts.GraphRoot,
				Usage:       "Root directory from where to build graph dependencies.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("graph-root"),
				opts.StrictControls,
			),
		),

		// `--all` and `--graph` related flags.

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoStackGenerate,
			EnvVars:     tgPrefix.EnvVars(NoStackGenerate),
			Destination: &opts.NoStackGenerate,
			Usage:       "Disable automatic stack regeneration before running the command.",
		}),

		//  Backward compatibility with `terragrunt-` prefix flags.

		shared.NewConfigFlag(opts, prefix),

		shared.NewTFPathFlag(opts),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoAutoInitFlagName,
			EnvVars:     tgPrefix.EnvVars(NoAutoInitFlagName),
			Usage:       "Don't automatically run 'terraform/tofu init' during other terragrunt commands. You must run 'terragrunt init' manually.",
			Negative:    true,
			Destination: &opts.AutoInit,
		},
			flags.WithDeprecatedFlag(&clihelper.BoolFlag{
				EnvVars: terragruntPrefix.EnvVars("auto-init"),
			}, nil, opts.StrictControls)),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoAutoRetryFlagName,
			EnvVars:     tgPrefix.EnvVars(NoAutoRetryFlagName),
			Destination: &opts.AutoRetry,
			Usage:       "Don't automatically re-run command in case of transient errors.",
			Negative:    true,
		},
			flags.WithDeprecatedFlag(&clihelper.BoolFlag{
				EnvVars: terragruntPrefix.EnvVars("auto-retry"),
			}, nil, opts.StrictControls)),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoAutoApproveFlagName,
			EnvVars:     tgPrefix.EnvVars(NoAutoApproveFlagName),
			Destination: &opts.RunAllAutoApprove,
			Usage:       "Don't automatically append '-auto-approve' to the underlying OpenTofu/Terraform commands run with 'run --all'.",
			Negative:    true,
		},
			flags.WithDeprecatedFlag(&clihelper.BoolFlag{
				EnvVars: terragruntPrefix.EnvVars("auto-approve"),
			}, nil, opts.StrictControls)),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoAutoProviderCacheDirFlagName,
			EnvVars:     tgPrefix.EnvVars(NoAutoProviderCacheDirFlagName),
			Destination: &opts.NoAutoProviderCacheDir,
			Usage:       "Disable the auto-provider-cache-dir feature even when the experiment is enabled.",
		}),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoHooksFlagName,
			EnvVars:     tgPrefix.EnvVars(NoHooksFlagName),
			Destination: &opts.NoRunHooks,
			Usage:       "Disable Terragrunt hooks during run. Requires the 'optional-hooks' experiment.",
			Action: func(_ context.Context, _ *clihelper.Context, value bool) error {
				if !value {
					return nil
				}

				if opts.Experiments.Evaluate(experiment.OptionalHooks) {
					return nil
				}

				return ErrNoHooksRequiresExperiment
			},
		}),

		shared.NewDownloadDirFlag(opts, prefix),

		flags.NewFlag(&clihelper.GenericFlag[string]{
			Name:        SourceFlagName,
			EnvVars:     tgPrefix.EnvVars(SourceFlagName),
			Destination: &opts.Source,
			Usage:       "Download OpenTofu/Terraform configurations from the specified source into a temporary folder, and run Terraform in that temporary folder.",
		},
			flags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars("source"), opts.StrictControls)),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        SourceUpdateFlagName,
				EnvVars:     tgPrefix.EnvVars(SourceUpdateFlagName),
				Destination: &opts.SourceUpdate,
				Usage:       "Delete the contents of the temporary folder to clear out any old, cached source code before downloading new source code into it.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("source-update"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.MapFlag[string, string]{
				Name:        SourceMapFlagName,
				EnvVars:     tgPrefix.EnvVars(SourceMapFlagName),
				Destination: &opts.SourceMap,
				Usage:       "Replace any source URL (including the source URL of a config pulled in with dependency blocks) that has root source with dest.",
				Splitter:    util.SplitUrls,
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("source-map"),
				opts.StrictControls,
			),
		),

		// Assume IAM Role flags.
		shared.NewInputsDebugFlag(opts, prefix),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        UsePartialParseConfigCacheFlagName,
				EnvVars:     tgPrefix.EnvVars(UsePartialParseConfigCacheFlagName),
				Destination: &opts.UsePartialParseConfigCache,
				Usage:       "Enables caching of includes during partial parsing operations. Will also be used for the --iam-role option if provided.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("use-partial-parse-config-cache"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(&clihelper.SliceFlag[string]{
			Name:        VersionManagerFileNameFlagName,
			EnvVars:     tgPrefix.EnvVars(VersionManagerFileNameFlagName),
			Destination: &opts.VersionManagerFileName,
			Usage:       "File names used during the computation of the cache key for the version manager files.",
		}),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:    DependencyFetchOutputFromStateFlagName,
				EnvVars: tgPrefix.EnvVars(DependencyFetchOutputFromStateFlagName),
				Usage:   "Enable the dependency-fetch-output-from-state experiment to fetch dependency output directly from the state file instead of using tofu/terraform output.",
				Action: func(_ context.Context, _ *clihelper.Context, val bool) error {
					if val {
						return opts.Experiments.EnableExperiment(
							experiment.DependencyFetchOutputFromState,
						)
					}

					return nil
				},
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("fetch-dependency-output-from-state"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoDependencyFetchOutputFromStateFlagName,
			EnvVars:     tgPrefix.EnvVars(NoDependencyFetchOutputFromStateFlagName),
			Destination: &opts.NoDependencyFetchOutputFromState,
			Usage:       "Disable the dependency-fetch-output-from-state feature even when the experiment is enabled.",
			Hidden:      true,
		}),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        TFForwardStdoutFlagName,
				EnvVars:     tgPrefix.EnvVars(TFForwardStdoutFlagName),
				Destination: &opts.ForwardTFStdout,
				Usage:       "If specified, the output of OpenTofu/Terraform commands will be printed as is, without being integrated into the Terragrunt log.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("forward-tf-stdout"),
				opts.StrictControls,
			),
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("include-module-prefix"),
				opts.StrictControls,
				controls.LegacyLogs,
			),
		),

		flags.NewFlag(
			&clihelper.SliceFlag[string]{
				Name:    UnitsThatIncludeFlagName,
				EnvVars: tgPrefix.EnvVars(UnitsThatIncludeFlagName),
				Usage:   "If flag is set, 'run --all' will only run the command against Terragrunt modules that include the specified file.",
				Hidden:  true,
				Action: func(ctx context.Context, _ *clihelper.Context, value []string) error {
					if len(value) != 0 {
						if err := opts.StrictControls.FilterByNames(controls.UnitsThatInclude).
							Evaluate(ctx); err != nil {
							return err
						}

						for _, v := range value {
							attrExpr, err := filter.NewAttributeExpression(
								filter.AttributeReading,
								v,
							)
							if err != nil {
								return err
							}

							opts.Filters = append(
								opts.Filters,
								filter.NewFilter(attrExpr, attrExpr.String()),
							)
						}

						return nil
					}

					return nil
				},
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("modules-that-include"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        DisableCommandValidationFlagName,
				EnvVars:     tgPrefix.EnvVars(DisableCommandValidationFlagName),
				Destination: &opts.DisableCommandValidation,
				Usage:       "When this flag is set, Terragrunt will not validate the tofu/terraform command.",
				Hidden:      true,
				Action: func(ctx context.Context, _ *clihelper.Context, value bool) error {
					if value {
						return opts.StrictControls.FilterByNames(controls.DisableCommandValidation).
							Evaluate(ctx)
					}

					return nil
				},
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("disable-command-validation"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:    NoDestroyDependenciesCheckFlagName,
			EnvVars: tgPrefix.EnvVars(NoDestroyDependenciesCheckFlagName),
			Usage:   "When this flag is set, Terragrunt will not check for dependent units when destroying.",
			Hidden:  true,
			Action: func(ctx context.Context, _ *clihelper.Context, value bool) error {
				if value {
					return opts.StrictControls.FilterByNames(controls.NoDestroyDependenciesCheck).
						Evaluate(ctx)
				}

				return nil
			},
		}),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        DestroyDependenciesCheckFlagName,
			EnvVars:     tgPrefix.EnvVars(DestroyDependenciesCheckFlagName),
			Destination: &opts.DestroyDependenciesCheck,
			Usage:       "When this flag is set, Terragrunt will check for dependent units when destroying.",
		}),

		// Terragrunt Provider Cache flags.

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        ProviderCacheFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCacheFlagName),
				Destination: &opts.ProviderCacheOptions.Enabled,
				Usage:       "Enables Terragrunt's provider caching.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        ProviderCacheDirFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCacheDirFlagName),
				Destination: &opts.ProviderCacheOptions.Dir,
				Usage:       "The path to the Terragrunt provider cache directory. By default, 'terragrunt/providers' folder in the user cache directory.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache-dir"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        ProviderCacheTokenFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCacheTokenFlagName),
				Destination: &opts.ProviderCacheOptions.Token,
				Usage:       "The token for authentication to the Terragrunt Provider Cache server. By default, assigned automatically.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache-token"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        ProviderCacheHostnameFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCacheHostnameFlagName),
				Destination: &opts.ProviderCacheOptions.Hostname,
				Usage:       "The hostname of the Terragrunt Provider Cache server. By default, 'localhost'.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache-hostname"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[int]{
				Name:        ProviderCachePortFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCachePortFlagName),
				Destination: &opts.ProviderCacheOptions.Port,
				Usage:       "The port of the Terragrunt Provider Cache server. By default, assigned automatically.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache-port"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.SliceFlag[string]{
				Name:        ProviderCacheRegistryNamesFlagName,
				EnvVars:     tgPrefix.EnvVars(ProviderCacheRegistryNamesFlagName),
				Destination: &opts.ProviderCacheOptions.RegistryNames,
				Usage:       "The list of remote registries to cached by Terragrunt Provider Cache server. By default, 'registry.terraform.io', 'registry.opentofu.org'.",
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("provider-cache-registry-names"),
				opts.StrictControls,
			),
		),

		shared.NewAuthProviderCmdFlag(opts, prefix),

		shared.NewNoDiscoveryAuthProviderCmdFlag(opts, prefix),

		// Terragrunt engine flags.

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:    EngineEnableFlagName,
				EnvVars: tgPrefix.EnvVars(EngineEnableFlagName),
				Usage:   "Enable the iac-engine experiment to use IaC engines.",
				Hidden:  true,
				Action: func(_ context.Context, _ *clihelper.Context, val bool) error {
					if val {
						return opts.Experiments.EnableExperiment(experiment.IacEngine)
					}

					return nil
				},
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("experimental-engine"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        EngineCachePathFlagName,
				EnvVars:     tgPrefix.EnvVars(EngineCachePathFlagName),
				Destination: &opts.EngineOptions.CachePath,
				Usage:       "Cache path for Terragrunt engine files.",
				Hidden:      true,
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("engine-cache-path"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.BoolFlag{
				Name:        EngineSkipCheckFlagName,
				EnvVars:     tgPrefix.EnvVars(EngineSkipCheckFlagName),
				Destination: &opts.EngineOptions.SkipChecksumCheck,
				Usage:       "Skip checksum check for Terragrunt engine files.",
				Hidden:      true,
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("engine-skip-check"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(
			&clihelper.GenericFlag[string]{
				Name:        EngineLogLevelFlagName,
				EnvVars:     tgPrefix.EnvVars(EngineLogLevelFlagName),
				Destination: &opts.EngineOptions.LogLevel,
				Usage:       "Terragrunt engine log level.",
				Hidden:      true,
			},
			flags.WithDeprecatedEnvVars(
				terragruntPrefix.EnvVars("engine-log-level"),
				opts.StrictControls,
			),
		),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        NoEngineFlagName,
			EnvVars:     tgPrefix.EnvVars(NoEngineFlagName),
			Destination: &opts.EngineOptions.NoEngine,
			Usage:       "Disable IaC engines even when the iac-engine experiment is enabled.",
			Hidden:      true,
		}),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        SummaryDisableFlagName,
			EnvVars:     tgPrefix.EnvVars(SummaryDisableFlagName),
			Destination: &opts.SummaryDisable,
			Usage:       `Disable the summary output at the end of a run.`,
		}),

		flags.NewFlag(&clihelper.BoolFlag{
			Name:        SummaryPerUnitFlagName,
			EnvVars:     tgPrefix.EnvVars(SummaryPerUnitFlagName),
			Destination: &opts.SummaryPerUnit,
			Usage:       `Show duration information for each unit in the summary output.`,
		}),

		flags.NewFlag(&clihelper.GenericFlag[string]{
			Name:    ReportFileFlagName,
			EnvVars: tgPrefix.EnvVars(ReportFileFlagName),
			Usage:   `Path to generate report file in.`,
			Setter: func(value string) error {
				if value == "" {
					return nil
				}

				opts.ReportFile = value

				ext := filepath.Ext(value)
				if ext == "" {
					ext = ".csv"
				}

				if ext != ".csv" && ext != ".json" {
					return nil
				}

				if opts.ReportFormat == "" {
					opts.ReportFormat = report.Format(ext[1:])
				}

				return nil
			},
		}),

		flags.NewFlag(&clihelper.GenericFlag[string]{
			Name:    ReportFormatFlagName,
			EnvVars: tgPrefix.EnvVars(ReportFormatFlagName),
			Usage:   `Format of the report file.`,
			Setter: func(value string) error {
				if value == "" && opts.ReportFormat == "" {
					opts.ReportFormat = report.FormatCSV

					return nil
				}

				opts.ReportFormat = report.Format(value)

				switch opts.ReportFormat {
				case report.FormatCSV:
				case report.FormatJSON:
				default:
					return fmt.Errorf("unsupported report format: %s", value)
				}

				return nil
			},
		}),

		flags.NewFlag(&clihelper.GenericFlag[string]{
			Name:        ReportSchemaFlagName,
			EnvVars:     tgPrefix.EnvVars(ReportSchemaFlagName),
			Usage:       `Path to generate report schema file in.`,
			Destination: &opts.ReportSchemaFile,
		}),
	}

	// Add shared flags
	cmdFlags = cmdFlags.Add(shared.NewBackendFlags(opts, prefix)...)
	cmdFlags = cmdFlags.Add(shared.NewFeatureFlags(opts, prefix)...)
	cmdFlags = cmdFlags.Add(shared.NewFailFastFlag(opts))
	cmdFlags = cmdFlags.Add(shared.NewIAMAssumeRoleFlags(opts, prefix)...)
	cmdFlags = cmdFlags.Add(shared.NewQueueFlags(opts, prefix)...)
	cmdFlags = cmdFlags.Add(shared.NewFilterFlags(l, opts)...)
	cmdFlags = cmdFlags.Add(shared.NewParallelismFlag(opts))
	cmdFlags = cmdFlags.Add(shared.NewCASFlags(opts, prefix)...)

	return cmdFlags.Sort()
}
