package jsbundler

// Public-route code generation.  The single source of truth is the TypeScript
// manifest frontend/src/pages/public/pages.ts.  This reads it (via esbuild +
// goja, so it's real evaluation, not fragile text parsing) and generates:
//
//   - internal/handlers/public_pages.gen.go    Go registry: routes + <title> +
//     the page's pre-rendered body (baked here, see genssr.go: writeGoRegistry)
//   - frontend/src/pages/public/routes.gen.ts  client router maps: route → body
//     component (publicRoutes) and route → <title> (publicTitles)
//
// Generated files are committed like the other build artifacts; regenerate by
// running the bundler.

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/dop251/goja"
	esbuild "github.com/evanw/esbuild/pkg/api"
)

const pagesManifest = "src/pages/public/pages.ts" // relative to frontendDir

type pageDef struct {
	Path      string `json:"path"`
	Module    string `json:"module"` // relative to frontend/src
	Component string `json:"component"`
	Title     string `json:"title"`
	Dynamic   bool   `json:"dynamic"` // ISR page: also bake its render JS for request-time data rendering
}

func generatePublicRoutes() error {
	defs, err := loadPageDefs()
	if err != nil {
		return fmt.Errorf("loading page manifest: %w", err)
	}
	if err := writeGoRegistry(defs); err != nil {
		return fmt.Errorf("writing Go registry: %w", err)
	}
	if err := writeClientRoutes(defs); err != nil {
		return fmt.Errorf("writing client routes: %w", err)
	}
	fmt.Printf("  Public routes: %d page(s) generated\n", len(defs))
	return nil
}

// loadPageDefs bundles and evaluates the TS manifest to get the page list.
func loadPageDefs() ([]pageDef, error) {
	absCwd, err := filepath.Abs(".")
	if err != nil {
		return nil, err
	}
	pagesAbs := filepath.ToSlash(filepath.Join(absCwd, frontendDir, pagesManifest))
	entry := fmt.Sprintf("import { publicPages } from %q;\nglobalThis.__PAGES__ = JSON.stringify(publicPages);", pagesAbs)

	res := esbuild.Build(esbuild.BuildOptions{
		Stdin: &esbuild.StdinOptions{
			Contents:   entry,
			ResolveDir: absCwd,
			Sourcefile: "pages-manifest.js",
			Loader:     esbuild.LoaderTS,
		},
		Bundle:   true,
		Format:   esbuild.FormatIIFE,
		Target:   esbuild.ES2017,
		Platform: esbuild.PlatformNeutral,
		LogLevel: esbuild.LogLevelSilent,
		Write:    false,
	})
	if len(res.Errors) > 0 {
		msgs := esbuild.FormatMessages(res.Errors, esbuild.FormatMessagesOptions{})
		return nil, fmt.Errorf("esbuild: %s", strings.Join(msgs, "\n"))
	}

	vm := goja.New()
	if _, err := vm.RunString(string(res.OutputFiles[0].Contents)); err != nil {
		return nil, err
	}
	raw := vm.Get("__PAGES__")
	if raw == nil {
		return nil, fmt.Errorf("manifest did not export publicPages")
	}
	var defs []pageDef
	if err := json.Unmarshal([]byte(raw.String()), &defs); err != nil {
		return nil, err
	}
	return defs, nil
}

// goRoute maps a URL pathname to a Go ServeMux pattern. Root needs "/{$}" so
// it matches exactly instead of as a catch-all subtree.
func goRoute(pathname string) string {
	if pathname == "/" {
		return "/{$}"
	}
	return pathname
}

func writeClientRoutes(defs []pageDef) error {
	// Dedupe imports by component name (a component may back several routes).
	seen := map[string]bool{}
	var imports, entries, titles strings.Builder
	for _, d := range defs {
		if !seen[d.Component] {
			seen[d.Component] = true
			rel, err := filepath.Rel("pages/public", filepath.FromSlash(d.Module))
			if err != nil {
				return err
			}
			imp := "./" + filepath.ToSlash(rel)
			fmt.Fprintf(&imports, "import { %s } from %q;\n", d.Component, imp)
		}
		fmt.Fprintf(&entries, "    %q: %s,\n", d.Path, d.Component)
		fmt.Fprintf(&titles, "    %q: %q,\n", d.Path, d.Title)
	}

	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\n")
	b.WriteString("import { JSXElement } from \"solid-js\";\n")
	b.WriteString(imports.String())
	b.WriteString("\n// Body component for each public route, keyed by URL pathname. The client\n")
	b.WriteString("// router (public.ts) renders these when navigating without a full reload.\n")
	b.WriteString("export const publicRoutes: Record<string, () => JSXElement> = {\n")
	b.WriteString(entries.String())
	b.WriteString("};\n")
	b.WriteString("\n// <title> for each public route, applied by the client router on navigation\n")
	b.WriteString("// (the first load gets its title from the server-rendered shell).\n")
	b.WriteString("export const publicTitles: Record<string, string> = {\n")
	b.WriteString(titles.String())
	b.WriteString("};\n")

	return os.WriteFile(filepath.Join(frontendDir, "src", "pages", "public", "routes.gen.ts"), []byte(b.String()), 0644)
}
