// Package build is the build pipeline: directive codegen, Tailwind, the wasm binary,
// the Solid bundle, and Go's JS shim.
//
// It used to be two packages — a `buildsteps` library and a `build` command that did
// nothing but loop over it and print. There was nothing for that split to be: the
// library had exactly two importers, both in this directory tree, and a package with no
// outside consumers is an import statement pretending to be a boundary. It is one
// package now.
//
// It cannot be `package main`, because the dev server imports it and Go will not let you
// import a main. So the cold build is a flag on the server rather than a second binary:
//
//	go run ./server -build      # build once and exit
//	go run ./server             # build, then watch and serve
//
// It is Go rather than a shell script for three reasons. The dev server has to call
// these steps on every save and cannot shell out to bash on Windows. Editors need to run
// them as tasks, and a task that only works on one platform is a task half the team
// cannot use. And the cold build and the watch build must be the SAME steps — the moment
// they are two scripts they drift, and the bug only shows up in whichever one you use
// less.
package build

import (
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"kjol/jsbundler"
)

// kjolRoot is the kjol Go module root, relative to the app. The Tailwind and codegen
// commands are run FROM there so the engine's dependencies resolve in kjol's own go.mod,
// and this app's stays lean.
const kjolRoot = "../.."

// Wwwroot is where every build artefact lands, and what the server serves.
const Wwwroot = "wwwroot"

// Step is one named stage. Naming them lets the cold build narrate itself without the
// watch loop having to care what they are called.
type Step struct {
	Name string
	Run  func() ([]byte, error)
}

func Steps() []Step {
	return []Step{
		{"generating directive glue (//gowasm:page, //gowasm:layout, //gowasm:server)", Codegen},
		{"compiling Tailwind CSS -> wwwroot/app.css", Tailwind},
		{"compiling ./wasm -> wwwroot/app.wasm (GOOS=js GOARCH=wasm)", Wasm},
		{"bundling the Solid app -> wwwroot/bundle.min.{js,css} (TSX -> Solid -> esbuild)", JS},
		{"copying Go's wasm_exec.js shim into wwwroot/", Shim},
	}
}

// All is the full build, in order. It is what the dev server runs on a code change.
//
// The returned bytes are the failing command's combined stdout+stderr, which the dev
// server puts straight into the browser's error overlay — so a compile error lands in
// front of you rather than in a terminal you were not looking at.
func All() ([]byte, error) {
	for _, s := range Steps() {
		if out, err := s.Run(); err != nil {
			return out, err
		}
	}
	return nil, nil
}

// Cold runs every step once, narrating as it goes, and exits non-zero on the first
// failure. This is `go run ./server -build`: CI, a cold start, or an editor's pre-launch
// task — anywhere there is nobody watching a browser overlay.
func Cold() {
	log.SetFlags(0)
	for _, s := range Steps() {
		log.Println("==>", s.Name)
		if out, err := s.Run(); err != nil {
			os.Stderr.Write(out)
			log.Fatalln("build failed:", err)
		}
	}
	log.Println("==> Done. Serve it with:  go run ./server")
}

// Codegen regenerates app/*.gen.go from the //gowasm: directives — the routes, the
// layouts, and the client stubs for server components. It runs FIRST: everything after
// it compiles the code it writes.
func Codegen() ([]byte, error) {
	return exec.Command("go", "run", "kjol/cmd/wasmgen", "./app").CombinedOutput()
}

// Tailwind compiles css/app.css to wwwroot/app.css, scanning the webui kit, the lexer's
// palette and this app's Go markup for utility candidates.
//
// It scans .go files, which is the whole point of kjol's native engine: the markup is
// written in Go, so that is where the class names are. Nothing in this stage involves
// JavaScript.
//
// EVERY package whose class names have to exist has to be listed here. That is not a
// warning about carelessness — it is the failure mode: a package left off this list still
// compiles, still renders, and just comes out unstyled, because the class it asked for was
// never generated. kjol/lexer is here for exactly that reason; its whole output is class
// names, and nothing else in the tree mentions text-teal-300.
func Tailwind() ([]byte, error) {
	cmd := exec.Command("go", "run", "./cmd/twcss",
		"-entry", "cmd/kjol-website/css/app.css",
		"-out", "cmd/kjol-website/wwwroot/app.css",
		"-base", ".",
		"webui/**/*.go",
		"lexer/**/*.go",
		"cmd/kjol-website/app/**/*.go",
		"cmd/kjol-website/server/**/*.go",
	)
	cmd.Dir = kjolRoot
	return cmd.CombinedOutput()
}

// Wasm compiles ./wasm to wwwroot/app.wasm.
func Wasm() ([]byte, error) {
	cmd := exec.Command("go", "build", "-o", filepath.Join(Wwwroot, "app.wasm"), "./wasm")
	cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
	return cmd.CombinedOutput()
}

// JS builds the Solid app: the SPA under /js, the server-rendered public pages, and
// their stylesheet. It is kjol/jsbundler — TSX compiled to Solid by a Go program, bundled
// by esbuild's Go API, styled by kjol/tw — run in-process rather than shelled out to, so
// a compile error comes back as a Go error and lands in the browser's error overlay like
// every other failure.
//
// It writes bundle.min.{js,css} and public.bundle.min.{js,css} into the SAME wwwroot as
// the wasm build. They never collide: different filenames, one static dir, one server.
//
// WebDir points at the shared tree — the kit, the vendored Solid runtime, the icon SVGs
// and the @theme scaffold all live there.
func JS() ([]byte, error) {
	err := jsbundler.Build(jsbundler.Config{
		AppFrontend: "frontend",
		WebDir:      filepath.Join(kjolRoot, "jsruntime"),
		Output:      Wwwroot,
		GenTSDir:    filepath.Join("frontend", "src", "ui", "generated"),
	})
	if err != nil {
		return []byte(err.Error()), err
	}
	return nil, nil
}

// Shim copies Go's wasm_exec.js into wwwroot. It is the loader the browser needs to start
// a Go wasm binary, it ships with the toolchain, and it must match the compiler that
// produced the binary — so it is copied from GOROOT rather than vendored.
func Shim() ([]byte, error) {
	out, err := exec.Command("go", "env", "GOROOT").Output()
	if err != nil {
		return out, err
	}
	goroot := strings.TrimSpace(string(out))

	for _, src := range []string{
		filepath.Join(goroot, "lib", "wasm", "wasm_exec.js"),  // Go >= 1.24
		filepath.Join(goroot, "misc", "wasm", "wasm_exec.js"), // Go <= 1.23
	} {
		b, err := os.ReadFile(src)
		if err != nil {
			continue
		}
		dst := filepath.Join(Wwwroot, "wasm_exec.js")
		// The GOROOT copy is read-only, and so is the copy we made last time. Remove it
		// first, or the write fails with a permission error that says nothing useful.
		os.Remove(dst)
		return nil, os.WriteFile(dst, b, 0o644)
	}
	return nil, os.ErrNotExist
}
