package main

import (
	_ "embed"
	"html/template"
	"log"
	"net/http"
	"time"

	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/webauthn"
)

// shellHTML — the shell partial every page is wrapped in: "shellTop", the
// "sidebar" it calls, and "shellBottom". It used to be two Go string constants
// with the sidebar assembled by a strings.Builder, which is the one piece of
// markup that was still concatenated in Go.
//
// Two template pieces wrap every page:
//
//	{{template "shellTop" "<page-key>"}}  ← opens <html>, topbar, sidebar, content
//	{{template "shellBottom"}}            ← closes content, inspector, </html>
//
// The page-key argument highlights the active sidebar link and sets breadcrumbs.
//
//go:embed shell.html
var shellHTML string

// sidebarSections maps sidebar section → page entries {label, url, icon}
var sidebarSections = []struct {
	Label string
	Pages []struct{ Label, URL, Key string }
}{
	{
		Label: "Workspace",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "Dashboard", URL: "/dash", Key: "dash"},
		},
	},
	{
		Label: "Infrastructure",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "History", URL: "/history", Key: "history"},
		},
	},
	{
		Label: "Automation",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "Rule Trace", URL: "/trace", Key: "trace"},
			{Label: "Notifications", URL: "/notifications", Key: "notifications"},
			{Label: "Tasks", URL: "/tasks", Key: "tasks"},
			{Label: "Reminders", URL: "/reminders", Key: "reminders"},
			{Label: "Routines", URL: "/routines", Key: "routines"},
			{Label: "Morning", URL: "/morning", Key: "morning"},
			{Label: "Intake", URL: "/events", Key: "events"},
		},
	},
	{
		Label: "Ecosystem",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "Siblings", URL: "/ecosystem", Key: "ecosystem"},
		},
	},
	{
		Label: "AI",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "Chat", URL: "/chat", Key: "chat"},
			{Label: "Voice", URL: "/", Key: "voice"},
		},
	},
	{
		Label: "Settings",
		Pages: []struct{ Label, URL, Key string }{
			{Label: "Tools", URL: "/tools", Key: "tools"},
			{Label: "Model", URL: "/models", Key: "models"},
			{Label: "Passkey", URL: "/auth/passkey", Key: "passkey"},
		},
	},
}

// pageChrome is the per-page title and ethos-icons.svg symbol id, keyed by the
// page key a page hands to shellTop. One table rather than two parallel
// switches, so a new page cannot end up with a title and no icon.
var pageChrome = map[string]struct{ Title, Icon string }{
	"dash":          {"Dashboard", "i-grid"},
	"history":       {"History", "i-clock"},
	"trace":         {"Rule Trace", "i-wave"},
	"notifications": {"Notifications", "i-bell"},
	"tasks":         {"Tasks", "i-grid"},
	"reminders":     {"Reminders", "i-calendar"},
	"routines":      {"Routines", "i-repeat"},
	"morning":       {"Morning Routines", "i-calendar"},
	"events":        {"Intake", "i-download"},
	"chat":          {"Chat", "i-message"},
	"voice":         {"Voice", "i-mic"},
	"ecosystem":     {"Ecosystem", "i-grid"},
	"tools":         {"Tools", "i-settings"},
	"models":        {"Resident Model", "i-wave"},
	"passkey":       {"Passkey", "i-lock"},
}

// pageIcon returns the ethos-icons.svg symbol id for the given page. The
// sidebar template wraps it in the <use> reference.
func pageIcon(key string) string {
	if c, ok := pageChrome[key]; ok {
		return c.Icon
	}
	return "i-search"
}

// pageTitle returns the human-readable page title for the given key. An
// unknown key renders as itself rather than as a blank crumb.
func pageTitle(key string) string {
	if c, ok := pageChrome[key]; ok {
		return c.Title
	}
	return key
}

// shellFuncs returns the FuncMap shared by every server-rendered page template.
func shellFuncs() template.FuncMap {
	return template.FuncMap{
		"pageTitle":       pageTitle,
		"pageIcon":        pageIcon,
		"sidebarSections": func() any { return sidebarSections },
		"ago": func(t time.Time) string {
			if t.IsZero() {
				return "never"
			}
			return time.Since(t).Round(time.Second).String() + " ago"
		},
	}
}

// parsePage parses one server-rendered page: the shell partial plus the page's
// own embedded markup, under the shared FuncMap. extra adds page-local
// functions (/tools needs capability lookups, /trace a time format) and may be
// nil.
//
// The name is also the page's log label, so a render failure says which page.
func parsePage(name, body string, extra template.FuncMap) *template.Template {
	funcs := shellFuncs()
	for k, v := range extra {
		funcs[k] = v
	}
	return template.Must(template.New(name).Funcs(funcs).Parse(shellHTML + body))
}

// renderPage writes one page. Every handler sent the same content type and
// logged the same way on failure; the header is already written by then, so a
// render error can only be logged, never reported.
func renderPage(w http.ResponseWriter, t *template.Template, data any) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := t.Execute(w, data); err != nil {
		log.Printf("%s render: %v", t.Name(), err)
	}
}

// requireCore answers whether the surface has a core to read. mavweb runs
// without -core (voice-only), and every page that needs mavend says so with a
// 503 naming itself rather than a blank error.
func requireCore(w http.ResponseWriter, core ipc.CoreAPI, surface string) bool {
	if core == nil {
		http.Error(w, surface+" disabled (no -core)", http.StatusServiceUnavailable)
		return false
	}
	return true
}

// stepUpGate reports whether the caller may proceed through the AuthStepUp
// gate, writing the 403 itself when it may not. See stepUpOK for the policy.
func stepUpGate(w http.ResponseWriter, session *webauthn.PasskeySession, requireStepUp bool) bool {
	if stepUpOK(session, requireStepUp) {
		return true
	}
	http.Error(w, "step-up required: assert a passkey first", http.StatusForbidden)
	return false
}

// stepUpOK is the single decision point for the AuthStepUp gate shared by
// POST /tools and POST /api/revert.
//
// A nil session means WebAuthn is not configured (-webauthn-origin /
// -webauthn-rpid unset), so step-up can never be asserted — not merely unmet.
// The default is therefore fail-OPEN: gating on an unassertable session would
// 403 those surfaces permanently. In that mode the actions rest on the
// transport-level auth in front of mavweb (wg+nginx+auth), and main logs a
// startup warning naming them. With -require-stepup the same situation fails
// CLOSED instead: no assertable step-up ⇒ deny.
func stepUpOK(session *webauthn.PasskeySession, requireStepUp bool) bool {
	if session == nil {
		return !requireStepUp
	}
	return session.IsStepUp()
}
