package redesign

import (
	"bytes"
	"cmp"
	"errors"
	"fmt"
	"io/fs"
	"path/filepath"
	"slices"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/hcl/v2/hclwrite"
	"github.com/zclconf/go-cty/cty"

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

// InvalidHCLValueError reports a user-supplied HCL fragment that failed
// to parse. Name is the values.<name> reference; Diag is the HCL
// diagnostic text. Match with errors.As.
type InvalidHCLValueError struct {
	Name string
	Diag string
}

func (e *InvalidHCLValueError) Error() string {
	return fmt.Sprintf("invalid HCL value for %q: %s", e.Name, e.Diag)
}

const (
	// valuesFileName is the sibling file Terragrunt reads to populate the
	// `values` object when evaluating a unit or stack configuration.
	valuesFileName = "terragrunt.values.hcl"

	// valuesStubPerm is the mode the generated stub is written with.
	valuesStubPerm = 0o644

	// valuesStubHeader leads the generated file.
	valuesStubHeader = "# Auto-generated by `terragrunt catalog` from the `values.*` references in this unit/stack.\n" +
		"# Populate each `\"TODO\"` entry with a real value before running terragrunt.\n" +
		"#\n" +
		"# Required values come first. These are referenced directly by the unit or\n" +
		"# stack, so terragrunt will fail until you fill them in.\n" +
		"#\n" +
		"# Optional values come next, pre-populated with the defaults pulled from each\n" +
		"# reference's `try(...)` fallback. Edit any you want to override, or delete\n" +
		"# lines you're happy leaving at the fallback.\n\n"

	requiredSectionHeader = "# === Required ===\n"
	optionalSectionHeader = "\n# === Optional (defaults from try() fallbacks) ===\n"
)

// ValuesReferences captures the outcome of a single HCL walk: names that
// must be populated (referenced outside any try) and names that are
// optional, each paired with a literal default when the try fallback was
// constant enough to evaluate without context.
type ValuesReferences struct {
	Required []string
	Optional []OptionalValue
}

// OptionalValue pairs an optional `values.<name>` reference with the literal
// default from its immediate enclosing `try(...)` call. Only references
// whose fallback evaluates without a context (i.e. pure literals) appear here.
type OptionalValue struct {
	Default cty.Value
	Name    string
}

// IsEmpty reports whether the walk surfaced no references at all.
func (r ValuesReferences) IsEmpty() bool {
	return len(r.Required) == 0 && len(r.Optional) == 0
}

// allNames returns the union of required and optional names, for use in
// diagnostic messages where the required/optional distinction doesn't matter.
func (r ValuesReferences) allNames() []string {
	names := make([]string, 0, len(r.Required)+len(r.Optional))
	names = append(names, r.Required...)

	for _, o := range r.Optional {
		names = append(names, o.Name)
	}

	slices.Sort(names)

	return names
}

// CollectValuesReferences parses the HCL file at path and classifies every
// `values.<name>` reference as required (not inside a try) or optional
// (inside a try). For optional references, the innermost try's last arg is
// evaluated in an empty context; if the eval succeeds and the value is
// wholly known, it's recorded as the default.
//
// If path does not exist, returns (zero ValuesReferences, nil).
func CollectValuesReferences(fsys vfs.FS, path string) (ValuesReferences, error) {
	raw, err := vfs.ReadFile(fsys, path)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return ValuesReferences{}, nil
		}

		return ValuesReferences{}, err
	}

	parser := hclparse.NewParser()

	file, diags := parser.ParseHCL(raw, filepath.Base(path))
	if diags.HasErrors() {
		return ValuesReferences{}, diags
	}

	body, ok := file.Body.(*hclsyntax.Body)
	if !ok {
		return ValuesReferences{}, nil
	}

	collector := newValuesCollector()
	if walkDiags := hclsyntax.Walk(body, collector); walkDiags.HasErrors() {
		return ValuesReferences{}, walkDiags
	}

	return collector.result(), nil
}

