// Package pages provides server-side HTML rendering via Go templates.
//
// Templates are embedded in the binary via //go:embed and organized as:
//
//	templates/base.html          — outer shell (banner + surface block + scripts)
//	templates/login.html         — standalone login page
//	templates/components/*.html  — reusable partials (model-select, team-select, etc.)
//	templates/surfaces/*.html    — one per surface (admin, settings, team-admin, extension)
package pages

import (
	"context"
	"crypto/rand"
	"embed"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"

	"switchboard-core/config"
	"switchboard-core/store"
)

//go:embed templates/*.html templates/components/*.html templates/surfaces/*.html
var templateFS embed.FS

// Engine holds parsed templates and rendering state.
type Engine struct {
	mu        sync.RWMutex
	templates *template.Template
	cfg       *config.Config
	stores    store.Stores
	loaders   map[string]DataLoaderFunc
	surfaces  []SurfaceManifest // v0.25.0: registered surface definitions
	devMode   bool
}

// SurfaceManifest describes a surface's registration. Core surfaces are
// registered in Go at startup. Extension surfaces will be registered from
// manifest files (future).
type SurfaceManifest struct {
	ID           string   `json:"id"`            // unique identifier: "admin", "settings", "my-dashboard"
	Route        string   `json:"route"`         // primary URL pattern: "/", "/admin"
	AltRoutes    []string `json:"alt_routes"`    // additional URL patterns
	Title        string   `json:"title"`         // human-readable: "Admin", "Settings"
	Template     string   `json:"template"`      // Go template name: "surface-admin", "surface-extension"
	Components   []string `json:"components"`    // component IDs used: ["file-tree"]
	DataRequires []string `json:"data_requires"` // data loader keys: ["workspace", "models"]
	Scripts      []string `json:"scripts"`       // JS files (surface-specific, beyond base.html common)
	Styles       []string `json:"styles"`        // CSS files (surface-specific)
	Auth         string   `json:"auth"`          // "authenticated", "public", "session", "admin"
	Layout       string   `json:"layout"`        // default pane layout preset: "single", "editor"
	Source       string   `json:"source"`        // "core" or "extension"
}

// BannerConfig holds environment banner settings.
type BannerConfig struct {
	Text       string `json:"text"`
	Color      string `json:"color"`
	Background string `json:"background"`
	Visible    bool   `json:"visible"`
}

// MessageConfig holds dismissible message bar settings.
type MessageConfig struct {
	Text    string `json:"text"`
	Variant string `json:"variant"` // info, warn, error, success
	Visible bool   `json:"visible"`
}

// FooterConfig holds optional footer bar settings.
type FooterConfig struct {
	Text    string `json:"text"`
	Visible bool   `json:"visible"`
}

// PageData is passed to every template render.
type PageData struct {
	Banner      BannerConfig
	Message     MessageConfig
	Footer      FooterConfig
	Surface     string // active surface ID
	Section     string // sub-section (for admin pages)
	CSPNonce    string
	BasePath    string
	Version     string
	Environment string
	User        *UserContext
	Data        any // surface-specific data from loader

	// v0.22.7: Theme + settings injection
	Theme           string // "dark", "light", or "" (= use default)
	SurfaceSettings any    // JSON-serialized to window.__SETTINGS__

	// v0.25.0: Surface manifest — layout preset, components, etc.
	Manifest *SurfaceManifest `json:"manifest,omitempty"`

	// v0.25.0: Enabled surface IDs for conditional nav rendering.
	EnabledSurfaces []string `json:"-"`

	// v0.27.0: Extension surfaces for sidebar nav rendering.
	ExtensionSurfaces []ExtensionNavItem `json:"-"`

	// v0.22.7: Login/splash page fields
	InstanceName     string // branding: instance display name
	LogoURL          string // branding: custom logo URL
	Tagline          string // branding: tagline under instance name
	RegistrationOpen bool   // whether self-registration is enabled
	AuthMode         string // v0.24.1: "builtin", "mtls", "oidc"
}

// SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list.
// Used by templates: {{if .SurfaceEnabled "editor"}}
func (pd PageData) SurfaceEnabled(id string) bool {
	for _, s := range pd.EnabledSurfaces {
		if s == id {
			return true
		}
	}
	return false
}

