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

package addrs

import (
	"github.com/hashicorp/hcl/v2"
	regaddr "github.com/opentofu/registry-address/v2"
	"github.com/opentofu/svchost"

	"github.com/opentofu/opentofu/internal/tfdiags"
)

// Provider encapsulates a single provider type. In the future this will be
// extended to include additional fields including Namespace and SourceHost
type Provider = regaddr.Provider

// DefaultProviderRegistryHost is the hostname used for provider addresses that do
// not have an explicit hostname.
const DefaultProviderRegistryHost = regaddr.DefaultProviderRegistryHost

// BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider
// namespace. Built-in provider addresses must also have their namespace set
// to BuiltInProviderNamespace in order to be considered as built-in.
//
// Since we currently only have one built-in provider and it was inherited
// from OpenTofu's predecessor, we currently exclusively use the "transitional"
// builtin provider host that matches what the predecessor used, thereby
// helping with cross-compatibility. If we introduce any OpenTofu-specific
// built-in providers in future then we should consider using
// [regaddr.BuiltInProviderHost] for those ones instead, since that one
// uses a hostname that belongs to the OpenTofu project.
const BuiltInProviderHost = regaddr.TransitionalBuiltInProviderHost

// BuiltInProviderNamespace is the provider namespace used for "built-in"
// providers. Built-in provider addresses must also have their hostname
// set to BuiltInProviderHost in order to be considered as built-in.
//
// The this namespace is literally named "builtin", in the hope that users
// who see FQNs containing this will be able to infer the way in which they are
// special, even if they haven't encountered the concept formally yet.
const BuiltInProviderNamespace = regaddr.BuiltInProviderNamespace

// LegacyProviderNamespace is the special string used in the Namespace field
// of type Provider to mark a legacy provider address. This special namespace
// value would normally be invalid, and can be used only when the hostname is
// DefaultRegistryHost because that host owns the mapping from legacy name to
// FQN.
const LegacyProviderNamespace = regaddr.LegacyProviderNamespace

func IsDefaultProvider(addr Provider) bool {
	return addr.Hostname == DefaultProviderRegistryHost && addr.Namespace == "hashicorp"
}

// NewProvider constructs a provider address from its parts, and normalizes
// the namespace and type parts to lowercase using unicode case folding rules
// so that resulting addrs.Provider values can be compared using standard
// Go equality rules (==).
//
// The hostname is given as a svchost.Hostname, which is required by the
// contract of that type to have already been normalized for equality testing.
//
// This function will panic if the given namespace or type name are not valid.
// When accepting namespace or type values from outside the program, use
// ParseProviderPart first to check that the given value is valid.
func NewProvider(hostname svchost.Hostname, namespace, typeName string) Provider {
	return regaddr.NewProvider(hostname, namespace, typeName)
}

// ImpliedProviderForUnqualifiedType represents the rules for inferring what
// provider FQN a user intended when only a naked type name is available.
//
// For all except the type name "terraform" this returns a so-called "default"
// provider, which is under the registry.terraform.io/hashicorp/ namespace.
//
// As a special case, the string "terraform" maps to
// "terraform.io/builtin/terraform" because that is the more likely user
// intent than the now-unmaintained "registry.terraform.io/hashicorp/terraform"
// which remains only for compatibility with older OpenTofu versions.
func ImpliedProviderForUnqualifiedType(typeName string) Provider {
	switch typeName {
	case "terraform":
		// Note for future maintainers: any additional strings we add here
		// as implied to be builtin must never also be use as provider names
		// in the registry.terraform.io/hashicorp/... namespace, because
		// otherwise older versions of OpenTofu could implicitly select
		// the registry name instead of the internal one.
		return NewBuiltInProvider(typeName)
	default:
		return NewDefaultProvider(typeName)
	}
}