// valuesCollector walks an hclsyntax tree, separating every `values.<name>`
// reference into required vs optional and, when possible, capturing each
// optional's literal fallback.
type valuesCollector struct {
	requiredSet  map[string]struct{}
	optionalVals map[string]cty.Value
	tryStack     []*hclsyntax.FunctionCallExpr
}

func newValuesCollector() *valuesCollector {
	return &valuesCollector{
		requiredSet:  map[string]struct{}{},
		optionalVals: map[string]cty.Value{},
	}
}

func (c *valuesCollector) Enter(node hclsyntax.Node) hcl.Diagnostics {
	if call, ok := node.(*hclsyntax.FunctionCallExpr); ok && call.Name == "try" {
		c.tryStack = append(c.tryStack, call)

		return nil
	}

	trav, ok := node.(*hclsyntax.ScopeTraversalExpr)
	if !ok {
		return nil
	}

	const minTraversalLen = 2

	if len(trav.Traversal) < minTraversalLen {
		return nil
	}

	root, ok := trav.Traversal[0].(hcl.TraverseRoot)
	if !ok || root.Name != config.MetadataValues {
		return nil
	}

	attr, ok := trav.Traversal[1].(hcl.TraverseAttr)
	if !ok {
		return nil
	}

	c.recordReference(attr.Name)

	return nil
}

func (c *valuesCollector) Exit(node hclsyntax.Node) hcl.Diagnostics {
	if call, ok := node.(*hclsyntax.FunctionCallExpr); ok && call.Name == "try" {
		c.tryStack = c.tryStack[:len(c.tryStack)-1]
	}

	return nil
}

// recordReference classifies a single reference to `values.<name>`. A name
// seen outside any try wins over any earlier optional classification: once
// we see a required use, the user must supply it.
func (c *valuesCollector) recordReference(name string) {
	if len(c.tryStack) == 0 {
		c.requiredSet[name] = struct{}{}

		delete(c.optionalVals, name)

		return
	}

	if _, isRequired := c.requiredSet[name]; isRequired {
		return
	}

	if _, alreadyCaptured := c.optionalVals[name]; alreadyCaptured {
		return
	}

	innermost := c.tryStack[len(c.tryStack)-1]
	if len(innermost.Args) == 0 {
		return
	}

	fallback := innermost.Args[len(innermost.Args)-1]

	val, diags := fallback.Value(&hcl.EvalContext{})
	if diags.HasErrors() || !val.IsWhollyKnown() || val.IsNull() {
		return
	}

	c.optionalVals[name] = val
}

// result returns the sorted required/optional classification captured so far.
func (c *valuesCollector) result() ValuesReferences {
	required := make([]string, 0, len(c.requiredSet))
	for name := range c.requiredSet {
		required = append(required, name)
	}

	slices.Sort(required)

	optional := make([]OptionalValue, 0, len(c.optionalVals))
	for name, val := range c.optionalVals {
		optional = append(optional, OptionalValue{Name: name, Default: val})
	}

	slices.SortFunc(optional, func(a, b OptionalValue) int {
		return cmp.Compare(a.Name, b.Name)
	})

	return ValuesReferences{Required: required, Optional: optional}
}

// WriteValuesStub writes a terragrunt.values.hcl skeleton into dir with
// `"TODO"` for required entries and the try() fallback for optional ones.
// It is equivalent to WriteValuesFile with no user-supplied values.
func WriteValuesStub(fsys vfs.FS, dir string, refs ValuesReferences) (bool, error) {
	return WriteValuesFile(fsys, dir, refs, nil)
}

