package main

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

type repeatedFlag []string

func (value *repeatedFlag) String() string {
	return strings.Join(*value, ",")
}

func (value *repeatedFlag) Set(input string) error {
	*value = append(*value, input)
	return nil
}

func main() {
	var clean bool
	var extraArgs repeatedFlag
	var manifestPath string
	var mode string
	var outDir string
	var outputPath string
	var packageDir string
	var ts bool
	var wailsPath string

	flag.BoolVar(&clean, "clean", true, "")
	flag.Var(&extraArgs, "extra-arg", "")
	flag.StringVar(&manifestPath, "manifest", "", "")
	flag.StringVar(&mode, "mode", "", "")
	flag.StringVar(&outDir, "out-dir", "", "")
	flag.StringVar(&outputPath, "out", "", "")
	flag.StringVar(&packageDir, "package-dir", "", "")
	flag.BoolVar(&ts, "ts", true, "")
	flag.StringVar(&wailsPath, "wails", "", "")
	flag.Parse()

	require(mode != "", "missing --mode")
	require(outDir != "", "missing --out-dir")
	require(wailsPath != "", "missing --wails")

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

	switch mode {
	case "action":
		require(manifestPath != "", "missing --manifest")
		require(outputPath != "", "missing --out")
		must(runActionMode(manifestPath, outputPath, outDir, wailsPath, clean, ts, extraArgs))
	case "workspace":
		require(packageDir != "", "missing --package-dir")
		must(runWorkspaceMode(packageDir, outDir, wailsPath, clean, ts, extraArgs))
	default:
		panic("unsupported --mode")
	}
}

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 runActionMode(manifestPath string, outputPath string, outDir string, wailsPath string, clean bool, ts bool, extraArgs []string) error {
	tempRoot, err := os.MkdirTemp("", "rules-wails-bindings-*")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tempRoot)

	workDir := filepath.Join(tempRoot, "work")
	if err := os.MkdirAll(workDir, 0o755); err != nil {
		return err
	}
	if err := stageManifest(manifestPath, workDir); err != nil {
		return err
	}
	if err := runBindings(workDir, outDir, wailsPath, clean, ts, extraArgs); err != nil {
		return err
	}

	if err := os.RemoveAll(outputPath); err != nil {
		return err
	}
	if err := os.MkdirAll(outputPath, 0o755); err != nil {
		return err
	}
	return copyTree(filepath.Join(workDir, outDir), outputPath)
}

func runWorkspaceMode(packageDir string, outDir string, wailsPath string, clean bool, ts bool, extraArgs []string) error {
	return runBindings(packageDir, outDir, wailsPath, clean, ts, extraArgs)
}

func runBindings(cwd string, outDir string, wailsPath string, clean bool, ts bool, extraArgs []string) error {
	commandArgs := []string{"generate", "bindings", "-d", outDir}
	if clean {
		commandArgs = append(commandArgs, "-clean")
	}
	if ts {
		commandArgs = append(commandArgs, "-ts")
	}
	commandArgs = append(commandArgs, extraArgs...)

	command := exec.Command(wailsPath, commandArgs...)
	command.Dir = cwd
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr
	return command.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
}