// NewDefaultProvider returns the default address of a HashiCorp-maintained,
// Registry-hosted provider.
func NewDefaultProvider(name string) Provider {
	return regaddr.Provider{
		Type:      MustParseProviderPart(name),
		Namespace: "hashicorp",
		Hostname:  DefaultProviderRegistryHost,
	}
}

// NewBuiltInProvider returns the address of a "built-in" provider. See
// the docs for Provider.IsBuiltIn for more information.
func NewBuiltInProvider(name string) Provider {
	return regaddr.Provider{
		Type:      MustParseProviderPart(name),
		Namespace: BuiltInProviderNamespace,
		Hostname:  BuiltInProviderHost,
	}
}

// NewLegacyProvider returns a mock address for a provider.
// This will be removed when ProviderType is fully integrated.
func NewLegacyProvider(name string) Provider {
	return Provider{
		// We intentionally don't normalize and validate the legacy names,
		// because existing code expects legacy provider names to pass through
		// verbatim, even if not compliant with our new naming rules.
		Type:      name,
		Namespace: LegacyProviderNamespace,
		Hostname:  DefaultProviderRegistryHost,
	}
}

// ParseProviderSourceString parses a value of the form expected in the "source"
// argument of a required_providers entry and returns the corresponding
// fully-qualified provider address. This is intended primarily to parse the
// FQN-like strings returned by terraform-config-inspect.
//
// The following are valid source string formats:
//
//   - name
//   - namespace/name
//   - hostname/namespace/name
func ParseProviderSourceString(str string) (regaddr.Provider, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	ret, err := regaddr.ParseProviderSource(str)
	if pe, ok := err.(*regaddr.ParserError); ok {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  pe.Summary,
			Detail:   pe.Detail,
		})
		return ret, diags
	}

	if !ret.HasKnownNamespace() {
		ret.Namespace = "hashicorp"
	}

	return ret, nil
}

// MustParseProviderSourceString is a wrapper around ParseProviderSourceString that panics if
// it returns an error.
func MustParseProviderSourceString(str string) Provider {
	result, diags := ParseProviderSourceString(str)
	if diags.HasErrors() {
		panic(diags.Err().Error())
	}
	return result
}

// ParseProviderPart processes an addrs.Provider namespace or type string
// provided by an end-user, producing a normalized version if possible or
// an error if the string contains invalid characters.
//
// A provider part is processed in the same way as an individual label in a DNS
// domain name: it is transformed to lowercase per the usual DNS case mapping
// and normalization rules and may contain only letters, digits, and dashes.
// Additionally, dashes may not appear at the start or end of the string.
//
// These restrictions are intended to allow these names to appear in fussy
// contexts such as directory/file names on case-insensitive filesystems,
// repository names on GitHub, etc. We're using the DNS rules in particular,
// rather than some similar rules defined locally, because the hostname part
// of an addrs.Provider is already a hostname and it's ideal to use exactly
// the same case folding and normalization rules for all of the parts.
//
// In practice a provider type string conventionally does not contain dashes
// either. Such names are permitted, but providers with such type names will be
// hard to use because their resource type names will not be able to contain
// the provider type name and thus each resource will need an explicit provider
// address specified. (A real-world example of such a provider is the
// "google-beta" variant of the GCP provider, which has resource types that
// start with the "google_" prefix instead.)
//
// It's valid to pass the result of this function as the argument to a
// subsequent call, in which case the result will be identical.
func ParseProviderPart(given string) (string, error) {
	return regaddr.ParseProviderPart(given)
}

// MustParseProviderPart is a wrapper around ParseProviderPart that panics if
// it returns an error.
func MustParseProviderPart(given string) string {
	result, err := ParseProviderPart(given)
	if err != nil {
		panic(err.Error())
	}
	return result
}

// IsProviderPartNormalized compares a given string to the result of ParseProviderPart(string)
func IsProviderPartNormalized(str string) (bool, error) {
	normalized, err := ParseProviderPart(str)
	if err != nil {
		return false, err
	}
	if str == normalized {
		return true, nil
	}
	return false, nil
}
