package terraform

import (
	"context"
	"os"
	"path/filepath"
	"time"

	"github.com/gofrs/flock"
	"github.com/hashicorp/go-version"
	"github.com/hashicorp/hc-install/product"
	"github.com/hashicorp/hc-install/releases"
	"golang.org/x/xerrors"

	"cdr.dev/slog"
)

var (
	// TerraformVersion is the version of Terraform used internally
	// when Terraform is not available on the system.
	// NOTE: Keep this in sync with the version in scripts/Dockerfile.base.
	// NOTE: Keep this in sync with the version in install.sh.
	TerraformVersion = version.Must(version.NewVersion("1.6.6"))

	minTerraformVersion = version.Must(version.NewVersion("1.1.0"))
	maxTerraformVersion = version.Must(version.NewVersion("1.6.9")) // use .9 to automatically allow patch releases

	terraformMinorVersionMismatch = xerrors.New("Terraform binary minor version mismatch.")
)

// Install implements a thread-safe, idempotent Terraform Install
// operation.
func Install(ctx context.Context, log slog.Logger, dir string, wantVersion *version.Version) (string, error) {
	err := os.MkdirAll(dir, 0o750)
	if err != nil {
		return "", err
	}

	// Windows requires a separate lock file.
	// See https://github.com/pinterest/knox/blob/master/client/flock_windows.go#L64
	// for precedent.
	lockFilePath := filepath.Join(dir, "lock")
	lock := flock.New(lockFilePath)
	ok, err := lock.TryLockContext(ctx, time.Millisecond*100)
	if !ok {
		return "", xerrors.Errorf("could not acquire flock for %v: %w", lockFilePath, err)
	}
	defer lock.Close()

	binPath := filepath.Join(dir, product.Terraform.BinaryName())

	hasVersion, err := versionFromBinaryPath(ctx, binPath)
	if err == nil && hasVersion.Equal(wantVersion) {
		return binPath, err
	}

	installer := &releases.ExactVersion{
		InstallDir: dir,
		Product:    product.Terraform,
		Version:    TerraformVersion,
	}
	installer.SetLogger(slog.Stdlib(ctx, log, slog.LevelDebug))
	log.Debug(
		ctx,
		"installing terraform",
		slog.F("prev_version", hasVersion),
		slog.F("dir", dir),
		slog.F("version", TerraformVersion),
	)

	path, err := installer.Install(ctx)
	if err != nil {
		return "", xerrors.Errorf("install: %w", err)
	}

	// Sanity-check: if path != binPath then future invocations of Install
	// will fail.
	if path != binPath {
		return "", xerrors.Errorf("%s should be %s", path, binPath)
	}

	return path, nil
}
