package desktop

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// defaultEnsureDependencies is the production ensureDependencies hook: it makes
// sure desktop/electron and web have node_modules installed, and that the
// Electron binary has been downloaded (its postinstall is skipped by some
// package managers, e.g. bun).
func defaultEnsureDependencies(ctx context.Context, req ensureDepsRequest) error {
	if err := ensureNodeModules(ctx, req.electronDir); err != nil {
		return fmt.Errorf("electron node_modules: %w", err)
	}
	if err := ensureElectronBinary(ctx, req.electronDir); err != nil {
		return fmt.Errorf("electron binary: %w", err)
	}
	if err := ensureNodeModules(ctx, req.webDir); err != nil {
		return fmt.Errorf("web node_modules: %w", err)
	}
	return nil
}

// ensureNodeModules runs `npm install` (or `bun install` when npm is missing)
// when node_modules is absent or npm reports an incomplete dependency tree.
func ensureNodeModules(ctx context.Context, dir string) error {
	if nodeModulesReady(ctx, dir) {
		return nil
	}
	pkgMgr, args := selectPackageManager()
	slog.Info("installing node_modules", "dir", dir, "manager", pkgMgr)
	cmd := exec.CommandContext(ctx, pkgMgr, args...)
	cmd.Dir = dir
	cmd.Stdout = os.Stderr
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return err
	}
	if !nodeModulesReady(ctx, dir) {
		return fmt.Errorf("installed node_modules is incomplete")
	}
	return nil
}

func nodeModulesReady(ctx context.Context, dir string) bool {
	if !isDirNonEmpty(filepath.Join(dir, "node_modules")) {
		return false
	}
	if _, err := exec.LookPath("npm"); err != nil {
		// Preserve the Bun-only path: bun install remains responsible for its
		// dependency tree when npm is unavailable.
		return true
	}
	cmd := exec.CommandContext(ctx, "npm", "ls", "--depth=0", "--silent")
	cmd.Dir = dir
	return cmd.Run() == nil
}

// electronMirrorFallback is the mirror used when the initial Electron binary
// download fails AND the user has not set ELECTRON_MIRROR themselves. It points
// at npmmirror.com which is reachable from networks where github.com is
// blocked; users on other networks whose direct download succeeds never hit
// this path.
const electronMirrorFallback = "https://npmmirror.com/mirrors/electron/"

// ensureElectronBinary checks for node_modules/electron/path.txt — the
// sentinel written by Electron's postinstall — and runs install.js directly
// when it is missing. This recovers from package managers that skip
// postinstall hooks (notably bun) and from interrupted installs.
//
// If the first attempt fails and ELECTRON_MIRROR is unset, it retries once
// with electronMirrorFallback so a fresh clone behind the GFW still works
// without manual configuration.
func ensureElectronBinary(ctx context.Context, electronDir string) error {
	electronPkg := filepath.Join(electronDir, "node_modules", "electron")
	pathTxt := filepath.Join(electronPkg, "path.txt")
	if fileExists(pathTxt) {
		return nil
	}
	installScript := filepath.Join(electronPkg, "install.js")
	if !fileExists(installScript) {
		return fmt.Errorf("electron install.js missing at %s", installScript)
	}
	firstErr := runElectronInstall(ctx, installScript, electronPkg, os.Environ())
	if firstErr == nil {
		return nil
	}
	if os.Getenv("ELECTRON_MIRROR") != "" {
		return firstErr
	}
	slog.Info("electron postinstall failed; retrying with npmmirror fallback", "original_error", firstErr.Error())
	env := append(os.Environ(), "ELECTRON_MIRROR="+electronMirrorFallback)
	return runElectronInstall(ctx, installScript, electronPkg, env)
}

func runElectronInstall(ctx context.Context, script, dir string, env []string) error {
	return runElectronInstallFn(ctx, script, dir, env)
}

// runElectronInstallFn is the implementation behind runElectronInstall. It is
// a package-level var so tests can substitute a stub.
var runElectronInstallFn = func(ctx context.Context, script, dir string, env []string) error {
	mirror := ""
	for _, entry := range env {
		if v, ok := strings.CutPrefix(entry, "ELECTRON_MIRROR="); ok {
			mirror = v
		}
	}
	if mirror != "" {
		slog.Info("running electron postinstall", "mirror", mirror)
	} else {
		slog.Info("running electron postinstall")
	}
	cmd := exec.CommandContext(ctx, "node", script)
	cmd.Dir = dir
	cmd.Env = env
	cmd.Stdout = os.Stderr
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

// selectPackageManager prefers npm (compatible with Electron's postinstall)
// and falls back to bun. If neither is on PATH, returns npm so its
// not-found error surfaces naturally.
func selectPackageManager() (string, []string) {
	if _, err := exec.LookPath("npm"); err == nil {
		return "npm", []string{"install"}
	}
	if _, err := exec.LookPath("bun"); err == nil {
		return "bun", []string{"install"}
	}
	return "npm", []string{"install"}
}

func isDirNonEmpty(path string) bool {
	entries, err := os.ReadDir(path)
	if err != nil {
		return false
	}
	return len(entries) > 0
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}
