package bundler

// Public-page SSR code generation.  For each page in the manifest
// (frontend/src/pages/public/pages.ts) this renders the solid-js/html component
// to its data-free skeleton HTML — at build time, via the same in-package goja
// engine (ssr.go/renderer.go) the server re-runs for ISR — and bakes it into the
// Go registry:
//
//   - internal/handlers/public_pages.gen.go   routes + <title> + rendered body
//
// The server then just wraps each baked body in the document shell and serves
// it (internal/handlers/public_ssr.go); the browser bundle takes over on load.
//
// The env badge is intentionally absent from the rendered markup: this SSR
// render doesn't define esbuild's __ENV_TYPE__ (see ssr.go), so env.ts reads ""
// and the badge renders nothing here. The client takeover bundle bakes the real
// compile-time environment, so the badge appears after takeover.

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"
)

// writeGoRegistry renders each page's SSR skeleton and writes the Go registry
// (internal/handlers/public_pages.gen.go).  Rendering reads frontend/src and
// wwwroot/vendor relative to the project root, so the bundler must run from
// there (it always does).
//
// Rendering is cached per page (tmp/ssr-cache.json) keyed by the page's entry
// source + the content hashes of every file esbuild bundled into it, so a build
// only re-renders pages whose sources actually changed.  Cache misses render
// concurrently (one goja runtime per worker).  See ssrcache.go.
func writeGoRegistry(defs []pageDef) error {
	start := time.Now()
	engine := EngineHash()
	prev := loadSSRCache()
	reuse := prev.Engine == engine // an engine change invalidates every page
	hasher := newFileHasher()
	next := ssrCache{Engine: engine, Pages: make(map[string]ssrCacheEntry, len(defs))}

	bodies := make([]string, len(defs))
	renderJSs := make([]string, len(defs)) // bundled render entry, baked for dynamic (ISR) pages
	entries := make([]string, len(defs))
	cached := make([]bool, len(defs))

	// First pass (cheap, serial): reuse unchanged pages, collect the rest.
	var misses []renderJob
	for i, d := range defs {
		module := filepath.ToSlash(path.Join("frontend", "src", d.Module))
		entries[i] = ssrEntrySolid(module, d.Component, d.Path)
		if reuse {
			if ce, ok := prev.Pages[d.Path]; ok && ce.EntryHash == pageEntryHash(entries[i], d.Dynamic) && inputsUnchanged(ce.Inputs, hasher) {
				bodies[i] = ce.HTML
				renderJSs[i] = ce.RenderJS
				cached[i] = true
				next.Pages[d.Path] = ce // carry the fingerprint forward
				continue
			}
		}
		misses = append(misses, renderJob{idx: i, path: d.Path, component: d.Component, entry: entries[i]})
	}

	// Second pass (parallel): render the misses, then fingerprint their inputs.
	results, err := renderMisses(misses)
	if err != nil {
		return err
	}
	for _, r := range results {
		bodies[r.idx] = r.html
		// Bake the bundled render entry only for dynamic pages — the server
		// re-runs it with data at request time (ISR), so no esbuild or source
		// files are needed at runtime.
		js := ""
		if defs[r.idx].Dynamic {
			js = r.js
		}
		renderJSs[r.idx] = js
		next.Pages[r.path] = ssrCacheEntry{
			EntryHash: pageEntryHash(entries[r.idx], defs[r.idx].Dynamic),
			Inputs:    hashInputs(r.inputs, hasher),
			HTML:      r.html,
			RenderJS:  js,
		}
	}

	// Emit the registry in manifest order.
	var b strings.Builder
	b.WriteString("// Code generated by cmd/bundle; DO NOT EDIT.\n")
	b.WriteString("// Source: frontend/src/pages/public/pages.ts\n")
	b.WriteString("//\n")
	b.WriteString("// Each html field is the page's data-free SSR skeleton, rendered from its\n")
	b.WriteString("// Solid component at bundle time. The browser bundle re-renders it on load.\n\n")
	b.WriteString("package handlers\n\n")
	b.WriteString("var publicPages = []publicPage{\n")
	for i, d := range defs {
		fmt.Fprintf(&b, "\t{route: %q, title: %q, module: %q, component: %q, html: %q, renderJS: %q},\n", goRoute(d.Path), d.Title, d.Module, d.Component, bodies[i], renderJSs[i])
		status := "rendered"
		if cached[i] {
			status = "cached"
		}
		fmt.Printf("    %-20s %s (%s, %d bytes)\n", d.Path, d.Component, status, len(bodies[i]))
	}
	b.WriteString("}\n")

	if err := os.WriteFile(filepath.Join("internal", "handlers", "public_pages.gen.go"), []byte(b.String()), 0644); err != nil {
		return err
	}

	saveSSRCache(next)
	fmt.Printf("  SSR: %d rendered, %d cached in %s\n", len(misses), len(defs)-len(misses), time.Since(start).Round(time.Millisecond))
	return nil
}

// pageEntryHash keys the render cache. It folds in the page's `dynamic` flag so
// toggling ISR on/off re-renders the page (a dynamic page also bakes its render
// JS, which an entry-only hash wouldn't notice changed).
func pageEntryHash(entry string, dynamic bool) string {
	if dynamic {
		return hashString(entry + "\x00dynamic")
	}
	return hashString(entry)
}

// ssrEntrySolid builds the goja entry for one page: import its body component,
// wrap it in PublicLayout (currentPath = the page's path), render into a detached
// DOM-shim root with solid-js/web's render, and serialize. Mirrors the client
// takeover (public.tsx), which wraps the same body in the same layout — so the
// server markup and post-takeover markup match.
//
// The entry is written as plain JS using createComponent (what compiled Solid JSX
// would emit) rather than JSX, so it needs no transform; the imported .tsx page +
// layout ARE Solid-compiled by the Go-native compiler (Plugin).
//
// esbuild's __ENV_TYPE__ define is not applied to this SSR build, so env.ts
// yields "" and the EnvBadge in PublicLayout renders nothing during SSR; the
// client takeover bundle carries the baked value on load.
func ssrEntrySolid(module, component, currentPath string) string {
	return fmt.Sprintf("import { render, createComponent } from \"solid-js/web\";\n"+
		"import { PublicLayout } from \"./frontend/src/pages/public/PublicLayout.tsx\";\n"+
		"import { %[1]s } from \"./%[2]s\";\n"+
		"globalThis.__render = function () {\n"+
		"\tconst root = document.createElement(\"div\");\n"+
		"\tconst dispose = render(function () {\n"+
		"\t\treturn createComponent(PublicLayout, { currentPath: %[3]q, get children() { return createComponent(%[1]s, {}); } });\n"+
		"\t}, root);\n"+
		"\tconst out = globalThis.__serialize(root);\n"+
		"\tdispose();\n"+
		"\treturn out;\n"+
		"};", component, module, currentPath)
}