// ExtensionNavItem holds the minimal info needed to render an extension
// surface link in the sidebar nav.
type ExtensionNavItem struct {
	ID    string
	Title string
	Route string
}

// ConfigSectionEntry describes a package-declared config section for
// lazy-loading in Settings, Admin, or Team Admin surfaces.
// v0.38.3: manifest-driven discovery — no new tables or endpoints.
type ConfigSectionEntry struct {
	PackageID string `json:"package_id"` // section key & asset path prefix
	Label     string `json:"label"`      // nav link text
	Icon      string `json:"icon"`       // optional SVG path data (admin CatIcon format)
	Component string `json:"component"`  // relative asset path, e.g. "js/config.js"
	Category  string `json:"category"`   // admin only: category tab (default "system")
}

// extensionNavItems returns enabled extension surfaces for sidebar rendering.
// Queries the DB at render time so newly installed surfaces appear immediately.
func (e *Engine) extensionNavItems() []ExtensionNavItem {
	if e.stores.Packages == nil {
		return nil
	}
	ctx := context.Background()
	surfaces, err := e.stores.Packages.List(ctx)
	if err != nil {
		log.Printf("[pages] Failed to load extension surfaces for nav: %v", err)
		return nil
	}

	var items []ExtensionNavItem
	for _, sr := range surfaces {
		if sr.Source == "core" || !sr.Enabled {
			continue
		}
		// Only surface and full types have routes — extensions are headless
		if sr.Type != "surface" && sr.Type != "full" {
			continue
		}
		route := ""
		if sr.Manifest != nil {
			route, _ = sr.Manifest["route"].(string)
		}
		if route == "" {
			route = "/s/" + sr.ID
		}
		items = append(items, ExtensionNavItem{
			ID:    sr.ID,
			Title: sr.Title,
			Route: route,
		})
	}
	return items
}

// UserContext is the authenticated user's info available to templates.
type UserContext struct {
	ID          string `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"display_name"`
	Role        string `json:"role"`
	Email       string `json:"email"`
}

// DataLoaderFunc loads surface-specific data for template rendering.
type DataLoaderFunc func(c *gin.Context, stores store.Stores) (any, error)

// New creates a template engine. Call after config and stores are ready.
func New(cfg *config.Config, stores store.Stores) *Engine {
	e := &Engine{
		cfg:     cfg,
		stores:  stores,
		loaders: make(map[string]DataLoaderFunc),
		devMode: gin.Mode() == gin.DebugMode,
	}
	e.parseTemplates()
	e.registerLoaders()
	e.registerCoreSurfaces()
	e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state)
	return e
}

// registerCoreSurfaces populates the surface manifest list with the kernel
// surfaces. Extension surfaces are appended from DB at startup.
func (e *Engine) registerCoreSurfaces() {
	e.surfaces = []SurfaceManifest{
		{
			ID: "admin", Route: "/admin/:section", AltRoutes: []string{"/admin"},
			Title: "Admin", Template: "surface-admin", Auth: "admin",
			DataRequires: []string{"admin"},
			Layout: "single", Source: "core",
		},
		{
			ID: "settings", Route: "/settings/:section", AltRoutes: []string{"/settings"},
			Title: "Settings", Template: "surface-settings", Auth: "authenticated",
			DataRequires: []string{"settings"},
			Layout: "single", Source: "core",
		},
		{
			ID: "team-admin", Route: "/team-admin/:section", AltRoutes: []string{"/team-admin"},
			Title: "Team Admin", Template: "surface-team-admin", Auth: "authenticated",
			DataRequires: []string{"team-admin"},
			Layout: "single", Source: "core",
		},
		{
			ID: "workflow", Route: "/w/:id",
			Title: "Workflow", Template: "workflow", Auth: "session",
			DataRequires: []string{"workflow"},
			Layout: "single", Source: "core",
		},
		{
			ID: "workflow-landing", Route: "/w/:id/:slug",
			Title: "Workflow Landing", Template: "workflow-landing", Auth: "public",
			Layout: "single", Source: "core",
		},
		{
			ID: "welcome", Route: "/welcome",
			Title: "Welcome", Template: "surface-welcome", Auth: "authenticated",
			Layout: "single", Source: "core",
		},
	}
}

