// Package graph implements the logic for running commands against the
// graph of dependencies for the unit in the current working directory.
package graph

import (
	"context"
	"errors"
	"fmt"

	"github.com/gruntwork-io/terragrunt/internal/configbridge"
	"github.com/gruntwork-io/terragrunt/internal/filter"
	"github.com/gruntwork-io/terragrunt/internal/runner"
	"github.com/gruntwork-io/terragrunt/internal/runner/common"
	"github.com/gruntwork-io/terragrunt/internal/runner/run/creds"
	"github.com/gruntwork-io/terragrunt/internal/runner/runall"
	"github.com/gruntwork-io/terragrunt/internal/venv"

	"github.com/gruntwork-io/terragrunt/internal/os/stdout"
	"github.com/gruntwork-io/terragrunt/internal/report"
	"github.com/gruntwork-io/terragrunt/pkg/config"
	"github.com/gruntwork-io/terragrunt/pkg/log"

	"github.com/gruntwork-io/terragrunt/internal/shell"
	"github.com/gruntwork-io/terragrunt/pkg/options"
)

// Run executes the configured terraform command against the dependency
// graph of the unit in the working directory.
func Run(ctx context.Context, l log.Logger, v venv.Venv, opts *options.TerragruntOptions) error {
	// Get credentials BEFORE config parsing — sops_decrypt_file() and
	// get_aws_account_id() in locals need auth-provider credentials
	// available in v.Env during HCL evaluation.
	// *Getter discarded: graph.Run only needs creds in v.Env for initial config parse.
	// Per-unit creds are re-fetched in runnerpool task (intentional: each unit may have
	// different opts after clone).
	shellOpts := configbridge.ShellRunOptsFromOpts(opts)
	if _, err := creds.ObtainCredsForParsing(ctx, l, v, opts.AuthProviderCmd, shellOpts); err != nil {
		return err
	}

	ctx, pctx := configbridge.NewParsingContext(ctx, l, opts)
	pctx = pctx.WithVenv(v)

	cfg, err := config.ReadTerragruntConfig(ctx, l, pctx, pctx.ParserOptions)
	if err != nil {
		return err
	}

	if cfg == nil {
		return errors.New(
			"terragrunt was not able to render the config as json because it received no config." +
				" This is almost certainly a bug in Terragrunt. Please open an issue on" +
				" github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl",
		)
	}
	// consider root for graph identification passed destroy-graph-root argument
	rootDir := opts.GraphRoot

	// if destroy-graph-root is empty, use git to find top level dir.
	// may cause issues if in the same repo exist unrelated modules which will generate errors when scanning.
	if rootDir == "" {
		gitRoot, gitRootErr := shell.GitTopLevelDir(ctx, l, v, opts.WorkingDir)
		if gitRootErr != nil {
			return gitRootErr
		}

		rootDir = gitRoot
	}

	// Clone options and set RootWorkingDir to rootDir so discovery starts from the graph root
	// This allows discovering all modules including dependents (modules that depend on the working dir)
	graphOpts := opts.Clone()
	graphOpts.RootWorkingDir = rootDir

	runnerOpts := make([]common.Option, 0, 1)

	r := report.NewReport().WithWorkingDir(opts.WorkingDir)

	if l.Formatter().DisabledColors() || stdout.IsRedirected() {
		r.WithDisableColor()
	}

	if opts.ReportFormat != "" {
		r.WithFormat(opts.ReportFormat)
	}

	if opts.SummaryPerUnit {
		r.WithShowUnitLevelSummary()
	}

	// Limit graph to the working directory and its dependents.
	// The prefix ellipsis means "include dependents"; target is included by default.
	pathExpr, err := filter.NewPathFilter(opts.WorkingDir)
	if err != nil {
		return fmt.Errorf("failed to create path filter for %s: %w", opts.WorkingDir, err)
	}

	graphExpr := filter.NewGraphExpression(pathExpr).WithDependents()
	graphOpts.Filters = filter.Filters{filter.NewFilter(graphExpr, graphExpr.String())}

	if opts.ReportSchemaFile != "" {
		defer func() {
			if err := r.WriteSchemaToFile(v.FS, opts.ReportSchemaFile); err != nil {
				l.Warnf("Failed to write report schema to %s: %v", opts.ReportSchemaFile, err)
			}
		}()
	}

	if opts.ReportFile != "" {
		defer func() {
			if err := r.WriteToFile(v.FS, opts.ReportFile); err != nil {
				l.Warnf("Failed to write report to %s: %v", opts.ReportFile, err)
			}
		}()
	}

	if !opts.SummaryDisable {
		defer func() {
			if errors.Is(err, runall.ErrUserCancelled) {
				return
			}

			if writeErr := r.WriteSummary(v.Writers.Writer); writeErr != nil {
				l.Warnf("Failed to write summary: %v", writeErr)
			}
		}()
	}

	rnr, err := runner.NewStackRunner(ctx, l, v, graphOpts, runnerOpts...)
	if err != nil {
		return err
	}

	return runall.RunAllOnStack(ctx, l, v, graphOpts, rnr, r)
}
