package handlers

import (
	"bytes"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sync"
)

// The dashboard shell is pre-rendered by bun (`renderDashboard()` → dist/dashboard.html)
// and contains three placeholder tokens (data, layout, catalog). On each
// request we splice in JSON blobs as window.__PALIAD_DASHBOARD__ /
// __PALIAD_DASHBOARD_LAYOUT__ / __PALIAD_DASHBOARD_CATALOG__ so the client
// can paint the real data on first frame — no skeleton + /api/* waterfall.
const (
	dashboardDataPlaceholder    = "/*__PALIAD_DASHBOARD_DATA__*/"
	dashboardLayoutPlaceholder  = "/*__PALIAD_DASHBOARD_LAYOUT__*/"
	dashboardCatalogPlaceholder = "/*__PALIAD_DASHBOARD_CATALOG__*/"
)

var (
	dashboardShellOnce  sync.Once
	dashboardShellBytes []byte
	dashboardShellErr   error
)

// loadDashboardShell reads dist/dashboard.html once. Server restarts on new
// builds (container-level) so a lifetime cache is safe.
func loadDashboardShell() ([]byte, error) {
	dashboardShellOnce.Do(func() {
		path := filepath.Join("dist", "dashboard.html")
		dashboardShellBytes, dashboardShellErr = os.ReadFile(path)
		if dashboardShellErr != nil {
			return
		}
		if !bytes.Contains(dashboardShellBytes, []byte(dashboardDataPlaceholder)) {
			log.Printf("warning: dashboard.html is missing the data placeholder — client will fall back to /api/dashboard")
		}
	})
	return dashboardShellBytes, dashboardShellErr
}

// serveDashboardShell writes dist/dashboard.html with three JSON blobs
// spliced in (data, layout, catalog). A nil payload disables server-side
// hydration of that slot; the client falls back to fetching the
// corresponding /api/* endpoint on mount.
func serveDashboardShell(w http.ResponseWriter, _ *http.Request, payload, layout, catalog []byte) {
	shell, err := loadDashboardShell()
	if err != nil {
		http.Error(w, "dashboard shell unavailable", http.StatusInternalServerError)
		return
	}
	body := splicePlaceholder(shell, dashboardDataPlaceholder, "window.__PALIAD_DASHBOARD__=", payload)
	body = splicePlaceholder(body, dashboardLayoutPlaceholder, "window.__PALIAD_DASHBOARD_LAYOUT__=", layout)
	body = splicePlaceholder(body, dashboardCatalogPlaceholder, "window.__PALIAD_DASHBOARD_CATALOG__=", catalog)

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(body)
}

// splicePlaceholder replaces a single placeholder token with a JS
// assignment of the given JSON payload to a window.X global. A nil
// payload assigns `null` so the client can detect "no server-side
// hydration" and fall back to fetch.
func splicePlaceholder(shell []byte, placeholder, prefix string, payload []byte) []byte {
	var inline []byte
	if len(payload) > 0 {
		inline = append(inline, []byte(prefix)...)
		inline = append(inline, escapeForScript(payload)...)
		inline = append(inline, ';')
	} else {
		inline = append(inline, []byte(prefix+"null;")...)
	}
	return bytes.Replace(shell, []byte(placeholder), inline, 1)
}

// escapeForScript makes a JSON blob safe to embed directly in an inline
// <script>. JSON strings may contain `</script>` or U+2028/U+2029, both of
// which terminate script blocks in some parsers.
func escapeForScript(b []byte) []byte {
	b = bytes.ReplaceAll(b, []byte(`</`), []byte(`<\/`))
	b = bytes.ReplaceAll(b, []byte("\u2028"), []byte(`\u2028`))
	b = bytes.ReplaceAll(b, []byte("\u2029"), []byte(`\u2029`))
	return b
}

// mustJSON encodes v, falling back to null on error (logged). Used only for
// server-side hydration where a broken payload is non-fatal — the client will
// fetch /api/dashboard as a fallback.
func mustJSON(v any) []byte {
	b, err := json.Marshal(v)
	if err != nil {
		log.Printf("dashboard hydration encode: %v", err)
		return nil
	}
	return b
}
