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

package discovery

import (
	"bytes"
)

// PluginInstallProtocolVersion is the protocol version TF-core
// supports to communicate with servers, and is used to resolve
// plugin discovery with OpenTofu registry, in addition to
// any specified plugin version constraints
const PluginInstallProtocolVersion = 5

// PluginRequirements describes a set of plugins (assumed to be of a consistent
// kind) that are required to exist and have versions within the given
// corresponding sets.
type PluginRequirements map[string]*PluginConstraints

// PluginConstraints represents an element of PluginRequirements describing
// the constraints for a single plugin.
type PluginConstraints struct {
	// Specifies that the plugin's version must be within the given
	// constraints.
	Versions Constraints

	// If non-nil, the hash of the on-disk plugin executable must exactly
	// match the SHA256 hash given here.
	SHA256 []byte
}

// Allows returns true if the given version is within the receiver's version
// constraints.
func (s *PluginConstraints) Allows(v Version) bool {
	return s.Versions.Allows(v)
}

// AcceptsSHA256 returns true if the given executable SHA256 hash is acceptable,
// either because it matches the constraint or because there is no such
// constraint.
func (s *PluginConstraints) AcceptsSHA256(digest []byte) bool {
	if s.SHA256 == nil {
		return true
	}
	return bytes.Equal(s.SHA256, digest)
}

// Merge takes the contents of the receiver and the other given requirements
// object and merges them together into a single requirements structure
// that satisfies both sets of requirements.
//
// Note that it doesn't make sense to merge two PluginRequirements with
// differing required plugin SHA256 hashes, since the result will never
// match any plugin.
func (r PluginRequirements) Merge(other PluginRequirements) PluginRequirements {
	ret := make(PluginRequirements)
	for n, c := range r {
		ret[n] = &PluginConstraints{
			Versions: Constraints{}.Append(c.Versions),
			SHA256:   c.SHA256,
		}
	}
	for n, c := range other {
		if existing, exists := ret[n]; exists {
			ret[n].Versions = ret[n].Versions.Append(c.Versions)

			if existing.SHA256 != nil {
				if c.SHA256 != nil && !bytes.Equal(c.SHA256, existing.SHA256) {
					// If we've been asked to merge two constraints with
					// different SHA256 hashes then we'll produce a dummy value
					// that can never match anything. This is a silly edge case
					// that no reasonable caller should hit.
					ret[n].SHA256 = []byte(invalidProviderHash)
				}
			} else {
				ret[n].SHA256 = c.SHA256 // might still be nil
			}
		} else {
			ret[n] = &PluginConstraints{
				Versions: Constraints{}.Append(c.Versions),
				SHA256:   c.SHA256,
			}
		}
	}
	return ret
}

// LockExecutables applies additional constraints to the receiver that
// require plugin executables with specific SHA256 digests. This modifies
// the receiver in-place, since it's intended to be applied after
// version constraints have been resolved.
//
// The given map must include a key for every plugin that is already
// required. If not, any missing keys will cause the corresponding plugin
// to never match, though the direct caller doesn't necessarily need to
// guarantee this as long as the downstream code _applying_ these constraints
// is able to deal with the non-match in some way.
func (r PluginRequirements) LockExecutables(sha256s map[string][]byte) {
	for name, cons := range r {
		digest := sha256s[name]

		if digest == nil {
			// Prevent any match, which will then presumably cause the
			// downstream consumer of this requirements to report an error.
			cons.SHA256 = []byte(invalidProviderHash)
			continue
		}

		cons.SHA256 = digest
	}
}

const invalidProviderHash = "<invalid>"