// GetSurface returns the manifest for a surface by ID, or nil if not found.
func (e *Engine) GetSurface(id string) *SurfaceManifest {
	for i := range e.surfaces {
		if e.surfaces[i].ID == id {
			return &e.surfaces[i]
		}
	}
	return nil
}

// Surfaces returns all registered surface manifests.
func (e *Engine) Surfaces() []SurfaceManifest {
	return e.surfaces
}

// parseTemplates compiles all embedded templates with the custom FuncMap.
func (e *Engine) parseTemplates() {
	funcMap := template.FuncMap{
		"toJSON":         toJSON,
		"eq":             func(a, b string) bool { return a == b },
		"contains":       stringContains,
		"roleFilterType": roleFilterType,
		"esc":            template.HTMLEscapeString,
		"safeHTML":       func(s string) template.HTML { return template.HTML(s) },
		"join":           strings.Join,
		"dict":           dict,
		"or":             tmplOr,
	}

	tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFS,
		"templates/*.html",
		"templates/components/*.html",
		"templates/surfaces/*.html",
	)
	if err != nil {
		log.Fatalf("❌ Failed to parse templates: %v", err)
	}

	e.mu.Lock()
	e.templates = tmpl
	e.mu.Unlock()

	var names []string
	for _, t := range tmpl.Templates() {
		if t.Name() != "" {
			names = append(names, t.Name())
		}
	}
	log.Printf("[pages] Parsed %d templates", len(names))
}

// RegisterLoader adds a named data loader for a surface.
func (e *Engine) RegisterLoader(name string, fn DataLoaderFunc) {
	e.loaders[name] = fn
}

// Render renders a named template into the response.
func (e *Engine) Render(c *gin.Context, name string, data PageData) {
	data.BasePath = e.cfg.BasePath
	data.Version = Version
	data.Environment = e.cfg.Environment
	data.CSPNonce = generateNonce()
	data.Banner = e.loadBanner()
	data.Message = e.loadMessage()
	data.Footer = e.loadFooter()

	// v0.22.7: Default theme if not set
	if data.Theme == "" {
		data.Theme = "dark"
	}

	e.mu.RLock()
	tmpl := e.templates
	e.mu.RUnlock()

	c.Header("Content-Type", "text/html; charset=utf-8")
	c.Status(http.StatusOK)

	if err := tmpl.ExecuteTemplate(c.Writer, name, data); err != nil {
		log.Printf("[pages] Template render error (%s): %v", name, err)
	}
}

// RenderSurface is a Gin handler factory for surface routes.
func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc {
	return func(c *gin.Context) {
		user := e.getUserContext(c)

		var data any
		if loader, ok := e.loaders[surfaceID]; ok {
			var err error
			data, err = loader(c, e.stores)
			if err != nil {
				log.Printf("[pages] Data loader error (%s): %v", surfaceID, err)
				c.String(http.StatusInternalServerError, "Failed to load page data")
				return
			}
		}

		section := c.Param("section")
		if section == "" && surfaceID == "admin" {
			section = "users"
		}
		if section == "" && surfaceID == "settings" {
			section = "general"
		}
		if section == "" && surfaceID == "team-admin" {
			section = "members"
		}

		e.Render(c, "base.html", PageData{
			Surface:           surfaceID,
			Section:           section,
			User:              user,
			Data:              data,
			Manifest:          e.GetSurface(surfaceID),
			EnabledSurfaces:   e.EnabledSurfaceIDs(),
			ExtensionSurfaces: e.extensionNavItems(),
		})
	}
}

