package toolchain

import (
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

// Resolver resolves tool binaries from the current desktop client environment plus
// well-known per-user install locations used by AgentTeam-managed installers.
type Resolver struct {
	LookPath          func(string) (string, error)
	Stat              func(string) (os.FileInfo, error)
	UserHomeDir       func() (string, error)
	Getenv            func(string) string
	Setenv            func(string, string) error
	GOOS              string
	PathListSeparator string
}

// DefaultResolver returns a Resolver backed by the current process environment.
func DefaultResolver() Resolver {
	return Resolver{
		LookPath:          exec.LookPath,
		Stat:              os.Stat,
		UserHomeDir:       os.UserHomeDir,
		Getenv:            os.Getenv,
		Setenv:            os.Setenv,
		GOOS:              runtime.GOOS,
		PathListSeparator: string(os.PathListSeparator),
	}
}

// ResolveBun resolves the Bun binary from PATH or AgentTeam-known install locations.
func (r Resolver) ResolveBun() (string, error) {
	r = r.withDefaults()
	return r.ResolveBinary("bun", r.BunCandidatePaths())
}

// ResolveNPM resolves the npm binary from PATH or AgentTeam-known install locations.
func (r Resolver) ResolveNPM() (string, error) {
	r = r.withDefaults()
	return r.ResolveBinary("npm", r.NPMCandidatePaths())
}

// ResolveGo resolves the Go binary from PATH or AgentTeam-known install locations.
func (r Resolver) ResolveGo() (string, error) {
	r = r.withDefaults()
	if path, ok := r.managedGoPath(); ok {
		if r.binaryExists(path) {
			return path, nil
		}
	}
	return r.ResolveBinary("go", r.GoCandidatePaths())
}

// ResolveBinary resolves binary by first consulting PATH and then explicit candidates.
func (r Resolver) ResolveBinary(binary string, candidates []string) (string, error) {
	r = r.withDefaults()
	if path, err := r.LookPath(binary); err == nil {
		return path, nil
	}
	for _, candidate := range candidates {
		if r.binaryExists(candidate) {
			return candidate, nil
		}
	}
	return "", exec.ErrNotFound
}

// BunCandidatePaths returns explicit Bun binary locations that may not be in a
// running daemon's PATH after a just-completed installer run.
func (r Resolver) BunCandidatePaths() []string {
	r = r.withDefaults()
	var candidates []string
	bunName := "bun"
	if r.GOOS == "windows" {
		bunName = "bun.exe"
	}
	if installDir, ok := r.BunInstallDir(); ok {
		candidates = append(candidates, filepath.Join(installDir, "bin", bunName))
	}
	if homeDir, err := r.UserHomeDir(); err == nil && strings.TrimSpace(homeDir) != "" {
		candidates = append(candidates, filepath.Join(homeDir, ".bun", "bin", bunName))
		if r.GOOS == "windows" {
			candidates = append(candidates, filepath.Join(homeDir, "scoop", "apps", "bun", "current", bunName))
		}
	}
	return dedupe(candidates)
}

// NPMCandidatePaths returns explicit npm binary locations used on Windows.
func (r Resolver) NPMCandidatePaths() []string {
	r = r.withDefaults()
	if r.GOOS != "windows" {
		return nil
	}
	return dedupe([]string{
		filepath.Join(r.Getenv("ProgramFiles"), "nodejs", "npm.cmd"),
		filepath.Join(r.Getenv("ProgramFiles(x86)"), "nodejs", "npm.cmd"),
		filepath.Join(r.Getenv("LocalAppData"), "Programs", "nodejs", "npm.cmd"),
		filepath.Join(r.Getenv("LocalAppData"), "Microsoft", "WindowsApps", "npm.cmd"),
	})
}

// GoCandidatePaths returns explicit Go binary locations used on Windows.
func (r Resolver) GoCandidatePaths() []string {
	r = r.withDefaults()
	goName := "go"
	if r.GOOS == "windows" {
		goName = "go.exe"
	}
	candidates := make([]string, 0, 4)
	if path, ok := r.managedGoPath(); ok {
		candidates = append(candidates, path)
	}
	if r.GOOS != "windows" {
		candidates = append(candidates, filepath.Join("/usr/local", "go", "bin", goName))
	}
	if r.GOOS == "windows" {
		candidates = append(candidates,
			filepath.Join(r.Getenv("ProgramFiles"), "Go", "bin", "go.exe"),
			filepath.Join(r.Getenv("LocalAppData"), "Programs", "Go", "bin", "go.exe"),
			filepath.Join(r.Getenv("USERPROFILE"), "scoop", "apps", "go", "current", "bin", "go.exe"),
		)
	}
	return dedupe(candidates)
}

func (r Resolver) managedGoPath() (string, bool) {
	r = r.withDefaults()
	installDir, ok := r.GoInstallDir()
	if !ok {
		return "", false
	}
	goName := "go"
	if r.GOOS == "windows" {
		goName = "go.exe"
	}
	return filepath.Join(installDir, "go", "bin", goName), true
}

func (r Resolver) binaryExists(path string) bool {
	path = strings.TrimSpace(path)
	if path == "" {
		return false
	}
	info, err := r.Stat(path)
	return err == nil && !info.IsDir()
}

// BunInstallDir returns the Bun install root AgentTeam should use for installer runs.
func (r Resolver) BunInstallDir() (string, bool) {
	r = r.withDefaults()
	if installDir := strings.TrimSpace(r.Getenv("BUN_INSTALL")); installDir != "" {
		return installDir, true
	}
	homeDir, err := r.UserHomeDir()
	if err != nil || strings.TrimSpace(homeDir) == "" {
		return "", false
	}
	return filepath.Join(homeDir, ".bun"), true
}

// GoInstallDir returns the user-local parent directory AgentTeam should use for
// official Go tarball installs on POSIX platforms.
func (r Resolver) GoInstallDir() (string, bool) {
	r = r.withDefaults()
	if r.GOOS == "windows" {
		return "", false
	}
	homeDir, err := r.UserHomeDir()
	if err != nil || strings.TrimSpace(homeDir) == "" {
		return "", false
	}
	switch r.GOOS {
	case "darwin":
		return filepath.Join(homeDir, "Library", "Application Support", "agentteam", "toolchains"), true
	case "linux":
		return filepath.Join(homeDir, ".local", "share", "agentteam", "toolchains"), true
	default:
		return "", false
	}
}

// WithBunInstallEnv returns environ with BUN_INSTALL set when AgentTeam can resolve
// an install root. Existing BUN_INSTALL values are preserved.
func (r Resolver) WithBunInstallEnv(environ []string) []string {
	r = r.withDefaults()
	installDir, ok := r.BunInstallDir()
	if !ok {
		return environ
	}
	return upsertEnv(environ, "BUN_INSTALL", installDir)
}

// PrependBinaryDirToPATH prepends path's directory to the current process PATH.
func (r Resolver) PrependBinaryDirToPATH(path string) error {
	r = r.withDefaults()
	dir := strings.TrimSpace(filepath.Dir(path))
	if dir == "" || dir == "." {
		return nil
	}
	current := r.Getenv("PATH")
	separator := r.PathListSeparator
	if separator == "" {
		separator = string(os.PathListSeparator)
	}
	for _, entry := range splitPathList(current, separator) {
		if samePath(entry, dir, r.GOOS) {
			return nil
		}
	}
	if current == "" {
		return r.Setenv("PATH", dir)
	}
	return r.Setenv("PATH", dir+separator+current)
}

func (r Resolver) withDefaults() Resolver {
	defaults := DefaultResolver()
	if r.LookPath == nil {
		r.LookPath = defaults.LookPath
	}
	if r.Stat == nil {
		r.Stat = defaults.Stat
	}
	if r.UserHomeDir == nil {
		r.UserHomeDir = defaults.UserHomeDir
	}
	if r.Getenv == nil {
		r.Getenv = defaults.Getenv
	}
	if r.Setenv == nil {
		r.Setenv = defaults.Setenv
	}
	if r.GOOS == "" {
		r.GOOS = defaults.GOOS
	}
	if r.PathListSeparator == "" {
		r.PathListSeparator = defaults.PathListSeparator
	}
	return r
}

func dedupe(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		key := filepath.Clean(value)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, value)
	}
	return out
}

func upsertEnv(environ []string, key string, value string) []string {
	prefix := key + "="
	updated := append([]string{}, environ...)
	for i, entry := range updated {
		if strings.HasPrefix(entry, prefix) {
			updated[i] = prefix + value
			return updated
		}
	}
	return append(updated, prefix+value)
}

func splitPathList(value string, separator string) []string {
	if value == "" {
		return nil
	}
	if separator == "" || separator == string(os.PathListSeparator) {
		return filepath.SplitList(value)
	}
	return strings.Split(value, separator)
}

func samePath(a, b string, goos string) bool {
	a = filepath.Clean(strings.TrimSpace(a))
	b = filepath.Clean(strings.TrimSpace(b))
	if goos == "windows" {
		return strings.EqualFold(a, b)
	}
	return a == b
}
