package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

type manifestEntry struct {
	sourcePath   string
	relativePath string
}

func main() {
	var buildScript string
	var bunDarwinAarch64 string
	var bunDarwinX64 string
	var bunLinuxAarch64 string
	var bunLinuxX64 string
	var bunWindowsX64 string
	var manifestPath string
	var nodeModulesRoot string
	var outDir string
	var packageDir string
	var sharedNodeModulesRoot string
	var workspaceDir string

	flag.StringVar(&buildScript, "build-script", "build", "")
	flag.StringVar(&bunDarwinAarch64, "bun-darwin-aarch64", "", "")
	flag.StringVar(&bunDarwinX64, "bun-darwin-x64", "", "")
	flag.StringVar(&bunLinuxAarch64, "bun-linux-aarch64", "", "")
	flag.StringVar(&bunLinuxX64, "bun-linux-x64", "", "")
	flag.StringVar(&bunWindowsX64, "bun-windows-x64", "", "")
	flag.StringVar(&manifestPath, "manifest", "", "")
	flag.StringVar(&nodeModulesRoot, "node-modules-root", "", "")
	flag.StringVar(&outDir, "out", "", "")
	flag.StringVar(&packageDir, "package-dir", ".", "")
	flag.StringVar(&sharedNodeModulesRoot, "shared-node-modules-root", "", "")
	flag.StringVar(&workspaceDir, "workspace-dir", "", "")
	flag.Parse()

	require(manifestPath != "", "missing --manifest")
	require(nodeModulesRoot != "", "missing --node-modules-root")
	require(outDir != "", "missing --out")
	require(packageDir != "", "missing --package-dir")
	require(sharedNodeModulesRoot != "", "missing --shared-node-modules-root")

	bunPath, err := resolveBunBinary(bunDarwinAarch64, bunDarwinX64, bunLinuxAarch64, bunLinuxX64, bunWindowsX64)
	must(err)
	must(makeAbsolute(&bunPath))
	must(makeAbsolute(&manifestPath))
	must(makeAbsolute(&nodeModulesRoot))
	must(makeAbsolute(&outDir))
	must(makeAbsolute(&sharedNodeModulesRoot))

	tempRoot, err := os.MkdirTemp("", "rules-wails-frontend-*")
	must(err)
	defer os.RemoveAll(tempRoot)

	stageRoot := filepath.Join(tempRoot, "workspace")
	homeDir := filepath.Join(tempRoot, "home")
	must(os.MkdirAll(stageRoot, 0o755))
	must(os.MkdirAll(homeDir, 0o755))

	must(stageManifest(manifestPath, stageRoot))
	must(os.MkdirAll(filepath.Join(stageRoot, "node_modules"), 0o755))
	must(copyDirectoryContents(nodeModulesRoot, filepath.Join(stageRoot, "node_modules"), true))

	sharedBunStore := filepath.Join(sharedNodeModulesRoot, ".bun")
	if pathExists(sharedBunStore) {
		stageBunStore := filepath.Join(stageRoot, "node_modules", ".bun")
		_ = os.RemoveAll(stageBunStore)
		must(os.Symlink(sharedBunStore, stageBunStore))
	}

	must(restoreBunLinks(stageRoot))
	must(overlayWorkspacePackages(stageRoot))
	must(installWorkspaceAlias(stageRoot, packageDir, workspaceDir))
	must(linkPackageNodeModules(stageRoot, packageDir))

	packageRoot := stageRoot
	if packageDir != "." {
		packageRoot = filepath.Join(stageRoot, filepath.FromSlash(packageDir))
	}

	command := exec.Command(bunPath, "--bun", "run", buildScript)
	command.Dir = packageRoot
	command.Env = append(os.Environ(),
		"HOME="+homeDir,
		"PATH="+buildPath(stageRoot, packageRoot),
	)
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	command.Stdin = os.Stdin
	must(command.Run())

	must(os.RemoveAll(outDir))
	must(os.MkdirAll(outDir, 0o755))
	must(copyDirectoryContents(filepath.Join(packageRoot, "dist"), outDir, false))
}

