// Package webui serves the embedded static UI build over the public edge.
//
// The committed dist/ holds only a placeholder index.html so the gateway module
// compiles with a plain `go build` (and in CI) without a UI build. The production
// gateway image replaces dist/ with the real Vite build — minus landing.html, which
// ships in the separate landing container — before compiling (see
// gateway/Dockerfile), so the binary ships the UI inside it. Because Vite is built
// with a relative asset base, one build serves under any path: the game SPA is
// mounted at /app/ (web) and /telegram/ (the Telegram Mini App) — the single-origin
// model in docs/ARCHITECTURE.md §13.
//
// Caching: Vite emits hash-named files under assets/, so those are immutable and
// cached hard (a reload/relaunch is a cache hit, not a re-download); the HTML shells carry
// no-cache so a new deploy is picked up immediately.
package webui

import (
	"embed"
	"io/fs"
	"mime"
	"net/http"
	"path"
	"strings"
)

//go:embed all:dist
var dist embed.FS

// distFS returns the embedded build rooted at dist/. The directory is embedded at
// compile time, so its absence is a build error rather than a runtime condition.
func distFS() fs.FS {
	sub, err := fs.Sub(dist, "dist")
	if err != nil {
		panic("webui: embedded dist/ missing: " + err.Error())
	}
	return sub
}

// init registers the MIME type for .webmanifest, which Go's built-in table lacks and the
// distroless runtime image has no /etc/mime.types to supply. Without it the PWA Web App Manifest
// served under /app/ would be content-sniffed to text/plain, which some browsers reject.
func init() {
	_ = mime.AddExtensionType(".webmanifest", "application/manifest+json")
}

// Handler serves the compile-time embedded UI build over the public edge — the game SPA under
// /app/, /telegram/ and /vk/. It delegates to handlerFor over the embedded dist/.
func Handler(stripPrefix, indexName string) http.Handler {
	return handlerFor(distFS(), stripPrefix, indexName)
}

// handlerFor serves content as the UI: an existing file is served directly (hash-named assets get
// an immutable cache); every other path falls back to indexName (the SPA shell) so a client-side
// deep link still loads. When stripPrefix is non-empty it is removed from the request path before
// lookup, so the same build serves under a sub-path (e.g. "/app/" or "/telegram/"). Split from
// Handler so it can be exercised over an in-memory fs.FS in tests.
func handlerFor(content fs.FS, stripPrefix, indexName string) http.Handler {
	files := http.FileServer(http.FS(content))
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		name := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
		if name == "" {
			serveIndex(w, content, indexName)
			return
		}
		if info, err := fs.Stat(content, name); err != nil || info.IsDir() {
			// Unknown path or a directory: serve the shell, never a listing.
			serveIndex(w, content, indexName)
			return
		}
		// Hash-named build assets are immutable — cache them for a year so reopening the
		// app (notably a relaunched Telegram Mini App) is a cache hit, not a re-download.
		if strings.HasPrefix(name, "assets/") {
			w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
		} else if name == "sw.js" {
			// The service worker must be revalidated on every load so a new deploy's worker (and its
			// fresh precache manifest) is picked up promptly; a cached sw.js would strand clients on
			// the old build. The worker's network-first navigation then serves the fresh shell online.
			w.Header().Set("Cache-Control", "no-cache")
		}
		files.ServeHTTP(w, r)
	})
	if p := strings.TrimSuffix(stripPrefix, "/"); p != "" {
		return http.StripPrefix(p, h)
	}
	return h
}

// serveIndex writes the named HTML shell with a 200 status, so a client-routed deep link
// still loads the app rather than a 404. The shell is marked no-cache so a new deploy's
// shell (and the asset URLs it references) is fetched fresh.
func serveIndex(w http.ResponseWriter, content fs.FS, indexName string) {
	data, err := fs.ReadFile(content, indexName)
	if err != nil {
		http.Error(w, "ui not built", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(data)
}
