// 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 (
	"log"
	"os"
	"path/filepath"
	"strings"
)

// FindPlugins looks in the given directories for files whose filenames
// suggest that they are plugins of the given kind (e.g. "provider") and
// returns a PluginMetaSet representing the discovered potential-plugins.
//
// Currently this supports two different naming schemes. The current
// standard naming scheme is a subdirectory called $GOOS-$GOARCH containing
// files named terraform-$KIND-$NAME-V$VERSION. The legacy naming scheme is
// files directly in the given directory whose names are like
// terraform-$KIND-$NAME.
//
// Only one plugin will be returned for each unique plugin (name, version)
// pair, with preference given to files found in earlier directories.
//
// This is a convenience wrapper around FindPluginPaths and ResolvePluginsPaths.
func FindPlugins(kind string, dirs []string) PluginMetaSet {
	return ResolvePluginPaths(FindPluginPaths(kind, dirs))
}

// FindPluginPaths looks in the given directories for files whose filenames
// suggest that they are plugins of the given kind (e.g. "provider").
//
// The return value is a list of absolute paths that appear to refer to
// plugins in the given directories, based only on what can be inferred
// from the naming scheme. The paths returned are ordered such that files
// in later dirs appear after files in earlier dirs in the given directory
// list. Within the same directory plugins are returned in a consistent but
// undefined order.
func FindPluginPaths(kind string, dirs []string) []string {
	// This is just a thin wrapper around findPluginPaths so that we can
	// use the latter in tests with a fake machineName so we can use our
	// test fixtures.
	return findPluginPaths(kind, dirs)
}

func findPluginPaths(kind string, dirs []string) []string {
	prefix := "terraform-" + kind + "-"

	ret := make([]string, 0, len(dirs))

	for _, dir := range dirs {
		items, err := os.ReadDir(dir)
		if err != nil {
			// Ignore missing dirs, non-dirs, etc
			continue
		}

		log.Printf("[DEBUG] checking for %s in %q", kind, dir)

		for _, item := range items {
			fullName := item.Name()

			if !strings.HasPrefix(fullName, prefix) {
				continue
			}

			// New-style paths must have a version segment in filename
			if strings.Contains(strings.ToLower(fullName), "_v") {
				absPath, err := filepath.Abs(filepath.Join(dir, fullName))
				if err != nil {
					log.Printf("[ERROR] plugin filepath error: %s", err)
					continue
				}

				// Check that the file we found is usable
				if !pathIsFile(absPath) {
					log.Printf("[ERROR] ignoring non-file %s", absPath)
					continue
				}

				log.Printf("[DEBUG] found %s %q", kind, fullName)
				ret = append(ret, filepath.Clean(absPath))
				continue
			}

			// Legacy style with files directly in the base directory
			absPath, err := filepath.Abs(filepath.Join(dir, fullName))
			if err != nil {
				log.Printf("[ERROR] plugin filepath error: %s", err)
				continue
			}

			// Check that the file we found is usable
			if !pathIsFile(absPath) {
				log.Printf("[ERROR] ignoring non-file %s", absPath)
				continue
			}

			log.Printf("[WARN] found legacy %s %q", kind, fullName)

			ret = append(ret, filepath.Clean(absPath))
		}
	}

	return ret
}

// Returns true if and only if the given path refers to a file or a symlink
// to a file.
func pathIsFile(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}

	return !info.IsDir()
}

// ResolvePluginPaths takes a list of paths to plugin executables (as returned
// by e.g. FindPluginPaths) and produces a PluginMetaSet describing the
// referenced plugins.
//
// If the same combination of plugin name and version appears multiple times,
// the earlier reference will be preferred. Several different versions of
// the same plugin name may be returned, in which case the methods of
// PluginMetaSet can be used to filter down.
func ResolvePluginPaths(paths []string) PluginMetaSet {
	s := make(PluginMetaSet)

	type nameVersion struct {
		Name    string
		Version string
	}
	found := make(map[nameVersion]struct{})

	for _, path := range paths {
		baseName := strings.ToLower(filepath.Base(path))
		if !strings.HasPrefix(baseName, "terraform-") {
			// Should never happen with reasonable input
			continue
		}

		baseName = baseName[10:]
		firstDash := strings.Index(baseName, "-")
		if firstDash == -1 {
			// Should never happen with reasonable input
			continue
		}

		baseName = baseName[firstDash+1:]
		if baseName == "" {
			// Should never happen with reasonable input
			continue
		}

		// Trim the .exe suffix used on Windows before we start wrangling
		// the remainder of the path.
		baseName = strings.TrimSuffix(baseName, ".exe")

		parts := strings.SplitN(baseName, "_v", 2)
		name := parts[0]
		version := VersionZero
		if len(parts) == 2 {
			version = parts[1]
		}

		// Auto-installed plugins contain an extra name portion representing
		// the expected plugin version, which we must trim off.
		if underX := strings.Index(version, "_x"); underX != -1 {
			version = version[:underX]
		}

		if _, ok := found[nameVersion{name, version}]; ok {
			// Skip duplicate versions of the same plugin
			// (We do this during this step because after this we will be
			// dealing with sets and thus lose our ordering with which to
			// decide preference.)
			continue
		}

		s.Add(PluginMeta{
			Name:    name,
			Version: VersionStr(version),
			Path:    path,
		})
		found[nameVersion{name, version}] = struct{}{}
	}

	return s
}