func resolveBunBinary(darwinAarch64 string, darwinX64 string, linuxAarch64 string, linuxX64 string, windowsX64 string) (string, error) {
	switch runtime.GOOS + "/" + runtime.GOARCH {
	case "darwin/arm64":
		return darwinAarch64, nil
	case "darwin/amd64":
		return darwinX64, nil
	case "linux/arm64":
		return linuxAarch64, nil
	case "linux/amd64":
		return linuxX64, nil
	case "windows/amd64":
		return windowsX64, nil
	default:
		return "", fmt.Errorf("unsupported Bun exec platform: %s/%s", runtime.GOOS, runtime.GOARCH)
	}
}

func overlayWorkspacePackages(stageRoot string) error {
	packageRoots := make([]string, 0)

	err := filepath.Walk(stageRoot, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if info.IsDir() && info.Name() == "node_modules" {
			return filepath.SkipDir
		}
		if !info.IsDir() && info.Name() == "package.json" {
			packageRoots = append(packageRoots, filepath.Dir(path))
		}
		return nil
	})
	if err != nil {
		return err
	}

	for _, packageRoot := range packageRoots {
		packageName, err := readPackageName(filepath.Join(packageRoot, "package.json"))
		if err != nil || packageName == "" {
			continue
		}

		nodePackageDir := filepath.Join(append([]string{stageRoot, "node_modules"}, strings.Split(packageName, "/")...)...)
		if !pathExists(nodePackageDir) {
			continue
		}

		entries, err := os.ReadDir(packageRoot)
		if err != nil {
			return err
		}

		for _, entry := range entries {
			entryPath := filepath.Join(packageRoot, entry.Name())
			destinationPath := filepath.Join(nodePackageDir, entry.Name())
			_ = os.RemoveAll(destinationPath)
			if err := os.Symlink(entryPath, destinationPath); err != nil {
				return err
			}
		}
	}

	return nil
}

func restoreBunLinks(stageRoot string) error {
	bunPackagesRoot := filepath.Join(stageRoot, "node_modules", ".bun", "node_modules")
	if !pathExists(bunPackagesRoot) {
		return nil
	}

	entries, err := os.ReadDir(bunPackagesRoot)
	if err != nil {
		return err
	}

	for _, entry := range entries {
		entryPath := filepath.Join(bunPackagesRoot, entry.Name())
		if strings.HasPrefix(entry.Name(), "@") {
			scopeRoot := filepath.Join(stageRoot, "node_modules", entry.Name())
			if err := os.MkdirAll(scopeRoot, 0o755); err != nil {
				return err
			}

			scopedEntries, err := os.ReadDir(entryPath)
			if err != nil {
				return err
			}

			for _, scopedEntry := range scopedEntries {
				scopedPath := filepath.Join(entryPath, scopedEntry.Name())
				destinationPath := filepath.Join(scopeRoot, scopedEntry.Name())
				_ = os.RemoveAll(destinationPath)
				if err := os.Symlink(scopedPath, destinationPath); err != nil {
					return err
				}
			}

			continue
		}

		destinationPath := filepath.Join(stageRoot, "node_modules", entry.Name())
		_ = os.RemoveAll(destinationPath)
		if err := os.Symlink(entryPath, destinationPath); err != nil {
			return err
		}
	}

	return nil
}

func installWorkspaceAlias(stageRoot string, packageDir string, workspaceDir string) error {
	if workspaceDir == "" || workspaceDir == "." || workspaceDir == packageDir {
		return nil
	}

	targetPath := stageRoot
	if packageDir != "." {
		targetPath = filepath.Join(stageRoot, filepath.FromSlash(packageDir))
	}
	aliasPath := filepath.Join(stageRoot, filepath.FromSlash(workspaceDir))
	if err := os.MkdirAll(filepath.Dir(aliasPath), 0o755); err != nil {
		return err
	}
	_ = os.RemoveAll(aliasPath)
	return os.Symlink(targetPath, aliasPath)
}

