package handlers

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

// AgentToolInfo holds version info for an AI coding agent tool.
type AgentToolInfo struct {
	Name    string `json:"name"`
	Found   bool   `json:"found"`
	Version string `json:"version,omitempty"`
	Path    string `json:"path,omitempty"`
}

// RuntimeInfo holds version info for a programming language runtime or compiler.
type RuntimeInfo struct {
	Name    string `json:"name"`
	Found   bool   `json:"found"`
	Version string `json:"version,omitempty"`
	Path    string `json:"path,omitempty"`
}

type agentToolDef struct {
	Name     string
	Binaries []string
	Args     []string
}

type runtimeDef struct {
	Name     string
	Binaries []string // try in order
	Args     []string
}

var agentToolDefs = []agentToolDef{
	{Name: "Codex", Binaries: []string{"codex"}, Args: []string{"--version"}},
	{Name: "Claude Code", Binaries: []string{"claude"}, Args: []string{"--version"}},
	{Name: "OpenCode", Binaries: []string{"opencode"}, Args: []string{"--version"}},
}

var runtimeDefs = []runtimeDef{
	// python3 first; fall back to python on Windows where python3 may not exist
	{Name: "Python", Binaries: []string{"python3", "python"}, Args: []string{"--version"}},
	{Name: "Node.js", Binaries: []string{"node"}, Args: []string{"--version"}},
	{Name: "Bun", Binaries: []string{"bun"}, Args: []string{"--version"}},
	{Name: "Go", Binaries: []string{"go"}, Args: []string{"version"}},
	// Java outputs version to stderr — runVersionOutput handles this
	{Name: "Java", Binaries: []string{"java"}, Args: []string{"-version"}},
	{Name: "Rust", Binaries: []string{"rustc"}, Args: []string{"--version"}},
	// C compilers: detect both, show whichever is present
	{Name: "GCC", Binaries: []string{"gcc"}, Args: []string{"--version"}},
	{Name: "Clang", Binaries: []string{"clang"}, Args: []string{"--version"}},
}

// extraSearchPaths returns common installation directories not always in PATH.
func extraSearchPaths() []string {
	home, _ := os.UserHomeDir()
	paths := []string{
		filepath.Join(home, "bin"),
		filepath.Join(home, ".local", "bin"),
		filepath.Join(home, ".bun", "bin"),
		filepath.Join(home, "go", "bin"),
		filepath.Join(home, ".cargo", "bin"),
		"/usr/local/go/bin",
		"/usr/local/bin",
		"/usr/bin",
	}
	if runtime.GOOS == "windows" {
		appdata := os.Getenv("APPDATA")
		paths = append(paths,
			filepath.Join(appdata, "npm"),
			filepath.Join(home, "AppData", "Local", "Programs"),
			filepath.Join(home, "AppData", "Roaming", "npm"),
		)
	}
	return paths
}

// lookupBinary searches PATH then extra directories for the binary.
func lookupBinary(name string) string {
	if p, err := exec.LookPath(name); err == nil {
		return p
	}
	for _, dir := range extraSearchPaths() {
		candidate := filepath.Join(dir, name)
		if runtime.GOOS == "windows" {
			candidate += ".exe"
		}
		if _, err := os.Stat(candidate); err == nil {
			return candidate
		}
	}
	return ""
}

// runVersionOutput runs the binary and returns the first non-empty line from stdout
// or stderr (some tools like Java output version to stderr).
func runVersionOutput(binaryPath string, args []string) string {
	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, binaryPath, args...)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	_ = cmd.Run()
	out := strings.TrimSpace(stdout.String())
	if out == "" {
		out = strings.TrimSpace(stderr.String())
	}
	if idx := strings.IndexByte(out, '\n'); idx >= 0 {
		out = strings.TrimSpace(out[:idx])
	}
	return out
}

// runVersion is kept for backward compatibility.
func runVersion(binaryPath string, args []string) string {
	return runVersionOutput(binaryPath, args)
}

// detectAgentTools detects all AI agent tools in parallel.
func detectAgentTools() []AgentToolInfo {
	results := make([]AgentToolInfo, len(agentToolDefs))
	var wg sync.WaitGroup
	for i, def := range agentToolDefs {
		wg.Add(1)
		go func(idx int, d agentToolDef) {
			defer wg.Done()
			info := AgentToolInfo{Name: d.Name}
			for _, bin := range d.Binaries {
				p := lookupBinary(bin)
				if p == "" {
					continue
				}
				info.Found = true
				info.Path = p
				info.Version = runVersionOutput(p, d.Args)
				break
			}
			results[idx] = info
		}(i, def)
	}
	wg.Wait()
	return results
}

// detectRuntimes detects all language runtimes in parallel.
func detectRuntimes() []RuntimeInfo {
	results := make([]RuntimeInfo, len(runtimeDefs))
	var wg sync.WaitGroup
	for i, def := range runtimeDefs {
		wg.Add(1)
		go func(idx int, d runtimeDef) {
			defer wg.Done()
			info := RuntimeInfo{Name: d.Name}
			for _, bin := range d.Binaries {
				p := lookupBinary(bin)
				if p == "" {
					continue
				}
				info.Found = true
				info.Path = p
				info.Version = runVersionOutput(p, d.Args)
				break
			}
			results[idx] = info
		}(i, def)
	}
	wg.Wait()
	return results
}
