package dashboard

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/desktop/internal/toolchain"
)

// ---------------------------------------------------------------------------
// Toolchain types
// ---------------------------------------------------------------------------

// ToolchainType identifies a toolchain.
type ToolchainType string

const (
	ToolchainBun ToolchainType = "bun"
	ToolchainGo  ToolchainType = "go"
)

// AllToolchains returns all supported toolchain types.
func AllToolchains() []ToolchainType {
	return []ToolchainType{ToolchainBun, ToolchainGo}
}

// toolchainInfo holds detected information about a toolchain.
type toolchainInfo struct {
	Type      ToolchainType `json:"type"`
	Name      string        `json:"name"`
	Binary    string        `json:"binary"`
	Found     bool          `json:"found"`
	Path      string        `json:"path,omitempty"`
	Version   string        `json:"version,omitempty"`
	Latest    string        `json:"latest,omitempty"`
	Updatable bool          `json:"updatable"`
}

type toolchainService struct {
	detect func(ToolchainType) toolchainInfo
}

func newToolchainService() toolchainService {
	return toolchainService{detect: detectToolchain}
}

func (s toolchainService) DetectAll() []toolchainInfo {
	results := make([]toolchainInfo, 0, len(AllToolchains()))
	for _, tc := range AllToolchains() {
		results = append(results, s.detect(tc))
	}
	return results
}

func (s toolchainService) DetectAllWithUpdates() []toolchainInfo {
	results := make([]toolchainInfo, 0, len(AllToolchains()))
	for _, tc := range AllToolchains() {
		info := s.detect(tc)
		results = append(results, checkToolchainUpdate(tc, info))
	}
	return results
}

func (s toolchainService) MissingRequired() []ToolchainType {
	missing := make([]ToolchainType, 0)
	for _, info := range s.DetectAll() {
		if !info.Found {
			missing = append(missing, info.Type)
		}
	}
	return missing
}

var defaultToolchainService = newToolchainService()

var goDownloadMetadataFetcher = fetchGoDownloadMetadata

// ---------------------------------------------------------------------------
// GET /toolchain/detect
// ---------------------------------------------------------------------------

// ToolchainDetectHandler detects all toolchains.
type ToolchainDetectHandler struct{}

// NewToolchainDetectHandler creates a new ToolchainDetectHandler.
func NewToolchainDetectHandler() gin.HandlerFunc {
	return (&ToolchainDetectHandler{}).Handle
}

// Handle returns detection results for all toolchains (basic info only, no network calls).
func (h *ToolchainDetectHandler) Handle(c *gin.Context) {
	c.JSON(http.StatusOK, defaultToolchainService.DetectAll())
}

// ---------------------------------------------------------------------------
// GET /toolchain/check-updates
// ---------------------------------------------------------------------------

// ToolchainCheckUpdatesHandler checks latest versions for all installed toolchains.
type ToolchainCheckUpdatesHandler struct{}

// NewToolchainCheckUpdatesHandler creates a new ToolchainCheckUpdatesHandler.
func NewToolchainCheckUpdatesHandler() gin.HandlerFunc {
	return (&ToolchainCheckUpdatesHandler{}).Handle
}

// Handle checks for updates on all installed toolchains.
func (h *ToolchainCheckUpdatesHandler) Handle(c *gin.Context) {
	c.JSON(http.StatusOK, defaultToolchainService.DetectAllWithUpdates())
}

// ---------------------------------------------------------------------------
// POST /toolchain/:name/install (SSE stream)
// ---------------------------------------------------------------------------

var toolchainInstallSlots = make(chan struct{}, 1)

// ToolchainInstallHandler installs a toolchain and streams output via SSE.
type ToolchainInstallHandler struct{}

// NewToolchainInstallHandler creates a new ToolchainInstallHandler.
func NewToolchainInstallHandler() gin.HandlerFunc {
	return (&ToolchainInstallHandler{}).Handle
}

// Handle starts a toolchain installation and streams progress as Server-Sent Events.
func (h *ToolchainInstallHandler) Handle(c *gin.Context) {
	name := c.Param("name")
	tc := ToolchainType(name)
	if tc != ToolchainBun && tc != ToolchainGo {
		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unsupported toolchain: %q", name)})
		return
	}

	// Check if already available
	info := detectToolchain(tc)
	if info.Found {
		c.JSON(http.StatusOK, gin.H{"type": string(tc), "alreadyInstalled": true})
		return
	}

	streamToolchainOperation(c, func(ctx context.Context, w io.Writer) error {
		return installToolchain(ctx, tc, w)
	})
}

