// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package tfdiags

import (
	"bytes"
	"encoding/json"
	"fmt"

	ctyjson "github.com/zclconf/go-cty/cty/json"

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

	"github.com/hashicorp/terraform/internal/lang/marks"
)

// CompactValueStr produces a compact, single-line summary of a given value
// that is suitable for display in the UI.
//
// For primitives it returns a full representation, while for more complex
// types it instead summarizes the type, size, etc to produce something
// that is hopefully still somewhat useful but not as verbose as a rendering
// of the entire data structure.
func CompactValueStr(val cty.Value) string {
	// This is a specialized subset of value rendering tailored to producing
	// helpful but concise messages in diagnostics. It is not comprehensive
	// nor intended to be used for other purposes.

	val, valMarks := val.Unmark()
	for mark := range valMarks {
		switch mark {
		case marks.Sensitive:
			// We check this in here just to make sure, but note that the caller
			// of compactValueStr ought to have already checked this and skipped
			// calling into compactValueStr anyway, so this shouldn't actually
			// be reachable.
			return "(sensitive value)"
		case marks.Ephemeral:
			// A non-sensitive ephemeral value is fine to show in the UI. Values
			// that are both ephemeral and sensitive should have both markings
			// and should therefore get caught by the marks.Sensitive case
			// above.
			return "(ephemeral value)"
		default:
			// We don't know about any other marks, so we'll be conservative.
			// This shouldn't actually reachable since the caller should've
			// checked this and skipped calling compactValueStr anyway.
			return "value with unrecognized marks (this is a bug in Terraform)"
		}
	}

	// WARNING: We've only checked that the value isn't sensitive _shallowly_
	// here, and so we must never show any element values from complex types
	// in here. However, it's fine to show map keys and attribute names because
	// those are never sensitive in isolation: the entire value would be
	// sensitive in that case.

	ty := val.Type()
	switch {
	case val.IsNull():
		return "null"
	case !val.IsKnown():
		// Should never happen here because we should filter before we get
		// in here, but we'll do something reasonable rather than panic.
		return "(not yet known)"
	case ty == cty.Bool:
		if val.True() {
			return "true"
		}
		return "false"
	case ty == cty.Number:
		bf := val.AsBigFloat()
		return bf.Text('g', 10)
	case ty == cty.String:
		// Go string syntax is not exactly the same as HCL native string syntax,
		// but we'll accept the minor edge-cases where this is different here
		// for now, just to get something reasonable here.
		return fmt.Sprintf("%q", val.AsString())
	case ty.IsCollectionType() || ty.IsTupleType():
		l := val.LengthInt()
		switch l {
		case 0:
			return "empty " + ty.FriendlyName()
		case 1:
			return ty.FriendlyName() + " with 1 element"
		default:
			return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
		}
	case ty.IsObjectType():
		atys := ty.AttributeTypes()
		l := len(atys)
		switch l {
		case 0:
			return "object with no attributes"
		case 1:
			var name string
			for k := range atys {
				name = k
			}
			return fmt.Sprintf("object with 1 attribute %q", name)
		default:
			return fmt.Sprintf("object with %d attributes", l)
		}
	default:
		return ty.FriendlyName()
	}
}

// TraversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
func TraversalStr(traversal hcl.Traversal) string {
	// This is a specialized subset of traversal rendering tailored to
	// producing helpful contextual messages in diagnostics. It is not
	// comprehensive nor intended to be used for other purposes.

	var buf bytes.Buffer
	for _, step := range traversal {
		switch tStep := step.(type) {
		case hcl.TraverseRoot:
			buf.WriteString(tStep.Name)
		case hcl.TraverseAttr:
			buf.WriteByte('.')
			buf.WriteString(tStep.Name)
		case hcl.TraverseIndex:
			buf.WriteByte('[')
			if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
				buf.WriteString(CompactValueStr(tStep.Key))
			} else {
				// We'll just use a placeholder for more complex values,
				// since otherwise our result could grow ridiculously long.
				buf.WriteString("...")
			}
			buf.WriteByte(']')
		}
	}
	return buf.String()
}

// FormatValueStr produces a JSON-compatible, human-readable representation of a
// cty.Value that is suitable for display in the UI.
//
// The full representation of the value is produced, but with some redaction to
// nodes within the value sensitive and ephemeral marks.
// e.g {"a": "10", "b": "password"} => {"a": "10", "b": "(sensitive value)"}
func FormatValueStr(val cty.Value) (string, error) {
	var buf bytes.Buffer

	val, err := cty.Transform(val, func(path cty.Path, val cty.Value) (cty.Value, error) {
		// If a value is sensitive or ephemeral or unknown, we redact it, otherwise
		// we return the value as is.
		if marks.Has(val, marks.Sensitive) || marks.Has(val, marks.Ephemeral) || !val.IsKnown() {
			return cty.StringVal(CompactValueStr(val)), nil
		}
		return val, nil
	})
	if err != nil {
		return "", fmt.Errorf("unexpected error transforming value: %s", err)
	}

	jsonVal, err := ctyjson.Marshal(val, val.Type())
	if err != nil {
		return "", fmt.Errorf("unexpected error marshalling value: %s", err)
	}

	// indent the JSON output for better readability
	if err := json.Indent(&buf, jsonVal, "", "  "); err != nil {
		return "", fmt.Errorf("unexpected error formatting JSON: %s", err)
	}

	return buf.String(), nil
}