// RenderExtensionSurface handles /s/:slug — dynamic DB lookup for extension surfaces.
// No restart required after installing a surface via admin API.
func (e *Engine) RenderExtensionSurface() gin.HandlerFunc {
	return func(c *gin.Context) {
		slug := c.Param("slug")
		if slug == "" {
			c.String(http.StatusNotFound, "Surface not found")
			return
		}

		if e.stores.Packages == nil {
			c.String(http.StatusNotFound, "Surface registry not available")
			return
		}

		// Look up by ID (slug == surface ID)
		sr, err := e.stores.Packages.Get(c.Request.Context(), slug)
		if err != nil || sr == nil {
			c.String(http.StatusNotFound, "Surface not found: "+slug)
			return
		}
		if !sr.Enabled {
			c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
			return
		}
		if sr.Source == "core" {
			// Core surfaces have their own routes — don't serve them here
			c.String(http.StatusNotFound, "Surface not found: "+slug)
			return
		}

		user := e.getUserContext(c)

		// Build manifest from DB record
		route, _ := sr.Manifest["route"].(string)
		if route == "" {
			route = "/s/" + sr.ID
		}
		layout, _ := sr.Manifest["layout"].(string)
		if layout == "" {
			layout = "single"
		}
		manifest := &SurfaceManifest{
			ID:       sr.ID,
			Route:    route,
			Title:    sr.Title,
			Template: "surface-extension",
			Layout:   layout,
			Source:   "extension",
		}

		e.Render(c, "base.html", PageData{
			Surface:           sr.ID,
			User:              user,
			Manifest:          manifest,
			EnabledSurfaces:   e.EnabledSurfaceIDs(),
			ExtensionSurfaces: e.extensionNavItems(),
		})
	}
}

// PageRouteMiddleware holds middleware handlers for each auth level.
// Passed to RegisterPageRoutes by main.go.
type PageRouteMiddleware struct {
	Authenticated gin.HandlerFunc   // AuthOrRedirect — for authenticated surfaces
	Admin         []gin.HandlerFunc // AuthOrRedirect + RequireAdminPage
	Session       gin.HandlerFunc   // AuthOrSession — for workflow
}

// RegisterPageRoutes generates Gin routes from the registered surface manifests.
// Replaces manual per-surface route blocks in main.go.
//
// Groups surfaces by auth level to avoid redundant middleware chains.
// Workflow surfaces use RenderWorkflow() (separate template + data model).
// All other surfaces use RenderSurface().
func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddleware) {
	// Group surfaces by auth type
	byAuth := map[string][]SurfaceManifest{}
	for _, s := range e.surfaces {
		byAuth[s.Auth] = append(byAuth[s.Auth], s)
	}

	// Authenticated surfaces — single middleware group
	if surfaces, ok := byAuth["authenticated"]; ok && mw.Authenticated != nil {
		group := base.Group("")
		group.Use(mw.Authenticated)
		for _, s := range surfaces {
			if !e.IsSurfaceEnabled(s.ID) {
				// Disabled surface → redirect to chat
				registerRoutes(group, s, e.disabledRedirect())
				continue
			}
			handler := e.RenderSurface(s.ID)
			registerRoutes(group, s, handler)
		}

		// v0.27.0: Extension surface catch-all — DB lookup at request time.
		// No restart needed after installing new surfaces via admin API.
		group.GET("/s/:slug", e.RenderExtensionSurface())
	}

	// Admin surfaces — auth + admin role middleware
	if surfaces, ok := byAuth["admin"]; ok && len(mw.Admin) > 0 {
		for _, s := range surfaces {
			prefix := routePrefix(s.Route)
			group := base.Group(prefix)
			for _, m := range mw.Admin {
				group.Use(m)
			}
			// Admin surfaces are always enabled (needed to manage other surfaces)
			handler := e.RenderSurface(s.ID)
			registerRoutesRelative(group, prefix, s, handler)
		}
	}

	// Session surfaces (workflow) — session middleware
	if surfaces, ok := byAuth["session"]; ok && mw.Session != nil {
		for _, s := range surfaces {
			if !e.IsSurfaceEnabled(s.ID) {
				prefix := routePrefix(s.Route)
				group := base.Group(prefix)
				group.Use(mw.Session)
				registerRoutesRelative(group, prefix, s, e.disabledRedirect())
				continue
			}
			prefix := routePrefix(s.Route)
			group := base.Group(prefix)
			group.Use(mw.Session)
			handler := e.surfaceHandler(s)
			registerRoutesRelative(group, prefix, s, handler)
		}
	}

	// Public surfaces — no middleware
	if surfaces, ok := byAuth["public"]; ok {
		for _, s := range surfaces {
			if !e.IsSurfaceEnabled(s.ID) {
				registerRoutes(base, s, e.disabledRedirect())
				continue
			}
			handler := e.surfaceHandler(s)
			registerRoutes(base, s, handler)
		}
	}

	log.Printf("[pages] Registered routes for %d surfaces", len(e.surfaces))
}

