// Package bundler is the frontend build system: it drives esbuild's Go API for
// JS bundling, compiles Solid JSX/TSX and Tailwind v4 CSS with Go-native
// compilers (no Node, no Babel, no goja on the build path), and bakes the
// public-page SSR (which does still use goja to execute components). The
// `cmd/bundle` command is a thin CLI wrapper over this package.
//
// The package also carries the runtime SSR entry (RenderBundleWithData, in
// ssr.go/renderer.go) that the server imports for ISR.
//
// Pipeline stages live in sibling files:
//
//	js.go            esbuild-driven JS bundling + sourcemap fixup
//	export_shim.go   default/named export shim plugin
//	import_check.go  .js-extension import validation
//	css.go           Tailwind compile driver + minify
//	tailwind.go      official Tailwind v4 compiler run in goja
//	candidates.go    utility-class candidate scanner (feeds Tailwind's build())
//	genssr.go        public-page SSR bake + Go registry generation
//	ssr.go/renderer.go  the goja SSR engine (also used by the server at runtime)
//	watch.go         poll-and-rebuild watch loop
package bundler

import (
	"fmt"
	"strings"
	"sync"
	"time"
)

type bundleStats struct {
	files int
	bytes int
}

// Build runs the full one-shot build: FA icon subset, public-route generation,
// the JS/CSS bundles, and the SSR bake, printing a stats summary. c selects the
// app + kjol web trees (see Config); zero-value fields fall back to the
// single-tree defaults.
func Build(c Config) error {
	Configure(c)
	// esbuild silently resolves a `.js` import to a sibling `.ts`/`.tsx`/`.jsx`,
	// which lets misnamed specifiers slip through. Catch them up front so the
	// import path always names the file that actually exists on disk.
	if violations := checkImportExtensions(); len(violations) > 0 {
		reportImportViolations(violations)
		return fmt.Errorf("%d import-extension violation(s)", len(violations))
	}

	buildStart := time.Now()

	fmt.Println("Generating FA icon subset...")
	if err := generateFAIcons(); err != nil {
		return fmt.Errorf("FA icon generation failed: %w", err)
	}

	fmt.Println("Generating public routes...")
	routesStart := time.Now()
	if err := generatePublicRoutes(); err != nil {
		return fmt.Errorf("public route generation failed: %w", err)
	}
	routesDur := time.Since(routesStart)

	// The four bundles are independent — distinct output files, shared read-only
	// inputs plus the generated files above — so run them in parallel. The Go
	// Solid compiler and Go Tailwind engine are both stateless per call.
	fmt.Println("Bundling JS + CSS...")
	results := runBundlesParallel()

	for _, r := range results {
		if r.err != nil {
			return fmt.Errorf("%s failed: %w", r.name, r.err)
		}
	}

	// One column layout drives header, rows, and footer so every field lines up.
	// Files is %6d because candidate counts run into the thousands (a %3d would
	// overflow and shift Size/Time right on the CSS rows). Total width = 55.
	sep := strings.Repeat("-", 55)
	fmt.Println()
	fmt.Printf("%-27s %6s  %8s  %9s\n", "Bundle", "Files", "Size", "Time")
	fmt.Println(sep)
	for _, r := range results {
		printStats(r.name, r.stats, r.dur)
	}
	fmt.Println(sep)
	// %-46s%9s places the time in the same field (cols 47-55) as the bundle rows.
	fmt.Printf("%-46s%9s\n", "Public routes (SSR)", formatDuration(routesDur))
	fmt.Printf("%-46s%9s\n", "Total build time", formatDuration(time.Since(buildStart)))
	fmt.Println()
	fmt.Println("Done!")
	return nil
}

type bundleResult struct {
	name  string
	stats bundleStats
	dur   time.Duration
	err   error
}

// runBundlesParallel runs the four output bundles concurrently and returns their
// results in a stable order. Per-bundle durations overlap, so they won't sum to
// wall-clock — that's the point.
func runBundlesParallel() []bundleResult {
	jobs := []struct {
		name string
		fn   func() (bundleStats, error)
	}{
		{"bundle.min.js", bundleJS},
		{"public.bundle.min.js", bundlePublicJS},
		{"bundle.min.css", bundleSPACSS},
		{"public.bundle.min.css", bundlePublicCSS},
	}
	results := make([]bundleResult, len(jobs))
	var wg sync.WaitGroup
	for i, j := range jobs {
		wg.Add(1)
		go func(i int, name string, fn func() (bundleStats, error)) {
			defer wg.Done()
			t := time.Now()
			s, err := fn()
			results[i] = bundleResult{name: name, stats: s, dur: time.Since(t), err: err}
		}(i, j.name, j.fn)
	}
	wg.Wait()
	return results
}

func printStats(name string, s bundleStats, d time.Duration) {
	fmt.Printf("%-27s %6d  %8s  %9s\n", name, s.files, formatSize(s.bytes), formatDuration(d))
}

// formatDuration renders a build-phase duration compactly: sub-second in ms,
// otherwise seconds with two decimals.
func formatDuration(d time.Duration) string {
	if d < time.Second {
		return fmt.Sprintf("%dms", d.Milliseconds())
	}
	return fmt.Sprintf("%.2fs", d.Seconds())
}

func formatSize(bytes int) string {
	if bytes >= 1024*1024 {
		return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
	}
	return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
}