// WriteValuesFile writes a terragrunt.values.hcl into dir. The file contains
// a required section followed by an optional section. For each reference,
// the corresponding entry in values (if present and non-empty) is parsed as
// the right-hand side of an HCL assignment and emitted verbatim; otherwise
// required entries default to `"TODO"` and optional entries to the literal
// try() fallback captured during discovery.
//
// If both sections would be empty the file is not written. If a
// terragrunt.values.hcl already exists in dir it is left untouched.
//
// Returns written=true only when a new file was created. An invalid HCL
// fragment in values is surfaced as an error. Callers (typically the
// interactive form) validate user input before reaching this point.
func WriteValuesFile(fsys vfs.FS, dir string, refs ValuesReferences, values map[string]string) (bool, error) {
	if refs.IsEmpty() {
		return false, nil
	}

	filePath := filepath.Join(dir, valuesFileName)

	exists, err := vfs.FileExists(fsys, filePath)
	if err != nil {
		return false, err
	}

	if exists {
		return false, nil
	}

	var buf bytes.Buffer

	buf.WriteString(valuesStubHeader)

	// Sort defensively so callers passing a hand-constructed
	// ValuesReferences get deterministic output without needing to know that
	// CollectValuesReferences happens to pre-sort.
	if len(refs.Required) > 0 {
		buf.WriteString(requiredSectionHeader)

		sortedRequired := slices.Clone(refs.Required)
		slices.Sort(sortedRequired)

		required := hclwrite.NewEmptyFile()

		for _, name := range sortedRequired {
			if err := writeValueAttribute(required.Body(), name, values[name], cty.StringVal("TODO")); err != nil {
				return false, err
			}
		}

		buf.Write(required.Bytes())
	}

	if len(refs.Optional) > 0 {
		buf.WriteString(optionalSectionHeader)

		sortedOptional := slices.Clone(refs.Optional)
		slices.SortFunc(sortedOptional, func(a, b OptionalValue) int {
			return cmp.Compare(a.Name, b.Name)
		})

		optional := hclwrite.NewEmptyFile()

		for _, o := range sortedOptional {
			if err := writeValueAttribute(optional.Body(), o.Name, values[o.Name], o.Default); err != nil {
				return false, err
			}
		}

		buf.Write(optional.Bytes())
	}

	if err := vfs.WriteFile(fsys, filePath, buf.Bytes(), valuesStubPerm); err != nil {
		return false, err
	}

	return true, nil
}

// writeValueAttribute writes `name = <expr>` into body. When userValue is
// non-empty it is parsed as an HCL expression and emitted verbatim;
// otherwise fallback is written via the cty serializer.
func writeValueAttribute(body *hclwrite.Body, name, userValue string, fallback cty.Value) error {
	if userValue == "" {
		body.SetAttributeValue(name, fallback)

		return nil
	}

	tokens, err := parseHCLValueTokens(name, userValue)
	if err != nil {
		return err
	}

	body.SetAttributeRaw(name, tokens)

	return nil
}

// parseHCLValueTokens parses "<name> = <raw>" and returns the right-hand
// side as a token sequence usable with hclwrite.Body.SetAttributeRaw. This
// is how user-supplied HCL fragments (e.g. `["a", "b"]` or `{ k = "v" }`)
// are spliced into the generated values file without going through the cty
// serializer, which would otherwise require evaluating the fragment.
func parseHCLValueTokens(name, raw string) (hclwrite.Tokens, error) {
	src := []byte(name + " = " + raw + "\n")

	file, diags := hclwrite.ParseConfig(src, "values.hcl", hcl.Pos{Line: 1, Column: 1})
	if diags.HasErrors() {
		return nil, &InvalidHCLValueError{Name: name, Diag: diags.Error()}
	}

	attr := file.Body().GetAttribute(name)
	if attr == nil {
		return nil, fmt.Errorf("could not extract HCL value for %q from parsed source", name)
	}

	return attr.Expr().BuildTokens(nil), nil
}

// configFileForKind returns the filename the given component kind's main HCL
// file is expected to use, or "" if the kind has no associated HCL config.
func configFileForKind(kind ComponentKind) string {
	switch kind {
	case ComponentKindUnit:
		return config.DefaultTerragruntConfigPath
	case ComponentKindStack:
		return config.DefaultStackFile
	case ComponentKindModule, ComponentKindTemplate:
		return ""
	}

	return ""
}
