package main

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

func main() {
	var appName string
	var binaryName string
	var configFile string
	var macOSMinimumSystemVersion string
	var manifestPath string
	var outDir string
	var wailsPath string

	flag.StringVar(&appName, "app-name", "", "")
	flag.StringVar(&binaryName, "binary-name", "", "")
	flag.StringVar(&configFile, "config-file", "config.yml", "")
	flag.StringVar(&macOSMinimumSystemVersion, "macos-minimum-system-version", "", "")
	flag.StringVar(&manifestPath, "manifest", "", "")
	flag.StringVar(&outDir, "out", "", "")
	flag.StringVar(&wailsPath, "wails", "", "")
	flag.Parse()

	require(manifestPath != "", "missing --manifest")
	require(outDir != "", "missing --out")
	require(wailsPath != "", "missing --wails")
	require(appName != "", "missing --app-name")
	require(binaryName != "", "missing --binary-name")

	var err error
	wailsPath, err = filepath.Abs(wailsPath)
	must(err)
	must(resolveGoEnvToAbsolutePath())

	tempRoot, err := os.MkdirTemp("", "rules-wails-build-assets-*")
	must(err)
	defer func() {
		_ = os.Chmod(tempRoot, 0o755)
		_ = os.RemoveAll(tempRoot)
	}()

	workDir := filepath.Join(tempRoot, "work")
	homeDir := filepath.Join(tempRoot, "home")
	must(os.MkdirAll(workDir, 0o755))
	must(os.MkdirAll(homeDir, 0o755))
	must(stageManifest(manifestPath, workDir))

	command := exec.Command(
		wailsPath,
		"update",
		"build-assets",
		"-name", appName,
		"-binaryname", binaryName,
		"-config", filepath.Join(workDir, configFile),
		"-dir", workDir,
	)
	command.Dir = workDir
	command.Env = append(os.Environ(),
		"HOME="+homeDir,
		"LC_ALL=C",
		"TZ=UTC",
	)
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	must(command.Run())

	updateMacOSMinimumVersion(filepath.Join(workDir, "darwin", "Info.plist"), macOSMinimumSystemVersion)
	updateMacOSMinimumVersion(filepath.Join(workDir, "darwin", "Info.dev.plist"), macOSMinimumSystemVersion)

	must(os.RemoveAll(outDir))
	must(os.MkdirAll(outDir, 0o755))
	must(copyTree(workDir, outDir))
}

func resolveGoEnvToAbsolutePath() error {
	goBinary := os.Getenv("GO_BIN")
	if goBinary == "" || filepath.IsAbs(goBinary) {
		return nil
	}

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

	return os.Setenv("GO_BIN", absolutePath)
}

func updateMacOSMinimumVersion(path string, minimumVersion string) {
	if minimumVersion == "" {
		return
	}
	if _, err := os.Stat(path); err != nil {
		return
	}
	if _, err := os.Stat("/usr/libexec/PlistBuddy"); err != nil {
		return
	}

	setCommand := exec.Command("/usr/libexec/PlistBuddy", "-c", "Set :LSMinimumSystemVersion "+minimumVersion, path)
	if err := setCommand.Run(); err == nil {
		return
	}

	addCommand := exec.Command("/usr/libexec/PlistBuddy", "-c", "Add :LSMinimumSystemVersion string "+minimumVersion, path)
	_ = addCommand.Run()
}

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

	for _, entry := range entries {
		destinationPath := filepath.Join(destinationRoot, 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 copyTree(sourceRoot string, destinationRoot string) error {
	return filepath.Walk(sourceRoot, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		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 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 must(err error) {
	if err != nil {
		panic(err)
	}
}

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

type manifestEntry struct {
	sourcePath   string
	relativePath string
}
