package getproviders

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"path"

	"github.com/camptocamp/terraboard/internal/terraform/addrs"
	"github.com/hashicorp/go-retryablehttp"
	svchost "github.com/hashicorp/terraform-svchost"
)

// MissingProviderSuggestion takes a provider address that failed installation
// due to the remote registry reporting that it didn't exist, and attempts
// to find another provider that the user might have meant to select.
//
// If the result is equal to the given address then that indicates that there
// is no suggested alternative to offer, either because the function
// successfully determined there is no recorded alternative or because the
// lookup failed somehow. We don't consider a failure to find a suggestion
// as an installation failure, because the caller should already be reporting
// that the provider didn't exist anyway and this is only extra context for
// that error message.
//
// The result of this is a best effort, so any UI presenting it should be
// careful to give it only as a possibility and not necessarily a suitable
// replacement for the given provider.
//
// In practice today this function only knows how to suggest alternatives for
// "default" providers, which is to say ones that are in the hashicorp
// namespace in the Terraform registry. It will always return no result for
// any other provider. That might change in future if we introduce other ways
// to discover provider suggestions.
//
// If the given context is cancelled then this function might not return a
// renaming suggestion even if one would've been available for a completed
// request.
func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source, reqs Requirements) addrs.Provider {
	if !addr.IsDefault() {
		return addr
	}

	// Before possibly looking up legacy naming, see if the user has another provider
	// named in their requirements that is of the same type, and offer that
	// as a suggestion
	for req := range reqs {
		if req != addr && req.Type == addr.Type {
			return req
		}
	}

	// Our strategy here, for a default provider, is to use the default
	// registry's special API for looking up "legacy" providers and try looking
	// for a legacy provider whose type name matches the type of the given
	// provider. This should then find a suitable answer for any provider
	// that was originally auto-installable in v0.12 and earlier but moved
	// into a non-default namespace as part of introducing the hierarchical
	// provider namespace.
	//
	// To achieve that, we need to find the direct registry client in
	// particular from the given source, because that is the only Source
	// implementation that can actually handle a legacy provider lookup.
	regSource := findLegacyProviderLookupSource(addr.Hostname, source)
	if regSource == nil {
		// If there's no direct registry source in the installation config
		// then we can't provide a renaming suggestion.
		return addr
	}

	defaultNS, redirectNS, err := regSource.lookupLegacyProviderNamespace(ctx, addr.Hostname, addr.Type)
	if err != nil {
		return addr
	}

	switch {
	case redirectNS != "":
		return addrs.Provider{
			Hostname:  addr.Hostname,
			Namespace: redirectNS,
			Type:      addr.Type,
		}
	default:
		return addrs.Provider{
			Hostname:  addr.Hostname,
			Namespace: defaultNS,
			Type:      addr.Type,
		}
	}
}

// findLegacyProviderLookupSource tries to find a *RegistrySource that can talk
// to the given registry host in the given Source. It might be given directly,
// or it might be given indirectly via a MultiSource where the selector
// includes a wildcard for registry.terraform.io.
//
// Returns nil if the given source does not have any configured way to talk
// directly to the given host.
//
// If the given source contains multiple sources that can talk to the given
// host directly, the first one in the sequence takes preference. In practice
// it's pointless to have two direct installation sources that match the same
// hostname anyway, so this shouldn't arise in normal use.
func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *RegistrySource {
	switch source := source.(type) {

	case *RegistrySource:
		// Easy case: the source is a registry source directly, and so we'll
		// just use it.
		return source

	case *MemoizeSource:
		// Also easy: the source is a memoize wrapper, so defer to its
		// underlying source.
		return findLegacyProviderLookupSource(host, source.underlying)

	case MultiSource:
		// Trickier case: if it's a multisource then we need to scan over
		// its selectors until we find one that is a *RegistrySource _and_
		// that is configured to accept arbitrary providers from the
		// given hostname.

		// For our matching purposes we'll use an address that would not be
		// valid as a real provider FQN and thus can only match a selector
		// that has no filters at all or a selector that wildcards everything
		// except the hostname, like "registry.terraform.io/*/*"
		matchAddr := addrs.Provider{
			Hostname: host,
			// Other fields are intentionally left empty, to make this invalid
			// as a specific provider address.
		}

		for _, selector := range source {
			// If this source has suitable matching patterns to install from
			// the given hostname then we'll recursively search inside it
			// for *RegistrySource objects.
			if selector.CanHandleProvider(matchAddr) {
				ret := findLegacyProviderLookupSource(host, selector.Source)
				if ret != nil {
					return ret
				}
			}
		}

		// If we get here then there were no selectors that are both configured
		// to handle modules from the given hostname and that are registry
		// sources, so we fail.
		return nil

	default:
		// This source cannot be and cannot contain a *RegistrySource, so
		// we fail.
		return nil
	}
}