// surfaceHandler returns the appropriate Gin handler for a surface.
func (e *Engine) surfaceHandler(s SurfaceManifest) gin.HandlerFunc {
	if s.ID == "workflow" {
		return e.RenderWorkflow()
	}
	if s.ID == "workflow-landing" {
		return e.RenderWorkflowLanding()
	}
	return e.RenderSurface(s.ID)
}

// registerRoutes adds the primary route and alt routes for a surface.
// Used for surfaces in the root group (authenticated).
func registerRoutes(group *gin.RouterGroup, s SurfaceManifest, handler gin.HandlerFunc) {
	group.GET(s.Route, handler)
	for _, alt := range s.AltRoutes {
		if alt != s.Route {
			group.GET(alt, handler)
		}
	}
}

// registerRoutesRelative adds routes relative to a group prefix.
// Used for admin/session surfaces with their own prefix group.
// "/admin/:section" with prefix "/admin" → "/:section"
func registerRoutesRelative(group *gin.RouterGroup, prefix string, s SurfaceManifest, handler gin.HandlerFunc) {
	rel := stripPrefix(s.Route, prefix)
	group.GET(rel, handler)
	for _, alt := range s.AltRoutes {
		r := stripPrefix(alt, prefix)
		if r != rel {
			group.GET(r, handler)
		}
	}
}

// routePrefix extracts the first path segment as a group prefix.
// "/admin/:section" → "/admin", "/w/:id" → "/w", "/" → ""
func routePrefix(route string) string {
	trimmed := strings.TrimPrefix(route, "/")
	parts := strings.SplitN(trimmed, "/", 2)
	if len(parts) == 0 || parts[0] == "" || strings.HasPrefix(parts[0], ":") {
		return ""
	}
	return "/" + parts[0]
}

// stripPrefix removes a prefix from a route path. Returns "" if the
// entire path IS the prefix (which Gin interprets as the group root).
func stripPrefix(route, prefix string) string {
	if prefix == "" {
		return route
	}
	rel := strings.TrimPrefix(route, prefix)
	if rel == "" {
		rel = ""
	}
	return rel
}

// RenderLogin serves the standalone login page.
func (e *Engine) RenderLogin() gin.HandlerFunc {
	return func(c *gin.Context) {
		// v0.22.7: Populate splash/login fields from global config
		instanceName, logoURL, tagline := e.loadBranding()
		regOpen := e.isRegistrationOpen()

		e.Render(c, "login.html", PageData{
			Surface:          "login",
			InstanceName:     instanceName,
			LogoURL:          logoURL,
			Tagline:          tagline,
			RegistrationOpen: regOpen,
			AuthMode:         e.cfg.AuthMode,
		})
	}
}

// WorkflowPageData is passed to the workflow.html template via PageData.Data.
type WorkflowPageData struct {
	ChannelID          string
	ChannelTitle       string
	ChannelDescription string
	SessionID          string
	SessionName        string
	StageMode          string // custom | form_only | form_chat | review
	StageName          string
	FormTemplateJSON   string // typed form template JSON (empty if custom)
	TotalStages        int
	CurrentStage       int
	SurfacePkgID       string // v0.30.2: custom package surface override (empty = use StageMode)
	BrandingJSON       string // v0.35.0: workflow branding JSON (accent_color, logo_url, tagline)
}

// WorkflowLandingPageData is passed to workflow-landing.html.
type WorkflowLandingPageData struct {
	WorkflowID  string
	Scope       string
	Slug        string
	Name        string
	Description string
	Branding    struct {
		AccentColor string `json:"accent_color"`
		LogoURL     string `json:"logo_url"`
		Tagline     string `json:"tagline"`
	}
	PersonaName string
	PersonaIcon string
	StageCount     int
	FirstStageMode string // custom | form_only | form_chat (v0.29.3)
	ResumeURL      string // non-empty if visitor has an active session
}

