package plugin

import (
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"golang.org/x/mod/module"
)

const (
	DefaultRegistry  = "registry.gitlab.com"
	DefaultNamespace = "gitlab-org/fleeting/plugins"

	pluginPkgVersionKey = "FLEETING_PLUGIN_PKG_VERSION"
)

// bootstrap installs itself into the fleeting plugin directory.
//
// it is intended to be executed from within a container, where the only items
// alongside the plugin location are related to the plugin.
func bootstrap(pluginRepo string, version VersionInfo) error {
	if pluginRepo == "" {
		return fmt.Errorf("bootstrap requires plugin repository path as parameter, e.g: registry.gitlab.com/gitlab-org/fleeting/plugins/aws")
	}

	version.Version = strings.TrimPrefix(version.Version, "v")
	pkgVersion := os.Getenv(pluginPkgVersionKey)

	// notify on stderr if there's a discrepancy between pkg and binary version
	if !strings.HasPrefix(version.Version, pkgVersion) {
		fmt.Fprintf(os.Stderr, "warn: fleeting plugin package version and binary version differ: %q != %q\n", pkgVersion, version.Version)
	}

	// FLEETING_PLUGIN_PKG_VERSION takes precedent over the version bundled
	// with the binary if the binary version doesn't look like a
	// semantic version.
	parts := strings.SplitN(version.Version, ".", 3)
	if len(parts) != 3 {
		if pkgVersion != "" {
			version.Version = pkgVersion
		}

		// if there's no pkg or binary version, or "dev" is used, provide a
		// semantic pseudo version
		switch version.Version {
		case "", "dev":
			builtAt, _ := time.Parse(time.RFC3339, version.BuiltAt)
			version.Version = strings.TrimPrefix(module.PseudoVersion("v0", "", builtAt, version.Revision), "v")
		}
	}

	dir := os.Getenv("FLEETING_PLUGIN_PATH")
	if dir == "" {
		dir = "/plugins"
	}

	if _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) {
		return fmt.Errorf("plugin directory %v doesn't exist", dir)
	}

	pluginRepo, err := module.EscapePath(normalizeRepo(pluginRepo))
	if err != nil {
		return fmt.Errorf("escaping plugin path: %w", err)
	}

	pluginVersion, err := module.EscapeVersion(version.Version)
	if err != nil {
		return fmt.Errorf("escaping plugin version: %w", err)
	}

	path, err := os.Executable()
	if err != nil {
		return fmt.Errorf("getting executable path: %w", err)
	}

	pluginDir := filepath.Join(dir, pluginRepo, pluginVersion)

	if err := copyToPluginDir(filepath.Dir(path), dir, pluginDir); err != nil {
		return fmt.Errorf("bootstrap copy to plugin dir: %w", err)
	}

	// report plugin dir
	fmt.Println(pluginDir)

	return nil
}

func normalizeRepo(repo string) string {
	// add default registry and repository
	parts := strings.Split(repo, "/")
	switch {
	case len(parts) == 1:
		repo = path.Join(DefaultRegistry, DefaultNamespace, repo)
	case len(parts) > 1 && !strings.ContainsRune(parts[0], '.'):
		repo = path.Join(DefaultRegistry, repo)
	}

	// remove version, as we always use the version the binary reports
	base := path.Base(repo)
	idx := strings.IndexRune(base, ':')
	if idx > -1 {
		return path.Join(path.Dir(repo), base[:idx])
	}

	return repo
}

func copyToPluginDir(pathname, dir, pluginDir string) error {
	// bail early if plugin already exists
	if _, err := os.Stat(pluginDir); err == nil {
		return nil
	}

	tmpDir, err := os.MkdirTemp(dir, "plugin")
	if err != nil {
		return fmt.Errorf("creating temporary directory for plugin: %w", err)
	}
	defer os.RemoveAll(tmpDir)

	err = filepath.WalkDir(pathname, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if d.IsDir() {
			return nil
		}

		src, err := os.Open(path)
		if err != nil {
			return fmt.Errorf("opening file: %w", err)
		}
		defer src.Close()

		stat, err := src.Stat()
		if err != nil {
			return fmt.Errorf("stating file: %w", err)
		}

		dst, err := os.OpenFile(filepath.Join(tmpDir, d.Name()), os.O_CREATE|os.O_RDWR, stat.Mode().Perm())
		if err != nil {
			return fmt.Errorf("creating file: %w", err)
		}

		if _, err := io.Copy(dst, src); err != nil {
			return fmt.Errorf("copying file: %w", err)
		}

		if err := dst.Close(); err != nil {
			return fmt.Errorf("closing file: %w", err)
		}

		return nil
	})

	if err != nil {
		return fmt.Errorf("bootstrapping: %w", err)
	}

	if err := os.MkdirAll(filepath.Dir(pluginDir), 0o777); err != nil {
		return fmt.Errorf("creating plugin directory: %w", err)
	}

	if err := os.Rename(tmpDir, pluginDir); err != nil {
		return fmt.Errorf("renaming dir %v to %v", tmpDir, pluginDir)
	}

	return nil
}
