// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package format

import (
	"maps"
	"slices"

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

func isValueMarkedUnusable(v cty.Value) bool {
	return v.HasMark(marks.Sensitive) || v.HasMark(marks.Ephemeral)
}

// ObjectValueID takes a value that is assumed to be an object representation
// of some resource instance object and attempts to heuristically find an
// attribute of it that is likely to be a unique identifier in the remote
// system that it belongs to which will be useful to the user.
//
// If such an attribute is found, its name and string value intended for
// display are returned. Both returned strings are empty if no such attribute
// exists, in which case the caller should assume that the resource instance
// address within the OpenTofu configuration is the best available identifier.
//
// This is only a best-effort sort of thing, relying on naming conventions in
// our resource type schemas. The result is not guaranteed to be unique, but
// should generally be suitable for display to an end-user anyway.
//
// This function will panic if the given value is not of an object type.
func ObjectValueID(obj cty.Value) (k, v string) {
	if obj.IsNull() || !obj.IsKnown() {
		return "", ""
	}

	atys := obj.Type().AttributeTypes()

	switch {

	case atys["id"] == cty.String:
		v := obj.GetAttr("id")
		if isValueMarkedUnusable(v) {
			break
		}
		v, _ = v.Unmark()

		if v.IsKnown() && !v.IsNull() {
			return "id", v.AsString()
		}

	case atys["name"] == cty.String:
		// "name" isn't always globally unique, but if there isn't also an
		// "id" then it _often_ is, in practice.
		v := obj.GetAttr("name")
		if isValueMarkedUnusable(v) {
			break
		}
		v, _ = v.Unmark()

		if v.IsKnown() && !v.IsNull() {
			return "name", v.AsString()
		}
	}

	return "", ""
}

// ObjectValueName takes a value that is assumed to be an object representation
// of some resource instance object and attempts to heuristically find an
// attribute of it that is likely to be a human-friendly name in the remote
// system that it belongs to which will be useful to the user.
//
// If such an attribute is found, its name and string value intended for
// display are returned. Both returned strings are empty if no such attribute
// exists, in which case the caller should assume that the resource instance
// address within the OpenTofu configuration is the best available identifier.
//
// This is only a best-effort sort of thing, relying on naming conventions in
// our resource type schemas. The result is not guaranteed to be unique, but
// should generally be suitable for display to an end-user anyway.
//
// Callers that use both ObjectValueName and ObjectValueID at the same time
// should be prepared to get the same attribute key and value from both in
// some cases, since there is overlap between the id-extraction and
// name-extraction heuristics.
//
// This function will panic if the given value is not of an object type.
func ObjectValueName(obj cty.Value) (k, v string) {
	if obj.IsNull() || !obj.IsKnown() {
		return "", ""
	}

	atys := obj.Type().AttributeTypes()

	switch {

	case atys["name"] == cty.String:
		v := obj.GetAttr("name")
		if isValueMarkedUnusable(v) {
			break
		}
		v, _ = v.Unmark()

		if v.IsKnown() && !v.IsNull() {
			return "name", v.AsString()
		}

	case atys["tags"].IsMapType() && atys["tags"].ElementType() == cty.String:
		tags := obj.GetAttr("tags")
		if tags.IsNull() || !tags.IsWhollyKnown() || isValueMarkedUnusable(tags) {
			break
		}
		tags, _ = tags.Unmark()

		switch {
		case tags.HasIndex(cty.StringVal("name")).RawEquals(cty.True):
			v := tags.Index(cty.StringVal("name"))
			if isValueMarkedUnusable(v) {
				break
			}
			v, _ = v.Unmark()

			if v.IsKnown() && !v.IsNull() {
				return "tags.name", v.AsString()
			}
		case tags.HasIndex(cty.StringVal("Name")).RawEquals(cty.True):
			// AWS-style naming convention
			v := tags.Index(cty.StringVal("Name"))
			if isValueMarkedUnusable(v) {
				break
			}
			v, _ = v.Unmark()

			if v.IsKnown() && !v.IsNull() {
				return "tags.Name", v.AsString()
			}
		}
	}

	return "", ""
}

// ObjectValueIDOrName is a convenience wrapper around both ObjectValueID
// and ObjectValueName (in that preference order) to try to extract some sort
// of human-friendly descriptive string value for an object as additional
// context about an object when it is being displayed in a compact way (where
// not all of the attributes are visible.)
//
// Just as with the two functions it wraps, it is a best-effort and may return
// two empty strings if no suitable attribute can be found for a given object.
func ObjectValueIDOrName(obj cty.Value) (k, v string) {
	k, v = ObjectValueID(obj)
	if k != "" {
		return
	}
	k, v = ObjectValueName(obj)
	return
}

// ObjectValueBestGuess is a wrapper around ObjectValueIDOrName that attempts to extract the best possible human-friendly
// string value for an object by first trying the ID and Name heuristics, and then falling back to any other string attribute if those fail.
// This is intended to be used in cases where we want to display some sort of human-friendly context about an object,
// but we don't have a specific expectation about which attribute will be the most useful for that purpose.
//
// Just as with the functions it wraps, it may return two empty strings if no suitable attribute can be found for a given object.
func ObjectValueBestGuess(obj cty.Value) (k, v string) {
	// First,attempt to fetch it from id or name attributes, which are the most likely to be unique and human-friendly, respectively.
	k, v = ObjectValueIDOrName(obj)
	if k != "" {
		return
	}

	// If we cant get anything, just return "", "" to indicate that we have no useful information about this object.
	if obj.IsNull() || !obj.IsKnown() {
		return "", ""
	}

	attributeTypes := obj.Type().AttributeTypes()

	// Fetch whichever string attribute is first in the list that is not marked as unusable, and return it as the best effort description of this object.
	// We iterate over the attributes in sorted order to ensure that we get a deterministic result, even if the underlying map iteration order is not deterministic.
	for _, k := range slices.Sorted(maps.Keys(attributeTypes)) {
		// We specifically only want strings
		if attributeTypes[k] != cty.String {
			continue
		}

		v := obj.GetAttr(k)
		if v.IsNull() || !v.IsKnown() || isValueMarkedUnusable(v) {
			continue
		}
		v, _ = v.Unmark()

		if v.IsKnown() && !v.IsNull() {
			return k, v.AsString()
		}
	}

	// If we got here, then we have no useful information about this object, so we return "", "" to indicate that the
	// caller should skip printing this information, or attempt another way
	return "", ""
}
