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

package tofu

import (
	"fmt"
	"log"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

// The upgrade process from V2 to V3 state does not affect the structure,
// so we do not need to redeclare all of the structs involved - we just
// take a deep copy of the old structure and assert the version number is
// as we expect.
func upgradeStateV2ToV3(old *State) (*State, error) {
	new := old.DeepCopy()

	// Ensure the copied version is v2 before attempting to upgrade
	if new.Version != 2 {
		return nil, fmt.Errorf("Cannot apply v2->v3 state upgrade to " +
			"a state which is not version 2.")
	}

	// Set the new version number
	new.Version = 3

	// Change the counts for things which look like maps to use the %
	// syntax. Remove counts for empty collections - they will be added
	// back in later.
	for _, module := range new.Modules {
		for _, resource := range module.Resources {
			// Upgrade Primary
			if resource.Primary != nil {
				upgradeAttributesV2ToV3(resource.Primary)
			}

			// Upgrade Deposed
			if resource.Deposed != nil {
				for _, deposed := range resource.Deposed {
					upgradeAttributesV2ToV3(deposed)
				}
			}
		}
	}

	return new, nil
}

func upgradeAttributesV2ToV3(instanceState *InstanceState) error {
	collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`)
	collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`)

	// Identify the key prefix of anything which is a collection
	var collectionKeyPrefixes []string
	for key := range instanceState.Attributes {
		if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
			collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1])
		}
	}
	sort.Strings(collectionKeyPrefixes)

	log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes)

	// This could be rolled into fewer loops, but it is somewhat clearer this way, and will not
	// run very often.
	for _, prefix := range collectionKeyPrefixes {
		// First get the actual keys that belong to this prefix
		var potentialKeysMatching []string
		for key := range instanceState.Attributes {
			if strings.HasPrefix(key, prefix) {
				potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix))
			}
		}
		sort.Strings(potentialKeysMatching)

		var actualKeysMatching []string
		for _, key := range potentialKeysMatching {
			if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 {
				actualKeysMatching = append(actualKeysMatching, submatches[0][1])
			} else {
				if key != "#" {
					actualKeysMatching = append(actualKeysMatching, key)
				}
			}
		}
		actualKeysMatching = uniqueSortedStrings(actualKeysMatching)

		// Now inspect the keys in order to determine whether this is most likely to be
		// a map, list or set. There is room for error here, so we log in each case. If
		// there is no method of telling, we remove the key from the InstanceState in
		// order that it will be recreated. Again, this could be rolled into fewer loops
		// but we prefer clarity.

		oldCountKey := fmt.Sprintf("%s#", prefix)

		// First, detect "obvious" maps - which have non-numeric keys (mostly).
		hasNonNumericKeys := false
		for _, key := range actualKeysMatching {
			// Ensure that we attempt to parse the key using 64 bits, this is because the state
			// could've been generated on a 64-bit system, and we need to be able to
			// convert this on both a 32-bit and 64-bit arch.
			if _, err := strconv.ParseInt(key, 10, 64); err != nil {
				hasNonNumericKeys = true
			}
		}
		if hasNonNumericKeys {
			newCountKey := fmt.Sprintf("%s%%", prefix)

			instanceState.Attributes[newCountKey] = instanceState.Attributes[oldCountKey]
			delete(instanceState.Attributes, oldCountKey)
			log.Printf("[STATE UPGRADE] Detected %s as a map. Replaced count = %s",
				strings.TrimSuffix(prefix, "."), instanceState.Attributes[newCountKey])
		}

		// Now detect empty collections and remove them from state.
		if len(actualKeysMatching) == 0 {
			delete(instanceState.Attributes, oldCountKey)
			log.Printf("[STATE UPGRADE] Detected %s as an empty collection. Removed from state.",
				strings.TrimSuffix(prefix, "."))
		}
	}

	return nil
}

// uniqueSortedStrings removes duplicates from a slice of strings and returns
// a sorted slice of the unique strings.
func uniqueSortedStrings(input []string) []string {
	uniquemap := make(map[string]struct{})
	for _, str := range input {
		uniquemap[str] = struct{}{}
	}

	output := make([]string, len(uniquemap))

	i := 0
	for key := range uniquemap {
		output[i] = key
		i = i + 1
	}

	sort.Strings(output)
	return output
}
