package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

func main() {
	var frontendDev string
	var frontendURL string
	var readyTimeout time.Duration
	var watchTarget string
	var workspaceDir string

	flag.StringVar(&frontendDev, "frontend-dev", "", "")
	flag.StringVar(&frontendURL, "frontend-url", "http://127.0.0.1:9245", "")
	flag.DurationVar(&readyTimeout, "ready-timeout", 2*time.Minute, "")
	flag.StringVar(&watchTarget, "watch-target", "", "")
	flag.StringVar(&workspaceDir, "workspace-dir", ".", "")
	flag.Parse()

	require(frontendDev != "", "missing --frontend-dev")
	require(watchTarget != "", "missing --watch-target")

	workspaceRoot := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
	if workspaceRoot == "" {
		workspaceRoot = "."
	}
	runfilesDir := resolveRunfilesDir()

	frontendCommand := exec.Command(frontendDev)
	frontendCommand.Dir = workspaceRoot
	frontendCommand.Env = withRunfilesEnv(os.Environ(), runfilesDir)
	frontendCommand.Stdout = os.Stdout
	frontendCommand.Stderr = os.Stderr

	must(frontendCommand.Start())
	waitCh := make(chan error, 1)
	go func() {
		waitCh <- frontendCommand.Wait()
	}()
	defer terminate(frontendCommand, waitCh)

	must(waitForURL(frontendURL, readyTimeout, waitCh))

	watchCommand, err := resolveWatchCommand(watchTarget)
	must(err)
	watchCommand.Dir = workspaceRoot
	watchCommand.Env = os.Environ()
	watchCommand.Stdout = os.Stdout
	watchCommand.Stderr = os.Stderr
	watchCommand.Stdin = os.Stdin

	if err := watchCommand.Run(); err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			os.Exit(exitError.ExitCode())
		}
		panic(err)
	}

	_ = workspaceDir
}

func resolveRunfilesDir() string {
	for _, candidate := range []string{
		os.Getenv("RUNFILES_DIR"),
		os.Getenv("RUNFILES"),
	} {
		if candidate != "" {
			return candidate
		}
	}

	executablePath, err := os.Executable()
	if err != nil {
		return ""
	}

	normalizedPath := filepath.Clean(executablePath)
	marker := ".runfiles"
	if index := strings.Index(normalizedPath, marker+string(os.PathSeparator)); index >= 0 {
		return normalizedPath[:index+len(marker)]
	}
	if strings.HasSuffix(normalizedPath, marker) {
		return normalizedPath
	}

	return ""
}

func withRunfilesEnv(environment []string, runfilesDir string) []string {
	if runfilesDir == "" {
		return environment
	}

	environment = setEnv(environment, "RUNFILES_DIR", runfilesDir)
	environment = setEnv(environment, "RUNFILES", runfilesDir)
	return environment
}

func setEnv(environment []string, key string, value string) []string {
	prefix := key + "="
	for index, entry := range environment {
		if strings.HasPrefix(entry, prefix) {
			environment[index] = prefix + value
			return environment
		}
	}
	return append(environment, prefix+value)
}

func waitForURL(url string, timeout time.Duration, waitCh <-chan error) error {
	client := http.Client{Timeout: 2 * time.Second}
	deadline := time.Now().Add(timeout)

	for time.Now().Before(deadline) {
		select {
		case err := <-waitCh:
			if err == nil {
				return fmt.Errorf("frontend dev server exited before becoming ready")
			}
			return fmt.Errorf("frontend dev server exited before becoming ready: %w", err)
		default:
		}

		response, err := client.Get(url)
		if err == nil {
			response.Body.Close()
			if response.StatusCode >= 200 && response.StatusCode < 400 {
				return nil
			}
		}

		time.Sleep(250 * time.Millisecond)
	}

	return fmt.Errorf("frontend dev server did not become ready at %s", url)
}

func resolveWatchCommand(target string) (*exec.Cmd, error) {
	for _, candidate := range []string{"ibazel", "bazelisk", "bazel"} {
		if _, err := exec.LookPath(candidate); err == nil {
			return exec.Command(candidate, "run", target), nil
		}
	}

	return nil, fmt.Errorf("neither ibazel, bazelisk, nor bazel is available in PATH")
}

func terminate(command *exec.Cmd, waitCh <-chan error) {
	if command.Process == nil {
		return
	}
	_ = command.Process.Kill()
	select {
	case <-waitCh:
	case <-time.After(2 * time.Second):
	}
}

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

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