// lookupLegacyProviderNamespace is a special method available only on
// RegistrySource which can deal with legacy provider addresses that contain
// only a type and leave the namespace implied.
//
// It asks the registry at the given hostname to provide a default namespace
// for the given provider type, which can be combined with the given hostname
// and type name to produce a fully-qualified provider address.
//
// Not all unqualified type names can be resolved to a default namespace. If
// the request fails, this method returns an error describing the failure.
//
// This method exists only to allow compatibility with unqualified names
// in older configurations. New configurations should be written so as not to
// depend on it, and this fallback mechanism will likely be removed altogether
// in a future Terraform version.
func (s *RegistrySource) lookupLegacyProviderNamespace(ctx context.Context, hostname svchost.Hostname, typeName string) (string, string, error) {
	client, err := s.registryClient(hostname)
	if err != nil {
		return "", "", err
	}
	return client.legacyProviderDefaultNamespace(ctx, typeName)
}

// legacyProviderDefaultNamespace returns the raw address strings produced by
// the registry when asked about the given unqualified provider type name.
// The returned namespace string is taken verbatim from the registry's response.
//
// This method exists only to allow compatibility with unqualified names
// in older configurations. New configurations should be written so as not to
// depend on it.
func (c *registryClient) legacyProviderDefaultNamespace(ctx context.Context, typeName string) (string, string, error) {
	endpointPath, err := url.Parse(path.Join("-", typeName, "versions"))
	if err != nil {
		// Should never happen because we're constructing this from
		// already-validated components.
		return "", "", err
	}
	endpointURL := c.baseURL.ResolveReference(endpointPath)

	req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
	if err != nil {
		return "", "", err
	}
	req = req.WithContext(ctx)
	c.addHeadersToRequest(req.Request)

	// This is just to give us something to return in error messages. It's
	// not a proper provider address.
	placeholderProviderAddr := addrs.NewLegacyProvider(typeName)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", "", c.errQueryFailed(placeholderProviderAddr, err)
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		// Great!
	case http.StatusNotFound:
		return "", "", ErrProviderNotFound{
			Provider: placeholderProviderAddr,
		}
	case http.StatusUnauthorized, http.StatusForbidden:
		return "", "", c.errUnauthorized(placeholderProviderAddr.Hostname)
	default:
		return "", "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status))
	}

	type ResponseBody struct {
		Id      string `json:"id"`
		MovedTo string `json:"moved_to"`
	}
	var body ResponseBody

	dec := json.NewDecoder(resp.Body)
	if err := dec.Decode(&body); err != nil {
		return "", "", c.errQueryFailed(placeholderProviderAddr, err)
	}

	provider, diags := addrs.ParseProviderSourceString(body.Id)
	if diags.HasErrors() {
		return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
	}

	if provider.Type != typeName {
		return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", provider.Type, typeName)
	}

	var movedTo addrs.Provider
	if body.MovedTo != "" {
		movedTo, diags = addrs.ParseProviderSourceString(body.MovedTo)
		if diags.HasErrors() {
			return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
		}

		if movedTo.Type != typeName {
			return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", movedTo.Type, typeName)
		}
	}

	return provider.Namespace, movedTo.Namespace, nil
}
