package initwd

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	cleanhttp "github.com/hashicorp/go-cleanhttp"
	getter "github.com/hashicorp/go-getter"
	"github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc"
)

// We configure our own go-getter detector and getter sets here, because
// the set of sources we support is part of Terraform's documentation and
// so we don't want any new sources introduced in go-getter to sneak in here
// and work even though they aren't documented. This also insulates us from
// any meddling that might be done by other go-getter callers linked into our
// executable.

var goGetterNoDetectors = []getter.Detector{}

var goGetterDecompressors = map[string]getter.Decompressor{
	"bz2": new(getter.Bzip2Decompressor),
	"gz":  new(getter.GzipDecompressor),
	"xz":  new(getter.XzDecompressor),
	"zip": new(getter.ZipDecompressor),

	"tar.bz2":  new(getter.TarBzip2Decompressor),
	"tar.tbz2": new(getter.TarBzip2Decompressor),

	"tar.gz": new(getter.TarGzipDecompressor),
	"tgz":    new(getter.TarGzipDecompressor),

	"tar.xz": new(getter.TarXzDecompressor),
	"txz":    new(getter.TarXzDecompressor),
}

var goGetterGetters = map[string]getter.Getter{
	"file":  new(getter.FileGetter),
	"gcs":   new(getter.GCSGetter),
	"git":   new(getter.GitGetter),
	"hg":    new(getter.HgGetter),
	"s3":    new(getter.S3Getter),
	"http":  getterHTTPGetter,
	"https": getterHTTPGetter,
}

var getterHTTPClient = cleanhttp.DefaultClient()

var getterHTTPGetter = &getter.HttpGetter{
	Client: getterHTTPClient,
	Netrc:  true,
}

// A reusingGetter is a helper for the module installer that remembers
// the final resolved addresses of all of the sources it has already been
// asked to install, and will copy from a prior installation directory if
// it has the same resolved source address.
//
// The keys in a reusingGetter are resolved and trimmed source addresses
// (with a scheme always present, and without any "subdir" component),
// and the values are the paths where each source was previously installed.
type reusingGetter map[string]string

// getWithGoGetter retrieves the package referenced in the given address
// into the installation path and then returns the full path to any subdir
// indicated in the address.
//
// The errors returned by this function are those surfaced by the underlying
// go-getter library, which have very inconsistent quality as
// end-user-actionable error messages. At this time we do not have any
// reasonable way to improve these error messages at this layer because
// the underlying errors are not separately recognizable.
func (g reusingGetter) getWithGoGetter(instPath, addr string) (string, error) {
	packageAddr, subDir := splitAddrSubdir(addr)

	log.Printf("[DEBUG] will download %q to %s", packageAddr, instPath)

	realAddr, err := getter.Detect(packageAddr, instPath, getter.Detectors)
	if err != nil {
		return "", err
	}

	if isMaybeRelativeLocalPath(realAddr) {
		return "", &MaybeRelativePathErr{addr}
	}

	var realSubDir string
	realAddr, realSubDir = splitAddrSubdir(realAddr)
	if realSubDir != "" {
		subDir = filepath.Join(realSubDir, subDir)
	}

	if realAddr != packageAddr {
		log.Printf("[TRACE] go-getter detectors rewrote %q to %q", packageAddr, realAddr)
	}

	if prevDir, exists := g[realAddr]; exists {
		log.Printf("[TRACE] copying previous install %s to %s", prevDir, instPath)
		err := os.Mkdir(instPath, os.ModePerm)
		if err != nil {
			return "", fmt.Errorf("failed to create directory %s: %s", instPath, err)
		}
		err = copyDir(instPath, prevDir)
		if err != nil {
			return "", fmt.Errorf("failed to copy from %s to %s: %s", prevDir, instPath, err)
		}
	} else {
		log.Printf("[TRACE] fetching %q to %q", realAddr, instPath)
		client := getter.Client{
			Src: realAddr,
			Dst: instPath,
			Pwd: instPath,

			Mode: getter.ClientModeDir,

			Detectors:     goGetterNoDetectors, // we already did detection above
			Decompressors: goGetterDecompressors,
			Getters:       goGetterGetters,
		}
		err = client.Get()
		if err != nil {
			return "", err
		}
		// Remember where we installed this so we might reuse this directory
		// on subsequent calls to avoid re-downloading.
		g[realAddr] = instPath
	}

	// Our subDir string can contain wildcards until this point, so that
	// e.g. a subDir of * can expand to one top-level directory in a .tar.gz
	// archive. Now that we've expanded the archive successfully we must
	// resolve that into a concrete path.
	var finalDir string
	if subDir != "" {
		finalDir, err = getter.SubdirGlob(instPath, subDir)
		log.Printf("[TRACE] expanded %q to %q", subDir, finalDir)
		if err != nil {
			return "", err
		}
	} else {
		finalDir = instPath
	}

	// If we got this far then we have apparently succeeded in downloading
	// the requested object!
	return filepath.Clean(finalDir), nil
}

// splitAddrSubdir splits the given address (which is assumed to be a
// registry address or go-getter-style address) into a package portion
// and a sub-directory portion.
//
// The package portion defines what should be downloaded and then the
// sub-directory portion, if present, specifies a sub-directory within
// the downloaded object (an archive, VCS repository, etc) that contains
// the module's configuration files.
//
// The subDir portion will be returned as empty if no subdir separator
// ("//") is present in the address.
func splitAddrSubdir(addr string) (packageAddr, subDir string) {
	return getter.SourceDirSubdir(addr)
}

var localSourcePrefixes = []string{
	"./",
	"../",
	".\\",
	"..\\",
}

func isLocalSourceAddr(addr string) bool {
	for _, prefix := range localSourcePrefixes {
		if strings.HasPrefix(addr, prefix) {
			return true
		}
	}
	return false
}

func isRegistrySourceAddr(addr string) bool {
	_, err := regsrc.ParseModuleSource(addr)
	return err == nil
}

type MaybeRelativePathErr struct {
	Addr string
}

func (e *MaybeRelativePathErr) Error() string {
	return fmt.Sprintf("Terraform cannot determine the module source for %s", e.Addr)
}

func isMaybeRelativeLocalPath(addr string) bool {
	if strings.HasPrefix(addr, "file://") {
		_, err := os.Stat(addr[7:])
		if err != nil {
			return true
		}
	}
	return false
}
