package handlers

import (
	"html/template"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"thermograph/frontend/internal/render"
)

// headVerifyHTML is content.head_verify_html for the SPA shells: the
// search-engine ownership-verification <meta> tags, from env (empty when
// unset). The SSR pages carry the identical markup via the template FuncMap's
// head_verify entry (internal/content owns that copy); the shells need it
// here because their HTML never passes through the template engine.
// template.HTMLEscapeString emits the same five entities markupsafe escaped
// (&amp; &lt; &gt; &#39; &#34;).
func headVerifyHTML(google, bing string) template.HTML {
	var metas []string
	if google != "" {
		metas = append(metas, `<meta name="google-site-verification" content="`+
			template.HTMLEscapeString(google)+`">`)
	}
	if bing != "" {
		metas = append(metas, `<meta name="msvalidate.01" content="`+
			template.HTMLEscapeString(bing)+`">`)
	}
	return template.HTML(strings.Join(metas, "\n  "))
}

// shellMemoMax bounds the per-origin memo. The origin is client-controlled
// (Host/X-Forwarded-Host), so an unbounded map is the same cheap
// memory-exhaustion vector api_client.py's LRU cap closed — the Python's
// _by_origin dict had no bound (one canonical origin in every real topology
// made it moot); a flat cap keeps that property for legit traffic and just
// resets the memo under abuse instead of growing forever.
const shellMemoMax = 1024

// shell is one SPA-shell route's state — the Go port of app.py's _page():
// the file (and the verification <meta> tags, both constant for the process's
// lifetime) is read and prepped once, not on every request; only the
// __ORIGIN__ substitution actually varies per request, and even that repeats
// across requests (one canonical origin in the common topology), so the
// substituted HTML + its ETag are memoized per origin instead of
// re-read-and-re-sha1'd every time.
type shell struct {
	srv  *Server
	file string

	mu       sync.Mutex
	template string // "" = not loaded yet; a failed read retries next request
	byOrigin map[string]shellEntry
}

type shellEntry struct {
	html string
	etag string
}

// shellHandler builds the handler for one SPA-shell HTML page (the
// interactive tool's calendar/day/score/compare/legend/alerts views). It
// serves the file with its __ORIGIN__ placeholder (the link-preview/Open
// Graph tags) filled in — preview crawlers need absolute URLs, and the host
// differs between LAN and prod. originOf (not a simpler duplicate) matters
// here: this route is reached both directly (Caddy) and through backend's
// proxy fallback, and only that version prefers X-Forwarded-Host over Host —
// required for the proxied case to resolve the real browser-facing host
// instead of this internal hop's own address.
func (s *Server) shellHandler(file string) http.HandlerFunc {
	sh := &shell{srv: s, file: file, byOrigin: make(map[string]shellEntry)}
	return sh.serve
}

// load reads and preps the shell file; the caller holds sh.mu. Search-engine
// verification <meta> tags (same source as the SSR content pages) are folded
// in here, so the interactive tool's own pages carry them too.
func (sh *shell) load() (string, error) {
	if sh.template != "" {
		return sh.template, nil
	}
	raw, err := os.ReadFile(filepath.Join(sh.srv.staticDir, sh.file))
	if err != nil {
		return "", err
	}
	html := string(raw)
	if verify := string(sh.srv.headVerify); verify != "" {
		html = strings.Replace(html, "<head>", "<head>\n  "+verify, 1)
	}
	sh.template = html
	return html, nil
}

func (sh *shell) serve(w http.ResponseWriter, r *http.Request) {
	originPrefix := originOf(r) + sh.srv.base

	sh.mu.Lock()
	ent, ok := sh.byOrigin[originPrefix]
	if !ok {
		tpl, err := sh.load()
		if err != nil {
			sh.mu.Unlock()
			sh.srv.serverError(w, r, err)
			return
		}
		html := strings.ReplaceAll(tpl, "__ORIGIN__", originPrefix)
		ent = shellEntry{html: html, etag: render.ETag([]byte(html))}
		if len(sh.byOrigin) >= shellMemoMax {
			clear(sh.byOrigin)
		}
		sh.byOrigin[originPrefix] = ent
	}
	sh.mu.Unlock()

	// app.py's _page compared If-None-Match to the ETag verbatim (no
	// comma-splitting) — NotModifiedExact keeps that precise behavior.
	if render.NotModifiedExact(r.Header.Get("If-None-Match"), ent.etag) {
		w.Header().Set("ETag", ent.etag)
		w.WriteHeader(http.StatusNotModified)
		return
	}
	w.Header().Set("ETag", ent.etag)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	if r.Method != http.MethodHead {
		_, _ = io.WriteString(w, ent.html)
	}
}
