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

package hcl2shim

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"

	"github.com/zclconf/go-cty/cty"
)

// RequiresReplace takes a list of flatmapped paths from a
// InstanceDiff.Attributes along with the corresponding cty.Type, and returns
// the list of the cty.Paths that are flagged as causing the resource
// replacement (RequiresNew).
// This will filter out redundant paths, paths that refer to flatmapped indexes
// (e.g. "#", "%"), and will return any changes within a set as the path to the
// set itself.
func RequiresReplace(attrs []string, ty cty.Type) ([]cty.Path, error) {
	var paths []cty.Path

	for _, attr := range attrs {
		p, err := requiresReplacePath(attr, ty)
		if err != nil {
			return nil, err
		}

		paths = append(paths, p)
	}

	// now trim off any trailing paths that aren't GetAttrSteps, since only an
	// attribute itself can require replacement
	paths = trimPaths(paths)

	// There may be redundant paths due to set elements or index attributes
	// Do some ugly n^2 filtering, but these are always fairly small sets.
	for i := 0; i < len(paths)-1; i++ {
		for j := i + 1; j < len(paths); j++ {
			if reflect.DeepEqual(paths[i], paths[j]) {
				// swap the tail and slice it off
				paths[j], paths[len(paths)-1] = paths[len(paths)-1], paths[j]
				paths = paths[:len(paths)-1]
				j--
			}
		}
	}

	return paths, nil
}

// trimPaths removes any trailing steps that aren't of type GetAttrSet, since
// only an attribute itself can require replacement
func trimPaths(paths []cty.Path) []cty.Path {
	var trimmed []cty.Path
	for _, path := range paths {
		path = trimPath(path)
		if len(path) > 0 {
			trimmed = append(trimmed, path)
		}
	}
	return trimmed
}

func trimPath(path cty.Path) cty.Path {
	for len(path) > 0 {
		_, isGetAttr := path[len(path)-1].(cty.GetAttrStep)
		if isGetAttr {
			break
		}
		path = path[:len(path)-1]
	}
	return path
}

// requiresReplacePath takes a key from a flatmap along with the cty.Type
// describing the structure, and returns the cty.Path that would be used to
// reference the nested value in the data structure.
// This is used specifically to record the RequiresReplace attributes from a
// ResourceInstanceDiff.
func requiresReplacePath(k string, ty cty.Type) (cty.Path, error) {
	if k == "" {
		return nil, nil
	}
	if !ty.IsObjectType() {
		panic(fmt.Sprintf("requires replace path on non-object type: %#v", ty))
	}

	path, err := pathFromFlatmapKeyObject(k, ty.AttributeTypes())
	if err != nil {
		return path, fmt.Errorf("[%s] %w", k, err)
	}
	return path, nil
}

func pathSplit(p string) (string, string) {
	parts := strings.SplitN(p, ".", 2)
	head := parts[0]
	rest := ""
	if len(parts) > 1 {
		rest = parts[1]
	}
	return head, rest
}

func pathFromFlatmapKeyObject(key string, atys map[string]cty.Type) (cty.Path, error) {
	k, rest := pathSplit(key)

	path := cty.Path{cty.GetAttrStep{Name: k}}

	ty, ok := atys[k]
	if !ok {
		return path, fmt.Errorf("attribute %q not found", k)
	}

	if rest == "" {
		return path, nil
	}

	p, err := pathFromFlatmapKeyValue(rest, ty)
	if err != nil {
		return path, err
	}

	return append(path, p...), nil
}

func pathFromFlatmapKeyValue(key string, ty cty.Type) (cty.Path, error) {
	var path cty.Path
	var err error

	switch {
	case ty.IsPrimitiveType():
		err = fmt.Errorf("invalid step %q with type %#v", key, ty)
	case ty.IsObjectType():
		path, err = pathFromFlatmapKeyObject(key, ty.AttributeTypes())
	case ty.IsTupleType():
		path, err = pathFromFlatmapKeyTuple(key, ty.TupleElementTypes())
	case ty.IsMapType():
		path, err = pathFromFlatmapKeyMap(key, ty)
	case ty.IsListType():
		path, err = pathFromFlatmapKeyList(key, ty)
	case ty.IsSetType():
		path, err = pathFromFlatmapKeySet(key, ty)
	default:
		err = fmt.Errorf("unrecognized type: %s", ty.FriendlyName())
	}

	if err != nil {
		return path, err
	}

	return path, nil
}

func pathFromFlatmapKeyTuple(key string, etys []cty.Type) (cty.Path, error) {
	var path cty.Path
	var err error

	k, rest := pathSplit(key)

	// we don't need to convert the index keys to paths
	if k == "#" {
		return path, nil
	}

	idx, err := strconv.Atoi(k)
	if err != nil {
		return path, err
	}

	path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}}

	if idx >= len(etys) {
		return path, fmt.Errorf("index %s out of range in %#v", key, etys)
	}

	if rest == "" {
		return path, nil
	}

	ty := etys[idx]

	p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
	if err != nil {
		return path, err
	}

	return append(path, p...), nil
}

func pathFromFlatmapKeyMap(key string, ty cty.Type) (cty.Path, error) {
	var path cty.Path
	var err error

	k, rest := key, ""
	if !ty.ElementType().IsPrimitiveType() {
		k, rest = pathSplit(key)
	}

	// we don't need to convert the index keys to paths
	if k == "%" {
		return path, nil
	}

	path = cty.Path{cty.IndexStep{Key: cty.StringVal(k)}}

	if rest == "" {
		return path, nil
	}

	p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
	if err != nil {
		return path, err
	}

	return append(path, p...), nil
}

func pathFromFlatmapKeyList(key string, ty cty.Type) (cty.Path, error) {
	var path cty.Path
	var err error

	k, rest := pathSplit(key)

	// we don't need to convert the index keys to paths
	if key == "#" {
		return path, nil
	}

	idx, err := strconv.Atoi(k)
	if err != nil {
		return path, err
	}

	path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}}

	if rest == "" {
		return path, nil
	}

	p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
	if err != nil {
		return path, err
	}

	return append(path, p...), nil
}

func pathFromFlatmapKeySet(key string, ty cty.Type) (cty.Path, error) {
	// once we hit a set, we can't return consistent paths, so just mark the
	// set as a whole changed.
	return nil, nil
}

// FlatmapKeyFromPath returns the flatmap equivalent of the given cty.Path for
// use in generating legacy style diffs.
func FlatmapKeyFromPath(path cty.Path) string {
	var parts []string

	for _, step := range path {
		switch step := step.(type) {
		case cty.GetAttrStep:
			parts = append(parts, step.Name)
		case cty.IndexStep:
			switch ty := step.Key.Type(); ty {
			case cty.String:
				parts = append(parts, step.Key.AsString())
			case cty.Number:
				i, _ := step.Key.AsBigFloat().Int64()
				parts = append(parts, strconv.Itoa(int(i)))
			}
		}
	}

	return strings.Join(parts, ".")
}
