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

package getmodules

import (
	"context"
	"fmt"
	"maps"

	getter "github.com/hashicorp/go-getter"

	"github.com/opentofu/opentofu/internal/httpclient"
	"github.com/opentofu/opentofu/internal/tracing"
	"github.com/opentofu/opentofu/internal/tracing/traceattrs"
)

// PackageFetcher is a low-level utility for fetching remote module packages
// into local filesystem directories in preparation for use by higher-level
// module installer functionality implemented elsewhere.
//
// A PackageFetcher works only with entire module packages and never with
// the individual modules within a package.
//
// A particular PackageFetcher instance remembers the target directory of
// any successfully-installed package so that it can optimize future calls
// that have the same package address by copying the local directory tree,
// rather than fetching the package from its origin repeatedly. There is
// no way to reset this cache, so a particular PackageFetcher instance should
// live only for the duration of a single initialization process.
type PackageFetcher struct {
	getter *reusingGetter
}

// NewPackageFetcher constructs a new [PackageFetcher] that interacts with
// the rest of the system and with the execution environment using the
// given [PackageFetcherEnvironment] implementation.
//
// It's valid to set "env" to nil, but that will make certain module
// package source types unavailable for use and so that concession is
// intended only for use in unit tests.
func NewPackageFetcher(ctx context.Context, env PackageFetcherEnvironment) *PackageFetcher {
	env = preparePackageFetcherEnvironment(env)

	var httpClient = httpclient.New(ctx)

	// We use goGetterGetters as our starting point for the available
	// getters, but some need to be instantiated dynamically based on
	// the given "env". We shallow-copy the source map so that multiple
	// instances of PackageFetcher don't clobber each other's getters.
	getters := maps.Clone(goGetterGetters)

	// The OCI Distribution getter needs to acquire credentials based on
	// centrally-configured policy, encapsulated in env.OCIRepositoryStore.
	getters["oci"] = &ociDistributionGetter{
		getOCIRepositoryStore: env.OCIRepositoryStore,
	}

	// The HTTP getter (used for both "http" and "https" schemes) uses
	// the HTTP client we instantiated above, whose behavior can be
	// incluenced by the ctx argument we passed to it, such as by
	// enabling OpenTelemetry tracing when appropriate.
	var httpGetter = &getter.HttpGetter{
		Client:             httpClient,
		Netrc:              true,
		XTerraformGetLimit: 10,
	}
	getters["http"] = httpGetter
	getters["https"] = httpGetter

	return &PackageFetcher{
		getter: newReusingGetter(getters),
	}
}

// FetchPackage downloads or otherwise retrieves the filesystem inside the
// package at the given address into the given local installation directory.
//
// packageAddr must be formatted as if it were the result of an
// addrs.ModulePackage.String() call. It's only defined as a raw string here
// because the getmodules package can't import the addrs package due to
// that creating a package dependency cycle.
//
// PackageFetcher only works with entire packages. If the caller is processing
// a module source address which includes a subdirectory portion then the
// caller must resolve that itself, possibly with the help of the
// getmodules.SplitPackageSubdir and getmodules.ExpandSubdirGlobs functions.
func (f *PackageFetcher) FetchPackage(ctx context.Context, instDir string, packageAddr string) error {
	ctx, span := tracing.Tracer().Start(ctx, "Fetch Package",
		tracing.SpanAttributes(traceattrs.URLFull(packageAddr)),
	)
	defer span.End()
	err := f.getter.getWithGoGetter(ctx, instDir, packageAddr)
	if err != nil {
		span.RecordError(err)
		return err
	}
	return nil
}

// PackageFetcherEnvironment is an interface used with [NewPackageFetcher]
// to allow the caller to define how the package fetcher should interact
// with the rest of OpenTofu and with OpenTofu's execution environment.
//
// This interface may grow to include new methods if we learn of new
// requirements, so implementations of this interface should all be
// inside this codebase. If we decide to factor out module installation
// to a separate library for broader use in future then we should consider
// carefully whether a single interface aggregating all of the fetcher's
// concerns is still the best design for that different context.
type PackageFetcherEnvironment interface {
	OCIRepositoryStore(ctx context.Context, registryDomainName, repositoryPath string) (OCIRepositoryStore, error)
}

// preparePackageFetcherEnvironment takes a [PackageFetcherEnvironment]
// value provided directly by a caller to [NewPackageFetcher] and
// optionally wraps or replaces it as needed to better suit the
// implementation details of this package.
//
// Currently its only special behavior is that it replaces a nil
// PackageFetcherEnvironment with a default implementation that stubs
// out all of the methods, so that the other code in this package can
// then assume that the environment is always valid to call.
func preparePackageFetcherEnvironment(given PackageFetcherEnvironment) PackageFetcherEnvironment {
	if given == nil {
		return noopPackageFetcherEnvironment{}
	}
	return given
}

type noopPackageFetcherEnvironment struct{}

// OCIRepositoryStore implements PackageFetcherEnvironment.
func (n noopPackageFetcherEnvironment) OCIRepositoryStore(ctx context.Context, registryDomainName string, repositoryPath string) (OCIRepositoryStore, error) {
	return nil, fmt.Errorf("module installation from OCI repositories is not available in this context")
}