func linkPackageNodeModules(stageRoot string, packageDir string) error {
	if packageDir == "." {
		return nil
	}

	packageNodeModules := filepath.Join(stageRoot, filepath.FromSlash(packageDir), "node_modules")
	if pathExists(packageNodeModules) {
		return nil
	}
	if err := os.MkdirAll(filepath.Dir(packageNodeModules), 0o755); err != nil {
		return err
	}
	return os.Symlink(filepath.Join(stageRoot, "node_modules"), packageNodeModules)
}

func buildPath(stageRoot string, packageRoot string) string {
	entries := make([]string, 0, 3)
	for _, candidate := range []string{
		filepath.Join(packageRoot, "node_modules", ".bin"),
		filepath.Join(stageRoot, "node_modules", ".bin"),
		os.Getenv("PATH"),
	} {
		if candidate != "" {
			entries = append(entries, candidate)
		}
	}
	return strings.Join(entries, string(os.PathListSeparator))
}

func stageManifest(manifestPath string, destinationRoot string) error {
	entries, err := readManifest(manifestPath)
	if err != nil {
		return err
	}

	for _, entry := range entries {
		destinationPath := filepath.Join(destinationRoot, filepath.FromSlash(entry.relativePath))
		if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil {
			return err
		}
		if err := copyFile(entry.sourcePath, destinationPath); err != nil {
			return err
		}
	}

	return nil
}

func readManifest(path string) ([]manifestEntry, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	entries := make([]manifestEntry, 0)
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		parts := strings.SplitN(line, "\t", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid manifest line: %s", line)
		}
		entries = append(entries, manifestEntry{
			sourcePath:   parts[0],
			relativePath: parts[1],
		})
	}

	return entries, scanner.Err()
}

func readPackageName(path string) (string, error) {
	file, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer file.Close()

	var payload struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(file).Decode(&payload); err != nil {
		return "", err
	}
	return payload.Name, nil
}

func copyDirectoryContents(sourceRoot string, destinationRoot string, preserveSymlinks bool) error {
	if preserveSymlinks {
		copyBinary, err := resolveCopyBinary()
		if err != nil {
			return err
		}
		command := exec.Command(copyBinary, "-R", sourceRoot+"/.", destinationRoot+"/")
		command.Stdout = os.Stdout
		command.Stderr = os.Stderr
		return command.Run()
	}

	return filepath.Walk(sourceRoot, func(path string, info os.FileInfo, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}

		relativePath, err := filepath.Rel(sourceRoot, path)
		if err != nil {
			return err
		}
		if relativePath == "." {
			return nil
		}

		destinationPath := filepath.Join(destinationRoot, relativePath)
		if info.IsDir() {
			return os.MkdirAll(destinationPath, 0o755)
		}

		if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil {
			return err
		}

		return copyFile(path, destinationPath)
	})
}

func resolveCopyBinary() (string, error) {
	for _, candidate := range []string{"/bin/cp", "/usr/bin/cp"} {
		if pathExists(candidate) {
			return candidate, nil
		}
	}
	return "", fmt.Errorf("unable to locate cp binary for %s/%s", runtime.GOOS, runtime.GOARCH)
}

func copyFile(sourcePath string, destinationPath string) error {
	sourceFile, err := os.Open(sourcePath)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	destinationFile, err := os.Create(destinationPath)
	if err != nil {
		return err
	}
	defer destinationFile.Close()

	if _, err := io.Copy(destinationFile, sourceFile); err != nil {
		return err
	}

	return destinationFile.Chmod(0o644)
}

func makeAbsolute(path *string) error {
	if filepath.IsAbs(*path) {
		return nil
	}

	absolutePath, err := filepath.Abs(*path)
	if err != nil {
		return err
	}
	*path = absolutePath
	return nil
}

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

func must(err error) {
	if err != nil {
		panic(err)
	}
}

func require(condition bool, message string) {
	if !condition {
		panic(message)
	}
}