// RenderWorkflow renders the workflow entry page for anonymous session visitors.
// The middleware (AuthOrSession) has already created/resumed the session.
func (e *Engine) RenderWorkflow() gin.HandlerFunc {
	return func(c *gin.Context) {
		channelID := c.GetString("channel_id")
		sessionID := c.GetString("session_id")

		// Load channel metadata
		var title, description string
		if title == "" {
			title = "Workflow"
		}

		// Load session display name
		sessionName := "Visitor"

		// Load workflow stage info for form rendering (v0.29.3)
		var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
		var totalStages, currentStage int
		stageMode = "custom" // default

		instanceName, _, _ := e.loadBranding()

		e.Render(c, "workflow.html", PageData{
			Surface:      "workflow",
			InstanceName: instanceName,
			Data: WorkflowPageData{
				ChannelID:          channelID,
				ChannelTitle:       title,
				ChannelDescription: description,
				SessionID:          sessionID,
				SessionName:        sessionName,
				StageMode:          stageMode,
				StageName:          stageName,
				FormTemplateJSON:   formTplJSON,
				TotalStages:        totalStages,
				CurrentStage:       currentStage,
				SurfacePkgID:       surfacePkgID,
				BrandingJSON:       brandingJSON,
			},
		})
	}
}

// RenderWorkflowLanding renders the public landing page for a workflow.
// Visitors see the workflow name, description, persona info, and a Start button.
func (e *Engine) RenderWorkflowLanding() gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := c.Request.Context()
		// Route: /w/:id/:slug — :id is scope (team-id or "global"),
		// :slug is the workflow slug. Same :id param name as /w/:id
		// (chat surface) to avoid Gin wildcard conflicts.
		scope := c.Param("id")
		slug := c.Param("slug")

		// Resolve scope to team_id
		var teamID *string
		if scope != "global" {
			teamID = &scope
		}

		if e.stores.Workflows == nil {
			c.String(http.StatusNotFound, "Workflows not available")
			return
		}

		wf, err := e.stores.Workflows.GetBySlug(ctx, teamID, slug)
		if err != nil || wf == nil {
			c.String(http.StatusNotFound, "Workflow not found")
			return
		}
		if !wf.IsActive {
			c.String(http.StatusNotFound, "Workflow not available")
			return
		}

		data := WorkflowLandingPageData{
			WorkflowID:  wf.ID,
			Scope:       scope,
			Slug:        slug,
			Name:        wf.Name,
			Description: wf.Description,
		}

		// Parse branding
		if len(wf.Branding) > 0 {
			_ = json.Unmarshal(wf.Branding, &data.Branding)
		}

		// Load stages for count + first persona info + stage mode
		stages, _ := e.stores.Workflows.ListStages(ctx, wf.ID)
		data.StageCount = len(stages)
		if len(stages) > 0 {
			data.FirstStageMode = stages[0].StageMode
			if data.FirstStageMode == "" {
				data.FirstStageMode = "custom"
			}
			// Persona lookup deferred — personas are extensions now
		}

		instanceName, _, _ := e.loadBranding()

		e.Render(c, "workflow-landing.html", PageData{
			Surface:      "workflow-landing",
			InstanceName: instanceName,
			Data:         data,
		})
	}
}

// getUserContext extracts user info from gin context (set by auth middleware)
// and enriches it with display name, username, and email from the database.
func (e *Engine) getUserContext(c *gin.Context) *UserContext {
	userID, exists := c.Get("user_id")
	if !exists {
		return nil
	}
	uid := userID.(string)
	uc := &UserContext{
		ID:    uid,
		Email: c.GetString("email"),
		Role:  c.GetString("role"),
	}
	// Enrich from DB — username, display_name, and email may not be in JWT claims.
	if e.stores.Users != nil {
		if u, err := e.stores.Users.GetByID(c.Request.Context(), uid); err == nil && u != nil {
			uc.Username = u.Username
			uc.DisplayName = u.DisplayName
			if uc.Email == "" {
				uc.Email = u.Email
			}
		}
	}
	return uc
}

// loadBanner reads banner config from global settings.
func (e *Engine) loadBanner() BannerConfig {
	if e.stores.GlobalConfig == nil {
		return BannerConfig{}
	}
	raw, err := e.stores.GlobalConfig.Get(context.Background(), "banner")
	if err != nil || raw == nil {
		return BannerConfig{}
	}
	b := BannerConfig{}
	if v, ok := raw["enabled"].(bool); ok {
		b.Visible = v
	}
	if v, ok := raw["text"].(string); ok {
		b.Text = v
	}
	if v, ok := raw["fg"].(string); ok {
		b.Color = v
	}
	if v, ok := raw["bg"].(string); ok {
		b.Background = v
	}
	return b
}

