package core

import (
	"archive/zip"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

func findGo() string {
	if p, err := exec.LookPath("go"); err == nil {
		return p
	}
	if goroot := os.Getenv("GOROOT"); goroot != "" {
		candidate := filepath.Join(goroot, "bin", "go")
		if fileExists(candidate) {
			return candidate
		}
	}

	goexe := "go"
	if runtime.GOOS == "windows" {
		goexe = "go.exe"
	}

	for _, p := range []string{
		"/usr/local/go/bin/go",
		filepath.Join(os.Getenv("HOME"), ".local", "go", "bin", goexe),
		"/tmp/go/bin/" + goexe,
		"/usr/lib/go/bin/go",
		`C:\Go\bin\go.exe`,
		filepath.Join(os.Getenv("ProgramFiles"), "Go", "bin", "go.exe"),
		filepath.Join(os.Getenv("LocalAppData"), "go", "bin", goexe),
		filepath.Join(os.Getenv("HOME"), "sdk", "go", "bin", goexe),
		filepath.Join(os.TempDir(), "go", "bin", goexe),
	} {
		if fileExists(p) {
			return p
		}
	}
	return goexe
}

type goFile struct {
	Filename string `json:"filename"`
	OS       string `json:"os"`
	Arch     string `json:"arch"`
	SHA256   string `json:"sha256"`
}

type goVersion struct {
	Version string  `json:"version"`
	Stable  bool    `json:"stable"`
	Files   []goFile `json:"files"`
}

func ensureGo() (string, error) {
	if p, err := exec.LookPath("go"); err == nil {
		return p, nil
	}
	if goroot := os.Getenv("GOROOT"); goroot != "" {
		candidate := filepath.Join(goroot, "bin", "go")
		if fileExists(candidate) {
			return candidate, nil
		}
	}

	if p := findGo(); fileExists(p) {
		return p, nil
	}

	log.Printf("[*] Go not found, installing...")

	osName := runtime.GOOS
	archName := runtime.GOARCH
	switch archName {
	case "x86_64":
		archName = "amd64"
	case "aarch64":
		archName = "arm64"
	}

	ext := ".tar.gz"
	if osName == "windows" {
		ext = ".zip"
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/VERSION?m=text", nil)
	if err != nil {
		return "", fmt.Errorf("create version request: %w", err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("check go version: %w", err)
	}
	defer resp.Body.Close()
	versionBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("read go version: %w", err)
	}
	version := strings.TrimSpace(strings.Split(string(versionBytes), "\n")[0])

	filename := fmt.Sprintf("%s.%s-%s%s", version, osName, archName, ext)
	url := fmt.Sprintf("https://go.dev/dl/%s", filename)

	// Fetch checksums from go.dev JSON API
	checksums, err := fetchGoChecksums()
	if err != nil {
		return "", fmt.Errorf("fetch go checksums: %w", err)
	}
	expectedHash, ok := checksums[filename]
	if !ok {
		return "", fmt.Errorf("no checksum found for %s", filename)
	}

	log.Printf("[*] downloading %s ...", url)

	tarball, err := os.CreateTemp("", "go-*"+ext)
	if err != nil {
		return "", fmt.Errorf("create temp file: %w", err)
	}
	defer os.Remove(tarball.Name())

	dlCtx, dlCancel := context.WithTimeout(context.Background(), 120*time.Second)
	defer dlCancel()
	dlReq, err := http.NewRequestWithContext(dlCtx, "GET", url, nil)
	if err != nil {
		return "", fmt.Errorf("create download request: %w", err)
	}
	dlResp, err := http.DefaultClient.Do(dlReq)
	if err != nil {
		return "", fmt.Errorf("download go: %w", err)
	}
	defer dlResp.Body.Close()

	hash := sha256.New()
	if _, err := io.Copy(io.MultiWriter(tarball, hash), dlResp.Body); err != nil {
		return "", fmt.Errorf("write go archive: %w", err)
	}
	tarball.Close()

	got := hex.EncodeToString(hash.Sum(nil))
	if got != expectedHash {
		return "", fmt.Errorf("checksum mismatch for %s:\n  expected: %s\n  got:      %s", filename, expectedHash, got)
	}
	log.Printf("[*] SHA256 verified: %s", expectedHash)

	installDir := installGo(tarball.Name(), osName)

	log.Printf("[+] Go %s installed at %s", version, installDir)
	return filepath.Join(installDir, "bin", "go"), nil
}

func fetchGoChecksums() (map[string]string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/dl/?mode=json&include=all", nil)
	if err != nil {
		return nil, fmt.Errorf("create metadata request: %w", err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("get go metadata: %w", err)
	}
	defer resp.Body.Close()

	var versions []goVersion
	if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
		return nil, fmt.Errorf("decode go metadata: %w", err)
	}

	checksums := make(map[string]string)
	for _, v := range versions {
		for _, f := range v.Files {
			checksums[f.Filename] = f.SHA256
		}
	}
	return checksums, nil
}

func installGo(archive, osName string) string {
	homeDir, _ := os.UserHomeDir()

	candidates := []string{
		"/usr/local/go",
		filepath.Join(homeDir, ".local", "go"),
		"/tmp/go",
	}
	if osName == "windows" {
		progFiles := os.Getenv("ProgramFiles")
		if progFiles == "" {
			progFiles = `C:\Program Files`
		}
		candidates = []string{
			filepath.Join(progFiles, "Go"),
			filepath.Join(homeDir, "sdk", "go"),
			filepath.Join(os.TempDir(), "go"),
		}
	}

	for _, dst := range candidates {
		parent := filepath.Dir(dst)
		os.RemoveAll(dst)
		os.MkdirAll(parent, 0755)
		if extractGoArchive(archive, parent, osName) == nil {
			if _, err := os.Stat(dst); err == nil {
				log.Printf("[*] Go extracted to %s", dst)
				return dst
			}
		}
	}

	// Last resort: always try the root of the temp dir
	tmpRoot := "/tmp"
	if osName == "windows" {
		tmpRoot = os.TempDir()
	}
	os.RemoveAll(filepath.Join(tmpRoot, "go"))
	extractGoArchive(archive, tmpRoot, osName)
	return filepath.Join(tmpRoot, "go")
}

func extractGoArchive(archive, dst, osName string) error {
	if osName == "windows" {
		return extractZip(archive, dst)
	}
	return exec.Command("tar", "-C", dst, "-xzf", archive).Run()
}

func extractZip(src, dst string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return fmt.Errorf("open zip: %w", err)
	}
	defer r.Close()

	for _, f := range r.File {
		target := filepath.Join(dst, f.Name)

		// ZipSlip protection
		if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(dst)+string(os.PathSeparator)) {
			continue
		}

		if f.FileInfo().IsDir() {
			os.MkdirAll(target, 0755)
			continue
		}

		os.MkdirAll(filepath.Dir(target), 0755)

		rc, err := f.Open()
		if err != nil {
			return fmt.Errorf("open %s in zip: %w", f.Name, err)
		}

		out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
		if err != nil {
			rc.Close()
			return fmt.Errorf("create %s: %w", target, err)
		}

		_, err = io.Copy(out, rc)
		rc.Close()
		out.Close()
		if err != nil {
			return fmt.Errorf("extract %s: %w", f.Name, err)
		}
	}
	return nil
}

