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

package statefile

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

	"github.com/mitchellh/copystructure"
)

func upgradeStateV2ToV3(old *stateV2) (*stateV3, error) {
	if old == nil {
		return (*stateV3)(nil), nil
	}

	var new *stateV3
	{
		copy, err := copystructure.Config{Lock: true}.Copy(old)
		if err != nil {
			panic(err)
		}
		newWrongType := copy.(*stateV2)
		newRightType := (stateV3)(*newWrongType)
		new = &newRightType
	}

	// 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
			for _, deposed := range resource.Deposed {
				upgradeAttributesV2ToV3(deposed)
			}
		}
	}

	return new, nil
}

func upgradeAttributesV2ToV3(instanceState *instanceStateV2) 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 {
			if _, err := strconv.Atoi(key); 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
}