// loadMessage reads dismissible message bar config from global settings.
func (e *Engine) loadMessage() MessageConfig {
	if e.stores.GlobalConfig == nil {
		return MessageConfig{}
	}
	raw, err := e.stores.GlobalConfig.Get(context.Background(), "message")
	if err != nil || raw == nil {
		return MessageConfig{}
	}
	m := MessageConfig{}
	if v, ok := raw["enabled"].(bool); ok {
		m.Visible = v
	}
	if v, ok := raw["text"].(string); ok {
		m.Text = v
	}
	if v, ok := raw["variant"].(string); ok {
		m.Variant = v
	}
	if m.Variant == "" {
		m.Variant = "info"
	}
	return m
}

// loadFooter reads optional footer bar config from global settings.
func (e *Engine) loadFooter() FooterConfig {
	if e.stores.GlobalConfig == nil {
		return FooterConfig{}
	}
	raw, err := e.stores.GlobalConfig.Get(context.Background(), "footer")
	if err != nil || raw == nil {
		return FooterConfig{}
	}
	f := FooterConfig{}
	if v, ok := raw["enabled"].(bool); ok {
		f.Visible = v
	}
	if v, ok := raw["text"].(string); ok {
		f.Text = v
	}
	return f
}

// loadBranding reads instance branding from global settings.
func (e *Engine) loadBranding() (name, logoURL, tagline string) {
	name = "Switchboard Core" // default
	if e.stores.GlobalConfig == nil {
		return
	}
	raw, err := e.stores.GlobalConfig.Get(context.Background(), "branding")
	if err != nil || raw == nil {
		return
	}
	if v, ok := raw["instance_name"].(string); ok && v != "" {
		name = v
	}
	if v, ok := raw["logo_url"].(string); ok {
		logoURL = v
	}
	if v, ok := raw["tagline"].(string); ok {
		tagline = v
	}
	return
}

// isRegistrationOpen checks if self-registration is enabled.
func (e *Engine) isRegistrationOpen() bool {
	if e.stores.GlobalConfig == nil {
		return false
	}
	raw, err := e.stores.GlobalConfig.Get(context.Background(), "registration")
	if err != nil || raw == nil {
		return false
	}
	if v, ok := raw["enabled"].(bool); ok {
		return v
	}
	return false
}

// Version is injected at build time via ldflags.
var Version = "dev"

// SetVersion allows main.go to inject the build version.
func SetVersion(v string) {
	Version = v
}

// WriteError writes an error page directly to the response.
func (e *Engine) WriteError(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	fmt.Fprintf(w, `<!DOCTYPE html><html><head><title>Error</title></head><body><h1>%d</h1><p>%s</p></body></html>`, status, template.HTMLEscapeString(msg))
}

// ── Template FuncMap helpers ─────────────────

func toJSON(v any) template.JS {
	b, err := json.Marshal(v)
	if err != nil {
		return template.JS("null")
	}
	return template.JS(b)
}

func stringContains(haystack []string, needle string) bool {
	for _, s := range haystack {
		if s == needle {
			return true
		}
	}
	return false
}

// roleFilterType returns the model_type filter for a given role name.
func roleFilterType(role string) string {
	switch role {
	case "embedding":
		return "embedding"
	case "utility":
		return "chat"
	default:
		return ""
	}
}

// dict creates a map from alternating key/value pairs for template use.
func dict(pairs ...any) map[string]any {
	m := make(map[string]any, len(pairs)/2)
	for i := 0; i < len(pairs)-1; i += 2 {
		key, ok := pairs[i].(string)
		if !ok {
			continue
		}
		m[key] = pairs[i+1]
	}
	return m
}

// tmplOr returns the first non-empty string argument.
func tmplOr(vals ...string) string {
	for _, v := range vals {
		if v != "" {
			return v
		}
	}
	return ""
}

func generateNonce() string {
	b := make([]byte, 16)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		return "fallback-nonce"
	}
	return hex.EncodeToString(b)
}
