package web

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"log"
	"math"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
	"golang.org/x/crypto/bcrypt"
)

// hubSession holds per-session auth and CSRF data.
type hubSession struct {
	expiresAt time.Time
	csrfToken string
}

// Server handles the dashboard web UI.
type Server struct {
	store           *store.Store
	passwordHash    string
	apiKey          string // report API key — used for controller callbacks
	version         string
	logger          *log.Logger
	templates       *template.Template
	staleThreshold  time.Duration
	versionChecker  *VersionChecker
	templateFetcher *TemplateFetcher
	assetsMgr       *assets.Manager

	sessions   map[string]*hubSession
	sessionsMu sync.RWMutex
}

// New creates a new web server.
func New(store *store.Store, passwordHash, apiKey, version string, staleThreshold time.Duration, logger *log.Logger) *Server {
	funcMap := template.FuncMap{
		"timeAgo":     timeAgo,
		"statusColor": statusColor,
		"statusIcon":  statusIcon,
		"formatFloat": func(f float64) string { return fmt.Sprintf("%.0f", f) },
		"joinStrings": func(s []string, sep string) string { return strings.Join(s, sep) },
		"json": func(v interface{}) template.JS {
			b, _ := json.Marshal(v)
			return template.JS(b)
		},
		"hubVersion": func() string { return version },
		"add":        func(a, b int) int { return a + b },
		"mapGet": func(m map[string]int, key string) int {
			if m == nil {
				return 0
			}
			return m[key]
		},
		"memoryColor":   memoryColor,
		"accuracyClass": accuracyClass,
		"gt":            func(a, b int) bool { return a > b },
	}

	tmpl := template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.html"))

	return &Server{
		store:          store,
		passwordHash:   passwordHash,
		apiKey:         apiKey,
		version:        version,
		logger:         logger,
		templates:      tmpl,
		staleThreshold: staleThreshold,
		sessions:       make(map[string]*hubSession),
	}
}

// CleanupSessions removes expired sessions. Call with: go s.CleanupSessions(ctx).
func (s *Server) CleanupSessions(ctx context.Context) {
	ticker := time.NewTicker(15 * time.Minute)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			s.sessionsMu.Lock()
			now := time.Now()
			for t, sess := range s.sessions {
				if now.After(sess.expiresAt) {
					delete(s.sessions, t)
				}
			}
			s.sessionsMu.Unlock()
		}
	}
}

// SetVersionChecker sets the version checker (optional, may be nil if no registry credentials).
func (s *Server) SetVersionChecker(vc *VersionChecker) {
	s.versionChecker = vc
}

// SetTemplateFetcher sets the template fetcher for config generation (optional).
func (s *Server) SetTemplateFetcher(tf *TemplateFetcher) {
	s.templateFetcher = tf
}

// SetAssetManager sets the asset manager for the Configuration page (optional).
func (s *Server) SetAssetManager(am *assets.Manager) {
	s.assetsMgr = am
}

