// Package handlers serves the server-rendered public pages of the Kjøl JS Web
// section.
//
// This file is the APP side of a coupling inversion. kjol's bundler renders each
// public page at build time and generates public_pages.gen.go — a list of routes,
// titles, baked HTML, and (for dynamic pages) the render bundle. It does not know
// what a page is served as: no document shell, no stylesheet paths, no data. That
// is all here, because all of it is the application's business.
//
// The generated file declares `var publicPages = []publicPage{...}` and nothing
// else. The TYPE is ours — which is what lets the shape of a page be an app concern
// while the rendering of one stays the framework's.
package handlers

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"kjol/jsbundler"
	"kjol/webui"
)

// publicPage is the app-side shape the generated registry is written against.
// Field names and order are the generator's contract (jsbundler/genssr.go).
type publicPage struct {
	route     string // URL path, e.g. "/js/ssr"
	title     string // <title> text
	module    string // page module relative to frontend/src (informational)
	component string // exported body component name (informational)
	html      string // pre-rendered, data-free page body (PublicLayout + page content)
	renderJS  string // bundled render entry; baked ONLY for dynamic (ISR) pages
}

// buildInfo is the payload injected into the /js/ssr page. It mirrors the
// `BuildInfo` interface the component reads via serverData<T>() — the two have to
// agree, and the JSON tags are the whole of that agreement.
type buildInfo struct {
	RenderedAt string `json:"renderedAt"`
	Stage      string `json:"stage"`
}

// RegisterPublicPages binds every generated public page to its route.
//
// A page with a render bundle is rendered PER REQUEST with live data (the ISR
// path). A page without one serves the skeleton that was baked at build time. Both
// ship complete HTML; the difference is only whether the numbers in it are fresh.
func RegisterPublicPages(mux *http.ServeMux) {
	for _, p := range publicPages {
		mux.HandleFunc("GET "+p.route, servePublicPage(p))
	}
}

func servePublicPage(p publicPage) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		body := p.html
		data := ""

		// The ISR path. The SAME Solid component that was baked at build time is run
		// again here, in goja, with data injected — so the server's markup is not a
		// template with holes punched in it, it is the component's own output.
		if p.renderJS != "" {
			payload, err := json.Marshal(buildInfo{
				RenderedAt: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
				Stage:      "request time, in goja",
			})
			if err != nil {
				log.Printf("public page %s: marshalling data: %v", p.route, err)
			} else if rendered, err := jsbundler.RenderBundleWithData(p.renderJS, string(payload)); err != nil {
				// Fall through to the baked skeleton rather than 500. A page that cannot
				// render with data is still a page; serving nothing helps no one.
				log.Printf("public page %s: ISR render failed, serving skeleton: %v", p.route, err)
			} else {
				body, data = rendered, string(payload)
			}
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(w, document(p.title, body, data))
	}
}

// document wraps a rendered body in the page shell.
//
// __SERVER_DATA__ is inlined BEFORE the bundle, and it is the same JSON the server
// just rendered with. That is what makes the client takeover silent: public.tsx
// re-renders the identical component against the identical data and produces the
// identical markup, so the swap is invisible. Omit it and the page would render, then
// visibly collapse back to its loading skeleton the moment the bundle loaded.
func document(title, body, data string) string {
	serverData := ""
	if data != "" {
		serverData = "\n<script>window.__SERVER_DATA__ = " + data + ";</script>"
	}

	// webui.ThemeBootScript is the Go/WASM kit's — reused verbatim, because it reads the
	// same "kjol-theme" key the Solid kit's controller writes. One script, one key, and a
	// reader's choice of theme survives crossing between two front-ends that share
	// nothing else. It goes BEFORE the stylesheet, or a dark-mode reader gets a white
	// page until the CSS lands.
	return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>` + title + `</title>
` + webui.ThemeBootScript + `
<link rel="stylesheet" href="/public.bundle.min.css" />
</head>
<body class="antialiased">
<div id="page-root">` + body + `</div>` + serverData + `
<script type="module" src="/public.bundle.min.js"></script>
</body>
</html>`
}