// ---------------------------------------------------------------------------
// POST /toolchain/:name/upgrade (SSE stream)
// ---------------------------------------------------------------------------

// ToolchainUpgradeHandler upgrades a toolchain and streams output via SSE.
type ToolchainUpgradeHandler struct{}

// NewToolchainUpgradeHandler creates a new ToolchainUpgradeHandler.
func NewToolchainUpgradeHandler() gin.HandlerFunc {
	return (&ToolchainUpgradeHandler{}).Handle
}

// Handle starts a toolchain upgrade and streams progress as Server-Sent Events.
func (h *ToolchainUpgradeHandler) Handle(c *gin.Context) {
	name := c.Param("name")
	tc := ToolchainType(name)
	if tc != ToolchainBun && tc != ToolchainGo {
		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unsupported toolchain: %q", name)})
		return
	}

	streamToolchainOperation(c, func(ctx context.Context, w io.Writer) error {
		return upgradeToolchain(ctx, tc, w)
	})
}

func streamToolchainOperation(c *gin.Context, run func(context.Context, io.Writer) error) {
	startDashboardEventStream(c)

	pr, pw := io.Pipe()
	go func() {
		defer pw.Close()

		release, err := acquireToolchainInstallSlot(c.Request.Context())
		if err != nil {
			return
		}
		defer release()

		runErr := run(c.Request.Context(), pw)
		enc := json.NewEncoder(pw)
		if runErr != nil {
			if err := enc.Encode(map[string]string{"_type": "error", "message": runErr.Error()}); err != nil {
				_ = pw.CloseWithError(err)
			}
			return
		}
		if err := enc.Encode(map[string]any{"_type": "done", "success": true}); err != nil {
			_ = pw.CloseWithError(err)
		}
	}()

	defer pr.Close()
	streamDashboardPipe(c, pr, writeDashboardOperationStreamLine)
}

