// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package repl

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
)

// FormatValue formats a value in a way that resembles Terraform language syntax
// and uses the type conversion functions where necessary to indicate exactly
// what type it is given, so that equality test failures can be quickly
// understood.
func FormatValue(v cty.Value, indent int) string {
	if !v.IsKnown() {
		return "(known after apply)"
	}
	if v.HasMark(marks.Sensitive) {
		return "(sensitive value)"
	}
	if v.HasMark(marks.Ephemeral) {
		return "(ephemeral value)"
	}
	if v.IsNull() {
		ty := v.Type()
		switch {
		case ty == cty.DynamicPseudoType:
			return "null"
		case ty == cty.String:
			return "tostring(null)"
		case ty == cty.Number:
			return "tonumber(null)"
		case ty == cty.Bool:
			return "tobool(null)"
		case ty.IsListType():
			return fmt.Sprintf("tolist(null) /* of %s */", ty.ElementType().FriendlyName())
		case ty.IsSetType():
			return fmt.Sprintf("toset(null) /* of %s */", ty.ElementType().FriendlyName())
		case ty.IsMapType():
			return fmt.Sprintf("tomap(null) /* of %s */", ty.ElementType().FriendlyName())
		default:
			return fmt.Sprintf("null /* %s */", ty.FriendlyName())
		}
	}

	ty := v.Type()
	switch {
	case ty.IsPrimitiveType():
		switch ty {
		case cty.String:
			if formatted, isMultiline := formatMultilineString(v, indent); isMultiline {
				return formatted
			}
			return strconv.Quote(v.AsString())
		case cty.Number:
			bf := v.AsBigFloat()
			return bf.Text('f', -1)
		case cty.Bool:
			if v.True() {
				return "true"
			} else {
				return "false"
			}
		}
	case ty.IsObjectType():
		return formatMappingValue(v, indent)
	case ty.IsTupleType():
		return formatSequenceValue(v, indent)
	case ty.IsListType():
		return fmt.Sprintf("tolist(%s)", formatSequenceValue(v, indent))
	case ty.IsSetType():
		return fmt.Sprintf("toset(%s)", formatSequenceValue(v, indent))
	case ty.IsMapType():
		return fmt.Sprintf("tomap(%s)", formatMappingValue(v, indent))
	}

	// Should never get here because there are no other types
	return fmt.Sprintf("%#v", v)
}

func formatMultilineString(v cty.Value, indent int) (string, bool) {
	str := v.AsString()
	lines := strings.Split(str, "\n")
	if len(lines) < 2 {
		return "", false
	}

	// If the value is indented, we use the indented form of heredoc for readability.
	operator := "<<"
	if indent > 0 {
		operator = "<<-"
	}

	// Default delimiter is "End Of Text" by convention
	delimiter := "EOT"

OUTER:
	for {
		// Check if any of the lines are in conflict with the delimiter. The
		// parser allows leading and trailing whitespace, so we must remove it
		// before comparison.
		for _, line := range lines {
			// If the delimiter matches a line, extend it and start again
			if strings.TrimSpace(line) == delimiter {
				delimiter = delimiter + "_"
				continue OUTER
			}
		}

		// None of the lines match the delimiter, so we're ready
		break
	}

	// Write the heredoc, with indentation as appropriate.
	var buf strings.Builder

	buf.WriteString(operator)
	buf.WriteString(delimiter)
	for _, line := range lines {
		buf.WriteByte('\n')
		buf.WriteString(strings.Repeat(" ", indent))
		buf.WriteString(line)
	}
	buf.WriteByte('\n')
	buf.WriteString(strings.Repeat(" ", indent))
	buf.WriteString(delimiter)

	return buf.String(), true
}

func formatMappingValue(v cty.Value, indent int) string {
	var buf strings.Builder
	count := 0
	buf.WriteByte('{')
	indent += 2
	for it := v.ElementIterator(); it.Next(); {
		count++
		k, v := it.Element()
		buf.WriteByte('\n')
		buf.WriteString(strings.Repeat(" ", indent))
		buf.WriteString(FormatValue(k, indent))
		buf.WriteString(" = ")
		buf.WriteString(FormatValue(v, indent))
	}
	indent -= 2
	if count > 0 {
		buf.WriteByte('\n')
		buf.WriteString(strings.Repeat(" ", indent))
	}
	buf.WriteByte('}')
	return buf.String()
}

func formatSequenceValue(v cty.Value, indent int) string {
	var buf strings.Builder
	count := 0
	buf.WriteByte('[')
	indent += 2
	for it := v.ElementIterator(); it.Next(); {
		count++
		_, v := it.Element()
		buf.WriteByte('\n')
		buf.WriteString(strings.Repeat(" ", indent))
		buf.WriteString(FormatValue(v, indent))
		buf.WriteByte(',')
	}
	indent -= 2
	if count > 0 {
		buf.WriteByte('\n')
		buf.WriteString(strings.Repeat(" ", indent))
	}
	buf.WriteByte(']')
	return buf.String()
}