func findGit(dir string) string {
	gitExe := "git"
	if runtime.GOOS == "windows" {
		gitExe = "git.exe"
	}
	var found string
	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		if !d.IsDir() && strings.EqualFold(d.Name(), gitExe) {
			found = path
			return io.EOF
		}
		return nil
	})
	return found
}

// auto-installs MinGit on Windows, tries system package manager on Linux/macOS.
// Self-contained implant generation is the feature — defer per-platform packaging.
func installGit() (string, error) {
	homeDir, _ := os.UserHomeDir()
	installDir := filepath.Join(homeDir, ".necropolis", "mingit")

	if p := findGit(installDir); p != "" {
		return p, nil
	}

	switch runtime.GOOS {
	case "windows":
		log.Printf("[*] downloading MinGit...")
		url := "https://github.com/git-for-windows/git/releases/download/v2.47.0.windows.1/MinGit-2.47.0-64-bit.zip"
		archive, err := os.CreateTemp("", "mingit-*.zip")
		if err != nil {
			return "", fmt.Errorf("create temp: %w", err)
		}
		defer os.Remove(archive.Name())

		if err := downloadFile(url, archive); err != nil {
			return "", fmt.Errorf("download mingit: %w", err)
		}
		archive.Close()

		os.RemoveAll(installDir)
		os.MkdirAll(installDir, 0755)
		if err := extractZip(archive.Name(), installDir); err != nil {
			return "", fmt.Errorf("extract mingit: %w", err)
		}

		if p := findGit(installDir); p != "" {
			log.Printf("[+] MinGit installed at %s", p)
			return p, nil
		}
		return "", fmt.Errorf("MinGit extracted but git executable not found under %s", installDir)

	case "linux":
		for _, pm := range [][]string{
			{"apk", "add", "git"},
			{"apt-get", "install", "-y", "git"},
			{"yum", "install", "-y", "git"},
		} {
			if _, err := exec.LookPath(pm[0]); err != nil {
				continue
			}
			cmd := exec.Command(pm[0], pm[1:]...)
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			if cmd.Run() == nil {
				if p, err := exec.LookPath("git"); err == nil {
					return p, nil
				}
			}
		}
		return "", fmt.Errorf("could not install git via package manager; install git manually")

	case "darwin":
		for _, pm := range [][]string{
			{"xcode-select", "--install"},
			{"brew", "install", "git"},
		} {
			if _, err := exec.LookPath(pm[0]); err != nil {
				continue
			}
			cmd := exec.Command(pm[0], pm[1:]...)
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			if cmd.Run() == nil {
				if p, err := exec.LookPath("git"); err == nil {
					return p, nil
				}
			}
		}
		return "", fmt.Errorf("could not install git; install git manually")

	default:
		return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
	}
}

func downloadFile(url string, out *os.File) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("bad status: %s", resp.Status)
	}
	_, err = io.Copy(out, resp.Body)
	return err
}
