package main

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

// embedEmptyPlaceholder is the name of an empty placeholder file embedded per
// platform. It lets build tags select the right archive while keeping go:embed
// happy even before fetch-python.sh has run.
const embedEmptyPlaceholder = "EMPTY_PYTHON_RUNTIME"

// embedRuntimeArchive embeds one platform's runtime archive. The actual
// archive is generated by fetch-python.sh and stored under embed/<os>/.
// The variable is declared in the build-tagged files runtime_<os>.go.
// The placeholder file lets `go build` succeed before fetch-python.sh runs.

// installDir is the fixed user-level runtime location: ~/.g4f/python-embed.
// This keeps the executable fully relocatable and shares one runtime across
// all g4f-go binaries/versions on the machine. On Android it is overridden
// to the app's own writable directory (see runtime_android.go).
func installDir() string {
	if d := androidInstallDir(); d != "" {
		return d
	}
	home, err := os.UserHomeDir()
	if err != nil {
		home = "."
	}
	return filepath.Join(home, ".g4f", "python-embed")
}

// hasEmbeddedZip reports whether a real runtime archive is baked into this build.
func hasEmbeddedZip() bool {
	entries, err := embeddedArchives()
	return err == nil && len(entries) > 0
}

// embeddedArchives lists the runtime zips embedded for the current GOOS.
func embeddedArchives() ([]string, error) {
	osDir := "embed/" + runtime.GOOS
	return fs.Glob(embedRuntimeArchive, osDir+"/*.zip")
}

// goArchToken maps runtime.GOARCH to the token used in archive names.
func goArchToken() string {
	switch runtime.GOARCH {
	case "arm64":
		return "arm64"
	case "amd64", "x86_64":
		return "x64"
	case "386", "x86":
		return "x86"
	case "arm":
		return "arm"
	}
	return runtime.GOARCH
}

// pickArchive chooses the embedded zip that matches the host architecture.
// Falls back to a generic x64/amd64 archive (e.g. FreeBSD reuses linux-x64).
func pickArchive(entries []string) string {
	tok := goArchToken()
	for _, e := range entries {
		if strings.Contains(e, tok) {
			return e
		}
	}
	for _, e := range entries {
		if strings.Contains(e, "x64") || strings.Contains(e, "amd64") {
			return e
		}
	}
	return entries[0]
}

// extractEmbedded unpacks embed/<os>/<os>-<arch>-embed-<ver>.zip into the
// user install dir using stdlib archive/zip only (no external tool needed).
func extractEmbedded(binDir string) error {
	// The embed.FS layout mirrors the repo: embed/<os>/<archive>.zip.
	entries, err := embeddedArchives()
	if err != nil || len(entries) == 0 {
		return fmt.Errorf("no embedded runtime archive for this platform (run fetch-python.sh and rebuild)")
	}
	archive := pickArchive(entries)
	src, err := embedRuntimeArchive.Open(archive)
	if err != nil {
		return fmt.Errorf("open embedded archive: %w", err)
	}
	data, err := io.ReadAll(src)
	src.Close()
	if err != nil {
		return fmt.Errorf("read embedded archive: %w", err)
	}
	if err := os.MkdirAll(binDir, 0o755); err != nil {
		return err
	}
	if err := extractZip(bytes.NewReader(data), int64(len(data)), binDir); err != nil {
		return fmt.Errorf("extract embedded runtime: %w", err)
	}
	return nil
}

// g4fIsInstalled reports whether the g4f Python package is already installed
// in the downloaded runtime by checking the .installed stamp.
func g4fIsInstalled(binDir string) bool {
	stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
	_, err := os.Stat(stamp)
	return err == nil
}

// installG4F installs the g4f Python package into the downloaded runtime and
// writes the .installed stamp. Uses the bundled pip (ensurepip wheels in
// pbs installs, bootstrapped via `python -m ensurepip`) so no network access
// is required after the runtime was downloaded.
func installG4F(binDir, exe string, start time.Time) error {
	fmt.Printf("Installing gpt4free into the Python runtime...\n")
	if err := ensurePip(binDir, exe); err != nil {
		return err
	}
	code, err := runPython(noSignalCtx(), exe, []string{
		"-m", "pip", "install",
		"--no-input", "g4f[slim]",
	}, pipEnv(binDir)...)
	if err != nil || code != 0 {
		return fmt.Errorf("pip install g4f failed (exit %d): %w", code, err)
	}
	stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
	if err := os.MkdirAll(filepath.Dir(stamp), 0o755); err != nil {
		return err
	}
	if err := os.WriteFile(stamp, []byte("g4f installed"), 0o644); err != nil {
		return err
	}
	fmt.Printf("gpt4free installed in %.1fs\n", time.Since(start).Seconds())
	return nil
}

// upgradeG4F upgrades the g4f Python package in the downloaded runtime.
// Called on subsequent runs (after the initial install) when the user passes
// a subcommand that benefits from the latest version (api, gui, dev).
func upgradeG4F(binDir, exe string, start time.Time) error {
	fmt.Printf("Upgrading gpt4free...\n")
	if err := ensurePip(binDir, exe); err != nil {
		return err
	}
	code, err := runPython(noSignalCtx(), exe, []string{
		"-m", "pip", "install",
		"--no-input", "--upgrade", "g4f[slim]",
	}, pipEnv(binDir)...)
	if err != nil || code != 0 {
		return fmt.Errorf("pip upgrade g4f failed (exit %d): %w", code, err)
	}
	fmt.Printf("gpt4free upgraded in %.1fs\n", time.Since(start).Seconds())
	return nil
}

// ensurePip makes `python -m pip` available in the downloaded runtime.
// pbs installs ship ensurepip but no standalone pip; bootstrap once.
func ensurePip(binDir, exe string) error {
	code, err := runPython(noSignalCtx(), exe, []string{"-c", "import pip"}, pipEnv(binDir)...)
	if err == nil && code == 0 {
		return nil
	}
	fmt.Println("  bootstrapping pip (ensurepip)...")
	code, err = runPython(noSignalCtx(), exe, []string{"-m", "ensurepip", "--upgrade"}, pipEnv(binDir)...)
	if err != nil || code != 0 {
		return fmt.Errorf("ensurepip failed (exit %d): %w", code, err)
	}
	return nil
}

// pipEnv restricts pip to the downloaded runtime so installs never touch the
// host Python. pbs installs are a full layout: lib/pythonX.Y/site-packages
// (unix) or Lib/site-packages (windows) inside the interpreter home.
func pipEnv(binDir string) []string {
	home := pythonHome(binDir)
	lib := filepath.Join(home, "Lib", "site-packages")
	if runtime.GOOS != "windows" {
		lib = filepath.Join(home, "lib", "python3.14", "site-packages")
	}
	return []string{
		"PYTHONHOME=" + home,
		"PYTHONNOUSERSITE=1",
		"PYTHONDONTWRITEBYTECODE=1",
		"PYTHONUTF8=1",
		"PYTHONPATH=" + lib,
	}
}
