// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package lang

import (
	"crypto/sha256"
	"fmt"
	"io"
	"log"
	"sync"

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

type priorResultHash struct {
	hash [sha256.Size]byte
	// when the result was from a current run, we keep a record of the result
	// value to aid in debugging. Results stored in the plan will only have the
	// hash to avoid bloating the plan with what could be many very large
	// values.
	value cty.Value
}

type FunctionResults struct {
	mu sync.Mutex
	// results stores the prior result from a function call, keyed by
	// the hash of the function name and arguments.
	results map[[sha256.Size]byte]priorResultHash
}

// NewFunctionResultsTable initializes a mapping of function calls to prior
// results used to validate function calls. The hashes argument is an
// optional slice of prior result hashes used to preload the cache.
func NewFunctionResultsTable(hashes []FunctionResultHash) *FunctionResults {
	res := &FunctionResults{
		results: make(map[[sha256.Size]byte]priorResultHash),
	}

	res.insertHashes(hashes)
	return res
}

// CheckPrior compares the function call against any cached results, and returns
// an error if the result does not match a prior call.
func (f *FunctionResults) CheckPrior(name string, args []cty.Value, result cty.Value) error {
	return f.CheckPriorProvider(addrs.Provider{}, name, args, result)
}

// CheckPriorProvider compares the provider function call against any cached
// results, and returns an error if the result does not match a prior call.
func (f *FunctionResults) CheckPriorProvider(provider addrs.Provider, name string, args []cty.Value, result cty.Value) error {
	// Don't cache unknown values. We could technically store types and
	// refinements for validation, but we don't currently have a way to
	// serialize those in the plan. Unknowns are also handled much more
	// gracefully throughout the evaluation system, whereas invalid data is
	// harder to trace back to the source since it's usually only visible due to
	// unexpected side-effects.
	if !result.IsKnown() {
		return nil
	}

	argSum := sha256.New()

	if !provider.IsZero() {
		io.WriteString(argSum, provider.String()+"|")
	}
	io.WriteString(argSum, name)

	for _, arg := range args {
		// cty.Values have a Hash method, but it is not collision resistant. We
		// are going to rely on the GoString formatting instead, which gives
		// detailed results for all values.
		io.WriteString(argSum, "|"+arg.GoString())
	}

	f.mu.Lock()
	defer f.mu.Unlock()

	argHash := [sha256.Size]byte(argSum.Sum(nil))
	resHash := sha256.Sum256([]byte(result.GoString()))

	res, ok := f.results[argHash]
	if !ok {
		f.results[argHash] = priorResultHash{
			hash:  resHash,
			value: result,
		}
		return nil
	}

	if resHash != res.hash {
		provPrefix := ""
		if !provider.IsZero() {
			provPrefix = fmt.Sprintf("provider %s ", provider)
		}
		// Log the args for debugging in case the hcl context is
		// insufficient. The error should be adequate most of the time, and
		// could already be quite long, so we don't want to add all
		// arguments too.
		log.Printf("[ERROR] %sfunction %s returned an inconsistent result with args: %#v\n", provPrefix, name, args)
		// The hcl package will add the necessary context around the error in
		// the diagnostic, but we add the differing results when we can.
		if res.value != cty.NilVal {
			return fmt.Errorf("function returned an inconsistent result,\nwas: %#v,\nnow: %#v", res.value, result)
		}
		return fmt.Errorf("function returned an inconsistent result")
	}

	return nil
}

// insertHashes insert key-value pairs to the functionResults map. This is used
// to preload stored values before any Verify calls are made.
func (f *FunctionResults) insertHashes(hashes []FunctionResultHash) {
	f.mu.Lock()
	defer f.mu.Unlock()

	for _, res := range hashes {
		f.results[[sha256.Size]byte(res.Key)] = priorResultHash{
			hash: [sha256.Size]byte(res.Result),
		}
	}
}

// FunctionResultHash contains the key and result hash values from a prior function
// call.
type FunctionResultHash struct {
	Key    []byte
	Result []byte
}

// copy the hash values into a struct which can be recorded in the plan.
func (f *FunctionResults) GetHashes() []FunctionResultHash {
	f.mu.Lock()
	defer f.mu.Unlock()

	var res []FunctionResultHash
	for k, r := range f.results {
		res = append(res, FunctionResultHash{Key: k[:], Result: r.hash[:]})
	}
	return res
}