func acquireToolchainInstallSlot(ctx context.Context) (func(), error) {
	select {
	case toolchainInstallSlots <- struct{}{}:
		return func() { <-toolchainInstallSlots }, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

// ---------------------------------------------------------------------------
// Internal: detection
// ---------------------------------------------------------------------------

func detectToolchain(tc ToolchainType) toolchainInfo {
	resolver := toolchain.DefaultResolver()
	var binary, name string
	var path string
	var err error
	switch tc {
	case ToolchainBun:
		binary = "bun"
		name = "Bun"
		path, err = resolver.ResolveBun()
	case ToolchainGo:
		binary = "go"
		name = "Go"
		path, err = resolver.ResolveGo()
	default:
		return toolchainInfo{Type: tc, Name: string(tc)}
	}

	if err != nil {
		return toolchainInfo{Type: tc, Name: name, Binary: binary}
	}

	version := detectToolchainVersion(tc, path)

	return toolchainInfo{
		Type:    tc,
		Name:    name,
		Binary:  binary,
		Found:   true,
		Path:    path,
		Version: version,
	}
}

func checkToolchainUpdate(tc ToolchainType, info toolchainInfo) toolchainInfo {
	if !info.Found {
		return info
	}
	latest := detectToolchainLatest(tc)
	info.Latest = latest
	info.Updatable = latest != "" && info.Version != "" && info.Version != latest
	return info
}

func detectToolchainVersion(tc ToolchainType, path string) string {
	var args []string
	switch tc {
	case ToolchainBun:
		args = []string{path, "--version"}
	case ToolchainGo:
		args = []string{path, "version"}
	default:
		return ""
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, args[0], args[1:]...)
	out, err := cmd.Output()
	if err != nil {
		return ""
	}

	v := strings.TrimSpace(string(out))
	// Take first line only
	if idx := strings.Index(v, "\n"); idx >= 0 {
		v = v[:idx]
	}
	return v
}

func detectToolchainLatest(tc ToolchainType) string {
	return detectToolchainLatestContext(context.Background(), tc)
}

func detectToolchainLatestContext(ctx context.Context, tc ToolchainType) string {
	// Use npm/brew registry to check latest version
	switch tc {
	case ToolchainBun:
		// Try npm view to get latest version
		if _, err := exec.LookPath("npm"); err == nil {
			ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
			defer cancel()
			cmd := exec.CommandContext(ctx, "npm", "view", "bun", "version")
			out, err := cmd.Output()
			if err == nil {
				return strings.TrimSpace(string(out))
			}
		}
	case ToolchainGo:
		version, err := goDownloadMetadataFetcher(ctx)
		if err == nil {
			return version
		}
	}
	return ""
}

// ---------------------------------------------------------------------------
// Internal: install
// ---------------------------------------------------------------------------

func installToolchain(ctx context.Context, tc ToolchainType, w io.Writer) error {
	fmt.Fprintf(w, "Installing %s...\n", tc)
	switch tc {
	case ToolchainBun:
		if err := installBun(ctx, w); err != nil {
			return err
		}
		return refreshInstalledToolchainPath(tc, w)
	case ToolchainGo:
		if err := installGo(ctx, w); err != nil {
			return err
		}
		return refreshInstalledToolchainPath(tc, w)
	default:
		return fmt.Errorf("unsupported toolchain: %s", tc)
	}
}

func installBun(ctx context.Context, w io.Writer) error {
	switch runtime.GOOS {
	case "darwin", "linux":
		if hasCmd("curl") {
			fmt.Fprintln(w, "Using curl installer...")
			return runPipedToWriterWithEnv(ctx, w, []string{"curl", "-fsSL", "https://bun.sh/install"}, []string{"bash"}, bunInstallEnvironment())
		}
		if hasCmd("npm") {
			fmt.Fprintln(w, "Using npm installer...")
			return runCmdToWriter(ctx, w, "npm", "install", "-g", "bun")
		}
		return fmt.Errorf("no supported package manager found (need curl or npm)")
	case "windows":
		if hasCmd("npm") {
			fmt.Fprintln(w, "Using npm installer...")
			return runCmdToWriter(ctx, w, "npm", "install", "-g", "bun")
		}
		if hasCmd("powershell") {
			fmt.Fprintln(w, "Using PowerShell installer...")
			return runCmdToWriter(ctx, w, "powershell", "-c", "irm bun.sh/install.ps1 | iex")
		}
		return fmt.Errorf("no supported package manager found (need npm)")
	default:
		return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
	}
}

func installGo(ctx context.Context, w io.Writer) error {
	switch runtime.GOOS {
	case "darwin":
		if hasCmd("brew") {
			fmt.Fprintln(w, "Using brew installer...")
			return runCmdToWriter(ctx, w, "brew", "install", "go")
		}
		if hasCmd("curl") {
			fmt.Fprintln(w, "Using official installer...")
			return installGoFromOfficial(ctx, w)
		}
		return fmt.Errorf("no supported package manager found (need brew or curl)")
	case "linux":
		if hasCmd("curl") {
			fmt.Fprintln(w, "Using official installer...")
			return installGoFromOfficial(ctx, w)
		}
		return fmt.Errorf("curl is required. Install curl first.")
	case "windows":
		if hasCmd("winget") {
			fmt.Fprintln(w, "Using winget installer...")
			if err := runCmdToWriter(ctx, w, "winget", "install", "--id", "GoLang.Go", "--accept-source-agreements", "--accept-package-agreements"); err != nil {
				return explainWindowsGoInstallFailure("winget", err)
			}
			return nil
		}
		if hasCmd("scoop") {
			fmt.Fprintln(w, "Using scoop installer...")
			if err := runCmdToWriter(ctx, w, "scoop", "install", "go"); err != nil {
				return explainWindowsGoInstallFailure("scoop", err)
			}
			return nil
		}
		return fmt.Errorf("no supported package manager found (need winget or scoop)")
	default:
		return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
	}
}

func installGoFromOfficial(ctx context.Context, w io.Writer) error {
	latest := detectToolchainLatestContext(ctx, ToolchainGo)
	dlURL := goDownloadURL(latest, runtime.GOOS, runtime.GOARCH)
	fmt.Fprintf(w, "Downloading Go from %s ...\n", dlURL)

	if hasCmd("curl") && hasCmd("tar") {
		installDir, binaryPath, err := goInstallLayout()
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "Extracting to %s ...\n", installDir)
		if err := os.MkdirAll(installDir, 0o755); err != nil {
			return fmt.Errorf("prepare Go install directory %s: %w", installDir, err)
		}
		extractDir, err := os.MkdirTemp(installDir, ".go-extract-*")
		if err != nil {
			return fmt.Errorf("prepare temporary Go install directory under %s: %w", installDir, err)
		}
		defer os.RemoveAll(extractDir)

		if err := runPipedToWriter(
			ctx,
			w,
			[]string{"curl", "-fsSL", dlURL},
			[]string{"tar", "-C", extractDir, "-xzf", "-"},
		); err != nil {
			return fmt.Errorf("failed to download/extract Go: %w", err)
		}
		if err := replaceManagedGo(filepath.Join(extractDir, "go"), filepath.Join(installDir, "go")); err != nil {
			return err
		}

		fmt.Fprintf(w, "Go installed successfully at %s.\n", binaryPath)
		return nil
	}

	return fmt.Errorf("curl and tar are required for Go installation")
}

// ---------------------------------------------------------------------------
// Internal: upgrade
// ---------------------------------------------------------------------------

func upgradeToolchain(ctx context.Context, tc ToolchainType, w io.Writer) error {
	fmt.Fprintf(w, "Upgrading %s...\n", tc)
	switch tc {
	case ToolchainBun:
		if err := upgradeBun(ctx, w); err != nil {
			return err
		}
		return refreshInstalledToolchainPath(tc, w)
	case ToolchainGo:
		if err := upgradeGo(ctx, w); err != nil {
			return err
		}
		return refreshInstalledToolchainPath(tc, w)
	default:
		return fmt.Errorf("unsupported toolchain: %s", tc)
	}
}

func upgradeBun(ctx context.Context, w io.Writer) error {
	switch runtime.GOOS {
	case "darwin", "linux":
		if hasCmd("bun") {
			fmt.Fprintln(w, "Using bun upgrade...")
			return runCmdToWriter(ctx, w, "bun", "upgrade")
		}
		if hasCmd("npm") {
			fmt.Fprintln(w, "Using npm to update bun...")
			return runCmdToWriter(ctx, w, "npm", "install", "-g", "bun@latest")
		}
		if hasCmd("curl") {
			fmt.Fprintln(w, "Reinstalling via curl...")
			return runPipedToWriterWithEnv(ctx, w, []string{"curl", "-fsSL", "https://bun.sh/install"}, []string{"bash"}, bunInstallEnvironment())
		}
		return fmt.Errorf("no supported method found (need bun, npm, or curl)")
	case "windows":
		if hasCmd("bun") {
			fmt.Fprintln(w, "Using bun upgrade...")
			return runCmdToWriter(ctx, w, "bun", "upgrade")
		}
		if hasCmd("npm") {
			fmt.Fprintln(w, "Using npm to update bun...")
			return runCmdToWriter(ctx, w, "npm", "install", "-g", "bun@latest")
		}
		return fmt.Errorf("no supported method found (need bun or npm)")
	default:
		return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
	}
}

func upgradeGo(ctx context.Context, w io.Writer) error {
	switch runtime.GOOS {
	case "darwin":
		if hasCmd("brew") {
			fmt.Fprintln(w, "Using brew upgrade...")
			return runCmdToWriter(ctx, w, "brew", "upgrade", "go")
		}
		return upgradeGoFromOfficial(ctx, w)
	case "linux":
		return upgradeGoFromOfficial(ctx, w)
	case "windows":
		if hasCmd("winget") {
			fmt.Fprintln(w, "Using winget upgrade...")
			if err := runCmdToWriter(ctx, w, "winget", "upgrade", "--id", "GoLang.Go", "--accept-source-agreements", "--accept-package-agreements"); err != nil {
				return explainWindowsGoInstallFailure("winget", err)
			}
			return nil
		}
		if hasCmd("scoop") {
			fmt.Fprintln(w, "Using scoop update...")
			if err := runCmdToWriter(ctx, w, "scoop", "update", "go"); err != nil {
				return explainWindowsGoInstallFailure("scoop", err)
			}
			return nil
		}
		return fmt.Errorf("no supported method found (need winget or scoop)")
	default:
		return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
	}
}

func upgradeGoFromOfficial(ctx context.Context, w io.Writer) error {
	// Re-download and extract the latest version
	fmt.Fprintln(w, "Upgrading Go from official source...")
	return installGoFromOfficial(ctx, w)
}

func refreshInstalledToolchainPath(tc ToolchainType, w io.Writer) error {
	info := detectToolchain(tc)
	if !info.Found {
		return fmt.Errorf("%s installation completed, but %s was not found in the desktop client environment", tc, tc)
	}
	if err := toolchain.DefaultResolver().PrependBinaryDirToPATH(info.Path); err != nil {
		return fmt.Errorf("refresh %s PATH: %w", tc, err)
	}
	fmt.Fprintf(w, "%s available at %s\n", info.Name, info.Path)
	return nil
}

func bunInstallEnvironment() []string {
	return toolchain.DefaultResolver().WithBunInstallEnv(os.Environ())
}

func fetchGoDownloadMetadata(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://go.dev/dl/?mode=json", nil)
	if err != nil {
		return "", fmt.Errorf("create Go downloads request: %w", err)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("request Go downloads metadata: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("request Go downloads metadata: unexpected status %s", resp.Status)
	}

	var releases []struct {
		Version string `json:"version"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
		return "", fmt.Errorf("decode Go downloads metadata: %w", err)
	}
	if len(releases) == 0 || strings.TrimSpace(releases[0].Version) == "" {
		return "", fmt.Errorf("decode Go downloads metadata: empty release list")
	}
	return releases[0].Version, nil
}

func goDownloadURL(version string, goos string, goarch string) string {
	version = normalizeGoVersion(version)
	if version == "" {
		return fmt.Sprintf("https://go.dev/dl/latest.%s-%s.tar.gz", goos, goarch)
	}
	return fmt.Sprintf("https://go.dev/dl/%s.%s-%s.tar.gz", version, goos, goarch)
}

func normalizeGoVersion(version string) string {
	version = strings.TrimSpace(version)
	if version == "" {
		return ""
	}
	if strings.HasPrefix(version, "go") {
		return version
	}
	return "go" + version
}

func goInstallLayout() (string, string, error) {
	resolver := toolchain.DefaultResolver()
	installDir, ok := resolver.GoInstallDir()
	if !ok {
		return "", "", fmt.Errorf("determine Go install directory for %s: home directory is unavailable", runtime.GOOS)
	}

	goBinary := "go"
	if runtime.GOOS == "windows" {
		goBinary = "go.exe"
	}
	binaryPath := filepath.Join(installDir, "go", "bin", goBinary)
	return installDir, binaryPath, nil
}

func replaceManagedGo(extractedGoDir, finalGoDir string) error {
	if info, err := os.Stat(extractedGoDir); err != nil {
		return fmt.Errorf("verify extracted Go directory %s: %w", extractedGoDir, err)
	} else if !info.IsDir() {
		return fmt.Errorf("verify extracted Go directory %s: not a directory", extractedGoDir)
	}
	if err := validateManagedGoBinary(extractedGoDir); err != nil {
		return err
	}

	backupDir, err := reserveGoBackupDir(filepath.Dir(finalGoDir))
	if err != nil {
		return err
	}

	hasExisting := false
	if err := os.Rename(finalGoDir, backupDir); err != nil {
		if !os.IsNotExist(err) {
			return fmt.Errorf("backup previous Go installation %s: %w", finalGoDir, err)
		}
		if err := os.RemoveAll(backupDir); err != nil {
			return fmt.Errorf("remove unused Go backup directory %s: %w", backupDir, err)
		}
	} else {
		hasExisting = true
	}

	if err := os.Rename(extractedGoDir, finalGoDir); err != nil {
		if hasExisting {
			if restoreErr := os.Rename(backupDir, finalGoDir); restoreErr != nil {
				return fmt.Errorf("activate Go installation %s: %w; restore previous Go installation: %v", finalGoDir, err, restoreErr)
			}
		}
		return fmt.Errorf("activate Go installation %s: %w", finalGoDir, err)
	}

	if hasExisting {
		if err := os.RemoveAll(backupDir); err != nil {
			return fmt.Errorf("remove previous Go installation backup %s: %w", backupDir, err)
		}
	}
	return nil
}

func validateManagedGoBinary(goDir string) error {
	goName := "go"
	if runtime.GOOS == "windows" {
		goName = "go.exe"
	}
	binaryPath := filepath.Join(goDir, "bin", goName)
	info, err := os.Stat(binaryPath)
	if err != nil {
		return fmt.Errorf("verify extracted Go binary %s: %w", binaryPath, err)
	}
	if info.IsDir() {
		return fmt.Errorf("verify extracted Go binary %s: is a directory", binaryPath)
	}
	if runtime.GOOS != "windows" && info.Mode()&0o111 == 0 {
		return fmt.Errorf("verify extracted Go binary %s: not executable", binaryPath)
	}
	return nil
}

func reserveGoBackupDir(parent string) (string, error) {
	backupDir, err := os.MkdirTemp(parent, ".go-backup-*")
	if err != nil {
		return "", fmt.Errorf("reserve Go backup directory under %s: %w", parent, err)
	}
	if err := os.RemoveAll(backupDir); err != nil {
		return "", fmt.Errorf("clear reserved Go backup directory %s: %w", backupDir, err)
	}
	return backupDir, nil
}

func explainWindowsGoInstallFailure(installer string, err error) error {
	message := classifyWindowsGoInstallFailure(installer, err)
	if message == "" {
		return fmt.Errorf("%s failed to install Go. Review the installer output above for details. Original error: %w", installer, err)
	}
	return fmt.Errorf("%s Original error: %w", message, err)
}

func classifyWindowsGoInstallFailure(installer string, err error) string {
	lower := strings.ToLower(err.Error())
	switch {
	case strings.Contains(lower, "0x80072ee2"), strings.Contains(lower, "timeout"):
		return fmt.Sprintf("%s failed to install Go because the download or package source timed out. This is usually a network, proxy, VPN, or source-availability issue on Windows. Check network access to the package source and try again.", installer)
	case strings.Contains(lower, "0x80072f19"), strings.Contains(lower, "certificate"), strings.Contains(lower, "tls"):
		return fmt.Sprintf("%s failed to install Go because Windows could not establish a trusted HTTPS connection to the package source. This is usually caused by TLS interception, certificate trust issues, or restricted enterprise network settings. Check system certificate trust, proxy/VPN configuration, and try again.", installer)
	default:
		return ""
	}
}

// ---------------------------------------------------------------------------
// Internal: command helpers
// ---------------------------------------------------------------------------

func hasCmd(name string) bool {
	_, err := exec.LookPath(name)
	return err == nil
}

func runCmdToWriter(ctx context.Context, w io.Writer, args ...string) error {
	if len(args) == 0 {
		return fmt.Errorf("no command")
	}
	fmt.Fprintf(w, "Running: %s\n", strings.Join(args, " "))
	cmd := exec.CommandContext(ctx, args[0], args[1:]...)
	configureToolchainCommand(cmd)
	cmd.Stdout = w
	cmd.Stderr = w
	return cmd.Run()
}

func runPipedToWriter(ctx context.Context, w io.Writer, cmd1Args, cmd2Args []string) error {
	return runPipedToWriterWithEnv(ctx, w, cmd1Args, cmd2Args, nil)
}

func runPipedToWriterWithEnv(ctx context.Context, w io.Writer, cmd1Args, cmd2Args []string, cmd2Env []string) error {
	if len(cmd1Args) == 0 || len(cmd2Args) == 0 {
		return fmt.Errorf("invalid pipe command")
	}

	fmt.Fprintf(w, "Running: %s | %s\n", strings.Join(cmd1Args, " "), strings.Join(cmd2Args, " "))

	cmd1 := exec.CommandContext(ctx, cmd1Args[0], cmd1Args[1:]...)
	cmd2 := exec.CommandContext(ctx, cmd2Args[0], cmd2Args[1:]...)
	configureToolchainCommand(cmd1)
	configureToolchainCommand(cmd2)
	cmd1.WaitDelay = 500 * time.Millisecond
	cmd2.WaitDelay = 500 * time.Millisecond
	if len(cmd2Env) > 0 {
		cmd2.Env = cmd2Env
	}

	stdout, err := cmd1.StdoutPipe()
	if err != nil {
		return fmt.Errorf("connect %s stdout: %w", strings.Join(cmd1Args, " "), err)
	}
	cmd2.Stdin = stdout
	cmd2.Stdout = w
	cmd2.Stderr = w

	if err := cmd1.Start(); err != nil {
		return fmt.Errorf("failed to start %s: %w", strings.Join(cmd1Args, " "), err)
	}

	if err := cmd2.Start(); err != nil {
		killToolchainCommand(cmd1)
		_ = cmd1.Wait()
		return fmt.Errorf("failed to start %s: %w", strings.Join(cmd2Args, " "), err)
	}

	if err := cmd1.Wait(); err != nil {
		killToolchainCommand(cmd2)
		_ = cmd2.Wait()
		return fmt.Errorf("%s failed: %w", strings.Join(cmd1Args, " "), err)
	}

	if err := cmd2.Wait(); err != nil {
		return fmt.Errorf("%s failed: %w", strings.Join(cmd2Args, " "), err)
	}

	return nil
}
