package fs

import (
	"context"
	"fmt"
	"log"
	"path/filepath"

	"github.com/hashicorp/hc-install/errors"
	"github.com/hashicorp/hc-install/internal/src"
	"github.com/hashicorp/hc-install/internal/validators"
	"github.com/hashicorp/hc-install/product"
)

// AnyVersion finds an executable binary of any version
// either defined by ExactBinPath, or as part of Product.
//
// When ExactBinPath is used, the source is skipped when
// the binary is not found or accessible/executable.
//
// When Product is used, binary name is looked up within system $PATH
// and any declared ExtraPaths (which are *appended* to
// any directories in $PATH). Source is skipped if no binary
// is found or accessible/executable.
type AnyVersion struct {
	// Product represents the product (its binary name to look up),
	// conflicts with ExactBinPath
	Product *product.Product

	// ExtraPaths represents additional dir paths to be appended to
	// the default system $PATH, conflicts with ExactBinPath
	ExtraPaths []string

	// ExactBinPath represents exact path to the binary,
	// conflicts with Product and ExtraPaths
	ExactBinPath string

	logger *log.Logger
}

func (*AnyVersion) IsSourceImpl() src.InstallSrcSigil {
	return src.InstallSrcSigil{}
}

func (av *AnyVersion) Validate() error {
	if av.ExactBinPath == "" && av.Product == nil {
		return fmt.Errorf("must use either ExactBinPath or Product + ExtraPaths")
	}
	if av.ExactBinPath != "" && (av.Product != nil || len(av.ExtraPaths) > 0) {
		return fmt.Errorf("use either ExactBinPath or Product + ExtraPaths, not both")
	}
	if av.ExactBinPath != "" && !filepath.IsAbs(av.ExactBinPath) {
		return fmt.Errorf("expected ExactBinPath (%q) to be an absolute path", av.ExactBinPath)
	}
	if av.Product != nil && !validators.IsBinaryNameValid(av.Product.BinaryName()) {
		return fmt.Errorf("invalid binary name: %q", av.Product.BinaryName())
	}
	return nil
}

func (av *AnyVersion) SetLogger(logger *log.Logger) {
	av.logger = logger
}

func (av *AnyVersion) log() *log.Logger {
	if av.logger == nil {
		return discardLogger
	}
	return av.logger
}

func (av *AnyVersion) Find(ctx context.Context) (string, error) {
	if av.ExactBinPath != "" {
		err := checkExecutable(av.ExactBinPath)
		if err != nil {
			return "", errors.SkippableErr(err)
		}

		return av.ExactBinPath, nil
	}

	execPath, err := findFile(lookupDirs(av.ExtraPaths), av.Product.BinaryName(), checkExecutable)
	if err != nil {
		return "", errors.SkippableErr(err)
	}

	if !filepath.IsAbs(execPath) {
		var err error
		execPath, err = filepath.Abs(execPath)
		if err != nil {
			return "", errors.SkippableErr(err)
		}
	}
	return execPath, nil
}