// ServeHTTP routes web requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path

	// CSRF protection for all state-changing requests (web routes only).
	// API routes (/api/v1/) are Bearer-token authenticated and exempt.
	if r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodOptions {
		if path != "/login" && s.passwordHash != "" {
			if !s.validateCSRF(r) {
				s.logger.Printf("[WARN] CSRF rejected: %s %s from %s", r.Method, path, r.RemoteAddr)
				http.Error(w, "CSRF token missing or invalid. Please reload the page.", http.StatusForbidden)
				return
			}
		}
	}

	switch {
	case path == "/":
		s.handleDashboard(w, r)
	case path == "/style.css":
		s.handleCSS(w, r)
	case path == "/static/chart.min.js":
		w.Header().Set("Content-Type", "application/javascript")
		w.Header().Set("Cache-Control", "public, max-age=86400")
		w.Write(chartJS)
	case path == "/configuration":
		if r.Method == http.MethodPost {
			s.handleConfigurationAction(w, r)
		} else {
			s.handleConfiguration(w, r)
		}
	case path == "/apps" || path == "/apps/":
		s.handleApps(w, r)
	case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/reset-telemetry"):
		appName := strings.TrimPrefix(path, "/apps/")
		appName = strings.TrimSuffix(appName, "/reset-telemetry")
		if r.Method == http.MethodPost {
			s.handleResetAppTelemetry(w, r, appName)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/apps/") && strings.HasSuffix(path, "/delete-issues"):
		appName := strings.TrimPrefix(path, "/apps/")
		appName = strings.TrimSuffix(appName, "/delete-issues")
		if r.Method == http.MethodPost {
			s.handleDeleteAppIssues(w, r, appName)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/apps/"):
		appName := strings.TrimPrefix(path, "/apps/")
		s.handleAppDetail(w, r, appName)
	case path == "/login":
		s.handleLogin(w, r)
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/trigger-update"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/trigger-update")
		if r.Method == http.MethodPost {
			s.handleTriggerUpdate(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/block"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/block")
		if r.Method == http.MethodPost {
			s.handleBlockCustomer(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/unblock"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/unblock")
		if r.Method == http.MethodPost {
			s.handleUnblockCustomer(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/geo/disable"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/geo/disable")
		if r.Method == http.MethodPost {
			s.handleGeoDisable(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/push-config"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/push-config")
		if r.Method == http.MethodPost {
			s.handlePushConfig(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/pull-config"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/pull-config")
		if r.Method == http.MethodPost {
			s.handlePullConfig(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/config-diff"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/config-diff")
		if r.Method == http.MethodGet {
			s.handleConfigDiff(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/") && strings.HasSuffix(path, "/create-config"):
		customerID := strings.TrimPrefix(path, "/customers/")
		customerID = strings.TrimSuffix(customerID, "/create-config")
		if r.Method == http.MethodPost {
			s.handleCreateConfigFromReport(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/customers/"):
		customerID := strings.TrimPrefix(path, "/customers/")
		s.handleCustomerUnified(w, r, customerID)
	// Config management routes — exact matches first, then prefix matches
	case path == "/configs":
		s.handleConfigList(w, r)
	case path == "/configs/new":
		if r.Method == http.MethodPost {
			s.handleConfigCreate(w, r)
		} else {
			s.handleConfigNewForm(w, r)
		}
	case strings.HasPrefix(path, "/configs/") && strings.HasSuffix(path, "/delete"):
		customerID := strings.TrimPrefix(path, "/configs/")
		customerID = strings.TrimSuffix(customerID, "/delete")
		if r.Method == http.MethodPost {
			s.handleConfigDelete(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/configs/") && strings.HasSuffix(path, "/edit"):
		customerID := strings.TrimPrefix(path, "/configs/")
		customerID = strings.TrimSuffix(customerID, "/edit")
		if r.Method == http.MethodPost {
			s.handleConfigUpdate(w, r, customerID)
		} else {
			s.handleConfigEditForm(w, r, customerID)
		}
	case strings.HasPrefix(path, "/configs/") && strings.HasSuffix(path, "/preview"):
		customerID := strings.TrimPrefix(path, "/configs/")
		customerID = strings.TrimSuffix(customerID, "/preview")
		s.handleConfigPreview(w, r, customerID)
	case strings.HasPrefix(path, "/configs/") && strings.HasSuffix(path, "/regen-password"):
		customerID := strings.TrimPrefix(path, "/configs/")
		customerID = strings.TrimSuffix(customerID, "/regen-password")
		if r.Method == http.MethodPost {
			s.handleConfigRegenPassword(w, r, customerID)
		} else {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	case strings.HasPrefix(path, "/configs/"):
		// Redirect old config detail URL to unified customer page
		customerID := strings.TrimPrefix(path, "/configs/")
		http.Redirect(w, r, "/customers/"+customerID, http.StatusSeeOther)
	default:
		http.NotFound(w, r)
	}
}

// RequireAuth wraps a handler with session or basic authentication.
func (s *Server) RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Skip auth if no password configured
		if s.passwordHash == "" {
			next.ServeHTTP(w, r)
			return
		}

		// Always allow the login page through (GET and POST)
		if r.URL.Path == "/login" {
			next.ServeHTTP(w, r)
			return
		}

		// Check session cookie (random token stored server-side)
		if cookie, err := r.Cookie("hub_session"); err == nil {
			s.sessionsMu.RLock()
			sess, ok := s.sessions[cookie.Value]
			s.sessionsMu.RUnlock()
			if ok && time.Now().Before(sess.expiresAt) {
				next.ServeHTTP(w, r)
				return
			}
		}

		// Check basic auth (for programmatic/CLI access)
		_, password, ok := r.BasicAuth()
		if ok && bcrypt.CompareHashAndPassword([]byte(s.passwordHash), []byte(password)) == nil {
			next.ServeHTTP(w, r)
			return
		}

		// Redirect browsers to login page; send 401 for API-like requests
		if r.Header.Get("Accept") == "application/json" || r.Header.Get("X-Requested-With") != "" {
			w.Header().Set("WWW-Authenticate", `Basic realm="Felhom Hub"`)
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}
		http.Redirect(w, r, "/login", http.StatusFound)
	})
}

func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodPost {
		password := r.FormValue("password")
		if s.passwordHash != "" && bcrypt.CompareHashAndPassword([]byte(s.passwordHash), []byte(password)) == nil {
			// Generate random session token
			b := make([]byte, 32)
			_, _ = rand.Read(b)
			sessionToken := hex.EncodeToString(b)

			// Generate CSRF token
			cb := make([]byte, 32)
			_, _ = rand.Read(cb)
			csrfToken := hex.EncodeToString(cb)

			s.sessionsMu.Lock()
			s.sessions[sessionToken] = &hubSession{
				expiresAt: time.Now().Add(7 * 24 * time.Hour),
				csrfToken: csrfToken,
			}
			s.sessionsMu.Unlock()

			isSecure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
			http.SetCookie(w, &http.Cookie{
				Name:     "hub_session",
				Value:    sessionToken,
				Path:     "/",
				HttpOnly: true,
				SameSite: http.SameSiteLaxMode,
				Secure:   isSecure,
				MaxAge:   86400 * 7,
			})
			http.Redirect(w, r, "/", http.StatusSeeOther)
			return
		}
		// Render login with error
		w.WriteHeader(http.StatusUnauthorized)
		w.Write([]byte(`<html><head><title>Felhom Hub — Bejelentkezés</title></head><body style="font-family:sans-serif;display:flex;justify-content:center;padding-top:4rem"><form method="post" style="display:flex;flex-direction:column;gap:.75rem;width:300px"><h2>Felhom Hub</h2><p style="color:red">Hibás jelszó</p><input type="password" name="password" placeholder="Jelszó" autofocus style="padding:.5rem;border:1px solid #ccc;border-radius:4px"><button type="submit" style="padding:.5rem;background:#0088cc;color:#fff;border:none;border-radius:4px;cursor:pointer">Bejelentkezés</button></form></body></html>`))
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`<html><head><title>Felhom Hub — Bejelentkezés</title></head><body style="font-family:sans-serif;display:flex;justify-content:center;padding-top:4rem"><form method="post" style="display:flex;flex-direction:column;gap:.75rem;width:300px"><h2>Felhom Hub</h2><input type="password" name="password" placeholder="Jelszó" autofocus style="padding:.5rem;border:1px solid #ccc;border-radius:4px"><button type="submit" style="padding:.5rem;background:#0088cc;color:#fff;border:none;border-radius:4px;cursor:pointer">Bejelentkezés</button></form></body></html>`))
}

// validateCSRF checks the CSRF token for a session-based request.
// Returns true if CSRF is valid or if no session cookie is present (Basic Auth path).
func (s *Server) validateCSRF(r *http.Request) bool {
	cookie, err := r.Cookie("hub_session")
	if err != nil {
		// No session cookie — likely Basic Auth or programmatic access; skip CSRF
		return true
	}

	s.sessionsMu.RLock()
	sess, ok := s.sessions[cookie.Value]
	s.sessionsMu.RUnlock()
	if !ok {
		return false
	}

	submitted := r.FormValue("_csrf")
	if submitted == "" {
		submitted = r.Header.Get("X-CSRF-Token")
	}
	return submitted != "" && subtle.ConstantTimeCompare([]byte(submitted), []byte(sess.csrfToken)) == 1
}

// csrfToken returns the CSRF token for the current session.
func (s *Server) csrfToken(r *http.Request) string {
	cookie, err := r.Cookie("hub_session")
	if err != nil {
		return ""
	}
	s.sessionsMu.RLock()
	sess, ok := s.sessions[cookie.Value]
	s.sessionsMu.RUnlock()
	if !ok {
		return ""
	}
	return sess.csrfToken
}

// csrfField returns an HTML hidden input for embedding in forms.
func (s *Server) csrfField(r *http.Request) template.HTML {
	tok := s.csrfToken(r)
	return template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(tok) + `">`)
}

func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
	customers, err := s.store.GetCustomers()
	if err != nil {
		s.logger.Printf("[ERROR] Dashboard: %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	configs, _ := s.store.ListCustomerConfigs()

	type dashboardCustomer struct {
		store.CustomerSummary
		OverallStatus string // "ok", "warn", "down", "pending"
		BackupAge     string
		EventErrors   int
		EventWarnings int
	}

	// Build map of report customers keyed by ID
	seen := make(map[string]bool)
	var data []dashboardCustomer
	for _, c := range customers {
		// Skip blocked customers
		if s.store.IsCustomerBlocked(c.CustomerID) {
			continue
		}

		seen[c.CustomerID] = true
		dc := dashboardCustomer{CustomerSummary: c}

		// Determine overall status
		if c.HealthStatus == "disabled" {
			dc.OverallStatus = "disabled"
		} else if c.TimeSinceReport > time.Hour {
			dc.OverallStatus = "down"
		} else if c.TimeSinceReport > 30*time.Minute || c.HealthStatus == "warn" {
			dc.OverallStatus = "warn"
		} else if c.HealthStatus == "fail" {
			dc.OverallStatus = "down"
		} else {
			dc.OverallStatus = "ok"
		}

		// Backup age
		if c.BackupLastSnapshot != nil {
			dc.BackupAge = timeAgo(*c.BackupLastSnapshot)
		} else {
			dc.BackupAge = "–"
		}

		// Event counts (last 24h)
		if counts, err := s.store.CountEventsBySeverity(c.CustomerID, time.Now().Add(-24*time.Hour)); err == nil {
			dc.EventErrors = counts["error"]
			dc.EventWarnings = counts["warning"]
		}

		data = append(data, dc)
	}

	// Add config-only customers (no reports yet) as "pending"
	for _, cfg := range configs {
		if seen[cfg.CustomerID] || cfg.Status == "blocked" {
			continue
		}
		dc := dashboardCustomer{
			CustomerSummary: store.CustomerSummary{
				CustomerID:   cfg.CustomerID,
				CustomerName: cfg.CustomerName,
			},
			OverallStatus: "pending",
			BackupAge:     "–",
		}
		data = append(data, dc)
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.templates.ExecuteTemplate(w, "dashboard.html", data); err != nil {
		s.logger.Printf("[ERROR] Template render: %v", err)
	}
}

func (s *Server) handleTriggerUpdate(w http.ResponseWriter, r *http.Request, customerID string) {
	customer, err := s.store.GetCustomer(customerID)
	if err != nil {
		s.logger.Printf("[ERROR] Trigger update — get customer %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	if customer == nil {
		http.NotFound(w, r)
		return
	}

	// Get controller URL — from denormalized field or report JSON fallback
	controllerURL := customer.ControllerURL
	if controllerURL == "" {
		var rpt struct {
			ControllerURL string `json:"controller_url"`
		}
		json.Unmarshal([]byte(customer.ReportJSON), &rpt)
		controllerURL = rpt.ControllerURL
	}
	if controllerURL == "" {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`{"ok":false,"error":"Controller URL not available — waiting for next report"}`))
		return
	}

	if s.apiKey == "" {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"ok":false,"error":"API key not configured"}`))
		return
	}

	// POST to controller's self-update endpoint
	updateURL := controllerURL + "/api/selfupdate/update"
	req, err := http.NewRequest("POST", updateURL, nil)
	if err != nil {
		s.logger.Printf("[ERROR] Trigger update — create request for %s: %v", updateURL, err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"ok":false,"error":"Failed to create request"}`))
		return
	}
	req.Header.Set("Authorization", "Bearer "+s.apiKey)

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		s.logger.Printf("[ERROR] Trigger update — request to %s failed: %v", updateURL, err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadGateway)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller unreachable: %v", err)})
		return
	}
	defer resp.Body.Close()

	// Forward the controller's response
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
	s.logger.Printf("[INFO] Trigger update for %s — controller responded %d: %s", customerID, resp.StatusCode, string(body))

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(resp.StatusCode)
	w.Write(body)
}

// compareVersions returns >0 if a > b, 0 if equal, <0 if a < b.
// Accepts "X.Y.Z" format. Returns 0 on parse error.
func compareVersions(a, b string) int {
	a = strings.TrimPrefix(a, "v")
	b = strings.TrimPrefix(b, "v")
	aParts := strings.SplitN(a, ".", 3)
	bParts := strings.SplitN(b, ".", 3)
	if len(aParts) != 3 || len(bParts) != 3 {
		return 0
	}
	for i := 0; i < 3; i++ {
		ai, e1 := strconv.Atoi(aParts[i])
		bi, e2 := strconv.Atoi(bParts[i])
		if e1 != nil || e2 != nil {
			return 0
		}
		if ai != bi {
			return ai - bi
		}
	}
	return 0
}

func (s *Server) handleCSS(w http.ResponseWriter, r *http.Request) {
	data, err := templateFS.ReadFile("templates/style.css")
	if err != nil {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "text/css")
	w.Header().Set("Cache-Control", "public, max-age=3600")
	w.Write(data)
}

func timeAgo(t time.Time) string {
	d := time.Since(t)

	if d < time.Minute {
		return "just now"
	}
	if d < time.Hour {
		m := int(math.Round(d.Minutes()))
		return fmt.Sprintf("%d min ago", m)
	}
	if d < 24*time.Hour {
		h := int(math.Round(d.Hours()))
		return fmt.Sprintf("%dh ago", h)
	}
	days := int(d.Hours() / 24)
	return fmt.Sprintf("%dd ago", days)
}

func statusColor(status string) string {
	switch status {
	case "ok":
		return "#4ade80" // green
	case "warn":
		return "#facc15" // yellow
	case "down", "fail":
		return "#f87171" // red
	case "disabled", "pending", "blocked":
		return "#94a3b8" // gray
	default:
		return "#94a3b8" // gray
	}
}

func statusIcon(status string) string {
	return "●"
}

// handleConfiguration renders the Configuration page.
func (s *Server) handleConfiguration(w http.ResponseWriter, r *http.Request) {
	csrfToken := s.getCSRFToken(r)

	assetCount := 0
	assetLastSync := ""
	if s.assetsMgr != nil {
		assetCount = s.assetsMgr.FileCount()
		if m := s.assetsMgr.GetManifest(); m != nil {
			assetLastSync = m.Generated
		}
	}

	data := map[string]interface{}{
		"CSRFToken":     csrfToken,
		"AssetCount":    assetCount,
		"AssetLastSync": assetLastSync,
		"Flash":         r.URL.Query().Get("flash"),
	}
	if err := s.templates.ExecuteTemplate(w, "configuration.html", data); err != nil {
		s.logger.Printf("[ERROR] configuration.html template: %v", err)
	}
}

// handleConfigurationAction handles POST actions on the Configuration page.
func (s *Server) handleConfigurationAction(w http.ResponseWriter, r *http.Request) {
	action := r.FormValue("action")
	switch action {
	case "refresh_assets":
		if s.assetsMgr == nil {
			http.Redirect(w, r, "/configuration?flash=assets_not_configured", http.StatusSeeOther)
			return
		}
		if err := s.assetsMgr.ReSeed(); err != nil {
			s.logger.Printf("[ERROR] Asset re-seed failed: %v", err)
			http.Redirect(w, r, "/configuration?flash=assets_error", http.StatusSeeOther)
			return
		}
		s.logger.Printf("[INFO] Manual asset re-seed completed")
		http.Redirect(w, r, "/configuration?flash=assets_refreshed", http.StatusSeeOther)
	default:
		http.Redirect(w, r, "/configuration", http.StatusSeeOther)
	}
}
