// Package ui renders the HTMX/Tailwind frontend from server-side
// html/templates. Templates live under web/templates and are
// embedded into the binary via web.FS.
//
// Lifecycle:
//   - At startup, parse every layout, partial, and page into a
//     single *template.Template tree.
//   - To render a page, call Render(w, "<page>", ViewData{...}).
//     Render walks the page's template definitions (which override
//     the {{block "content"}} / {{block "title"}} placeholders in
//     the chosen layout) and writes the result.
package ui

import (
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"path"
	"strings"

	"gitea.dcglab.co.uk/steve/restic-manager/web"
)

// ViewData is the common frame every template renders against.
// Page handlers populate Page with their own concrete shape and the
// renderer wraps it.
type ViewData struct {
	// Title is rendered in <title>; layouts/base default to
	// "restic-manager" if absent. Pages that {{define "title"}} win.
	Title string

	// User is the currently signed-in user, or nil if the session
	// cookie is missing/invalid. The nav uses this to decide
	// whether to show "Sign out" or "Sign in".
	User *User

	// Active is the slug of the currently active primary nav tab
	// ("dashboard" / "alerts" / "audit" / "settings").
	// The nav partial highlights the matching tab.
	Active string

	// OpenAlerts is shown next to the Alerts tab when > 0.
	OpenAlerts int

	// Version is the build version printed in the top-right of the
	// chrome. Falls back to "dev" if the binary wasn't built with
	// -ldflags -X main.version=…
	Version string

	// Username pre-fills the login form on a re-render after a bad
	// attempt. Login-only.
	Username string

	// Error is a single banner-level error string. Login uses it
	// today; other pages can adopt the same field.
	Error string

	// OIDCEnabled is true when the server has an OIDC provider
	// configured. The login page uses it to show the SSO button.
	OIDCEnabled bool

	// OIDCDisplayName is the human-readable label for the OIDC
	// provider (e.g. "Authelia"). Shown on the SSO button.
	OIDCDisplayName string

	// OIDCError holds an error code returned via ?oidc_error=… after
	// a failed OIDC callback. The login page maps it to a user-facing
	// message.
	OIDCError string

	// Page carries page-specific data. Concrete type is the page's
	// own struct.
	Page any
}

// User is the minimal projection of the authenticated user that the
// templates need. Avoids leaking store internals into the view.
type User struct {
	ID       string
	Username string
	Role     string
}

// Renderer holds the parsed templates.
type Renderer struct {
	pages map[string]*template.Template
}

// New parses every layout, partial, and page from web.FS into one
// template tree per page. Pages associate with a layout via the
// path under templates/pages/: anything at templates/pages/login.html
// wraps in templates/layouts/chromeless.html, everything else wraps
// in templates/layouts/base.html.
//
// Returns an error if any template fails to parse — fail loud at
// startup, not at request time.
func New() (*Renderer, error) {
	// All layouts + partials are shared.
	commonPaths := []string{
		"templates/layouts/base.html",
		"templates/layouts/chromeless.html",
		"templates/partials/nav.html",
		"templates/partials/host_row.html",
		"templates/partials/toast.html",
		"templates/partials/awaiting_agent.html",
		"templates/partials/host_chrome.html",
		"templates/partials/tree_node.html",
		"templates/partials/alert_row.html",
		"templates/partials/crit_banner.html",
		"templates/partials/fleet_update_inner.html",
		"templates/partials/host_update_chip.html",
		"templates/partials/repo_size_chart.html",
	}

	pageEntries, err := fs.Glob(web.FS, "templates/pages/*.html")
	if err != nil {
		return nil, fmt.Errorf("ui: glob pages: %w", err)
	}
	if len(pageEntries) == 0 {
		return nil, fmt.Errorf("ui: no pages found under templates/pages/")
	}

	r := &Renderer{pages: make(map[string]*template.Template, len(pageEntries))}
	for _, p := range pageEntries {
		base := strings.TrimSuffix(path.Base(p), ".html")
		t, err := template.New(base).Funcs(funcMap()).
			ParseFS(web.FS, append(append([]string{}, commonPaths...), p)...)
		if err != nil {
			return nil, fmt.Errorf("ui: parse %s: %w", p, err)
		}
		r.pages[base] = t
	}
	return r, nil
}

// Render writes the named page (e.g. "dashboard", "login") to w,
// wrapped in the appropriate layout. layoutFor decides which.
func (r *Renderer) Render(w io.Writer, page string, data ViewData) error {
	t, ok := r.pages[page]
	if !ok {
		return fmt.Errorf("ui: unknown page %q", page)
	}
	if data.Version == "" {
		data.Version = "dev"
	}
	return t.ExecuteTemplate(w, layoutFor(page), data)
}

// RenderPartial writes a named partial template to w *without* the
// layout wrap. Used by HTMX endpoints that swap fragments into
// already-rendered pages. The partial is looked up in any page's
// template set (every page parses the full common-paths list, so
// they all see every partial). Pick "dashboard" arbitrarily as the
// lookup point — partials are layout-agnostic.
func (r *Renderer) RenderPartial(w io.Writer, name string, data ViewData) error {
	t, ok := r.pages["dashboard"]
	if !ok {
		return fmt.Errorf("ui: renderer has no pages registered")
	}
	if data.Version == "" {
		data.Version = "dev"
	}
	return t.ExecuteTemplate(w, name, data)
}

// layoutFor picks the layout name for a page. Login + bootstrap go
// chrome-less; everything else uses the standard navigation chrome.
func layoutFor(page string) string {
	switch page {
	case "login", "bootstrap", "setup":
		return "chromeless"
	default:
		return "base"
	}
}
