package shim

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"

	"github.com/safedep/dry/log"
)

const (
	// pmgBinSuffix matches the legacy per-user shim dir (~/.pmg/bin). Retained
	// as a fallback so shims installed by older PMG versions keep working after
	// upgrade, until they are regenerated by a fresh `pmg setup install`.
	pmgBinSuffix = "/.pmg/bin"

	// pmgShimPathEnv is the env var the shim script exports before exec'ing
	// pmg. Its value is the absolute path of the shim that was invoked, which
	// lets FilterPMGFromPath strip exactly the dir the shim lives in — no
	// matter where it was placed (`~/.pmg/bin`, `/usr/local/lib/pmg/bin`, a
	// custom location, etc.).
	//
	// Internal: set by the shim script, consumed by pmg in the same process
	// tree. Setting it manually from a user shell is unsupported — it would
	// cause FilterPMGFromPath to strip the wrong directory from PATH lookup
	// and could prevent pmg from resolving the real package manager.
	pmgShimPathEnv = "PMG_SHIM_PATH"
)

var resolverMu sync.Mutex

func FilterPMGFromPath(pathEnv string) string {
	if pathEnv == "" {
		return ""
	}

	var shimDir string
	if shimPath := os.Getenv(pmgShimPathEnv); shimPath != "" {
		shimDir = filepath.Clean(filepath.Dir(shimPath))
	}

	entries := filepath.SplitList(pathEnv)
	filtered := make([]string, 0, len(entries))

	for _, entry := range entries {
		if strings.HasSuffix(entry, pmgBinSuffix) {
			continue
		}
		if filepath.Clean(entry) == filepath.Clean(SystemBinDir()) {
			continue
		}
		if shimDir != "" && filepath.Clean(entry) == shimDir {
			continue
		}
		filtered = append(filtered, entry)
	}

	return strings.Join(filtered, string(os.PathListSeparator))
}

// ResolveRealBinary finds the real binary path for a command by searching
// PATH with ~/.pmg/bin stripped out. This prevents exec.CommandContext from
// resolving to the shim script, which would cause infinite recursion.
func ResolveRealBinary(name string) (string, error) {
	resolverMu.Lock()
	defer resolverMu.Unlock()

	originalPath := os.Getenv("PATH")
	filteredPath := FilterPMGFromPath(originalPath)

	if err := os.Setenv("PATH", filteredPath); err != nil {
		return "", fmt.Errorf("failed to set filtered PATH: %w", err)
	}
	defer func() {
		if err := os.Setenv("PATH", originalPath); err != nil {
			log.Warnf("failed to restore PATH: %v", err)
		}
	}()

	resolved, err := exec.LookPath(name)
	if err != nil {
		if errors.Is(err, exec.ErrNotFound) {
			return "", &BinaryNotFoundError{Name: name}
		}
		return "", fmt.Errorf("could not find %s in PATH (excluding pmg shims): %w", name, err)
	}

	return resolved, nil
}

func FilterPMGFromEnv(env []string) []string {
	result := make([]string, 0, len(env))

	for _, entry := range env {
		if pathValue, ok := strings.CutPrefix(entry, "PATH="); ok {
			filtered := FilterPMGFromPath(pathValue)
			result = append(result, "PATH="+filtered)
			continue
		}
		// Drop PMG_SHIM_PATH so child processes don't inherit a stale marker
		// from the shim invocation that triggered this exec.
		if strings.HasPrefix(entry, pmgShimPathEnv+"=") {
			continue
		}
		result = append(result, entry)
	}

	return result
}
