package handlers

import (
	"bufio"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"warpbox.dev/backend/libs/helpers"
	"warpbox.dev/backend/libs/jobs"
	"warpbox.dev/backend/libs/services"
	"warpbox.dev/backend/libs/web"
)

const adminCookieName = "warpbox_admin"

type adminPageData struct {
	Stats         services.AdminStats
	Boxes         []adminBoxView
	Users         []adminUserView
	Settings      services.UploadPolicySettings
	Storage       []services.StorageBackendView
	UserEdit      adminUserEditView
	StorageForm   adminStorageFormView
	StorageTest   adminStorageTestView
	StorageTypes  []adminStorageProviderView
	Logs          adminLogsView
	Bans          adminBansView
	Overview      adminOverview
	Section       string
	PageTitle     string
	LastInviteURL string
	Notice        string
	Error         string
}

type adminLogsView struct {
	Entries        []adminLogEntry
	Dates          []string
	Date           string
	Severity       string
	Source         string
	Query          string
	Sort           string
	TotalShown     int
	Total          int
	Page           int
	PerPage        int
	PerPageOptions []int
	TotalPages     int
	RangeFrom      int
	RangeTo        int
	PageLinks      []adminFilesPageLink
	HasPrev        bool
	HasNext        bool
	PrevHref       string
	NextHref       string
}

var adminLogsPageSizes = []int{50, 100, 250, 500}

const adminLogsDefaultPageSize = 100

type adminLogEntry struct {
	Date     string
	Time     string
	Source   string
	Severity string
	Code     string
	Message  string
	Method   string
	Path     string
	Status   string
	IP       string
	UserID   string
	Details  string
}

type adminBansView struct {
	Bans          []adminBanView
	Rules         []services.BanRule
	Settings      services.BanSettings
	Query         string
	Status        string
	ActiveCount   int
	ExpiredCount  int
	UnbannedCount int
	RulePatterns  string
	Notice        string
	Error         string
}

type adminBanView struct {
	ID          string
	Target      string
	Reason      string
	Source      string
	Status      string
	CreatedAt   string
	ExpiresAt   string
	LastMatched string
}

type adminStorageFormView struct {
	Mode          string
	Provider      string
	ProviderLabel string
	Action        string
	BackHref      string
	Config        services.StorageBackendConfig
}

type adminStorageTestView struct {
	Config     services.StorageBackendConfig
	UsageLabel string
	Tests      []services.StorageSpeedTest
	CanRun     bool
}

type adminStorageSpeedTestJSON struct {
	ID            string `json:"id"`
	Mode          string `json:"mode"`
	ModeLabel     string `json:"modeLabel"`
	Status        string `json:"status"`
	Stage         string `json:"stage"`
	Progress      int    `json:"progress"`
	CustomLabel   string `json:"customLabel,omitempty"`
	StartedLabel  string `json:"startedLabel"`
	FinishedLabel string `json:"finishedLabel"`
	Files         int    `json:"files"`
	SizeLabel     string `json:"sizeLabel"`
	WriteSpeed    string `json:"writeSpeed"`
	ReadSpeed     string `json:"readSpeed"`
	Error         string `json:"error,omitempty"`
}

type adminStorageProviderView struct {
	Provider    string
	Label       string
	Description string
	Icon        string
}

type adminOverview struct {
	UploadDays  []adminChartBar
	StorageDays []adminChartBar
	StatusBars  []adminStatBar
}

type adminChartBar struct {
	Label    string
	Value    string
	HeightPx int
	RawValue int64
}

type adminStatBar struct {
	Label        string
	Value        string
	RawValue     int
	WidthPercent int
}

type adminBoxView struct {
	ID             string
	Owner          string
	CreatedAt      string
	ExpiresAt      string
	FileCount      int
	TotalSizeLabel string
	DownloadCount  int
	MaxDownloads   int
	Protected      bool
	Expired        bool
}

type adminUserView struct {
	ID             string
	Username       string
	Email          string
	Role           string
	Status         string
	StorageUsed    string
	StorageQuota   string
	DailyUsed      string
	StorageBackend string
	CreatedAt      string
}

type adminUserEditView struct {
	ID                   string
	Username             string
	Email                string
	Role                 string
	Status               string
	StorageUsed          string
	DailyUsed            string
	EffectiveStorage     string
	EffectiveDaily       string
	EffectiveMaxDays     int
	EffectiveDailyBoxes  int
	EffectiveActiveBoxes int
	EffectiveBackend     string
	MaxUploadMB          string
	DailyUploadMB        string
	StorageQuotaMB       string
	MaxDays              string
	DailyBoxes           string
	ActiveBoxes          string
	ShortWindowRequests  string
	StorageBackendID     string
}

func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
	if a.isAdmin(r) {
		http.Redirect(w, r, "/admin", http.StatusSeeOther)
		return
	}
	a.renderAdminLogin(w, r, http.StatusOK, "")
}

func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) {
	if !a.rateLimiter.Allow("admin-login:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) {
		a.renderAdminLogin(w, r, http.StatusTooManyRequests, "Too many admin login attempts.")
		return
	}
	if err := r.ParseForm(); err != nil {
		a.renderAdminLogin(w, r, http.StatusBadRequest, "Unable to read login form.")
		return
	}
	if a.cfg.AdminToken == "" || r.FormValue("token") != a.cfg.AdminToken {
		a.logger.Warn("admin login failed", "source", "admin", "severity", "warn", "code", 4301, "ip", uploadClientIP(r))
		a.recordLoginAbuse(r, services.AbuseKindAdminLogin, "admin token login failed")
		a.renderAdminLogin(w, r, http.StatusUnauthorized, "Invalid admin token.")
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     adminCookieName,
		Value:    adminCookieValue(a.cfg.AdminToken),
		Path:     "/admin",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   r.TLS != nil,
		Expires:  time.Now().Add(12 * time.Hour),
	})
	a.logger.Info("admin login", "source", "admin", "severity", "user_activity", "code", 2301, "ip", uploadClientIP(r))
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
}

func (a *App) AdminLogout(w http.ResponseWriter, r *http.Request) {
	if !a.validateCSRF(w, r) {
		return
	}
	a.logger.Info("admin logout", "source", "admin", "severity", "user_activity", "code", 2303, "ip", uploadClientIP(r))
	a.clearUserSessionCookie(w)
	http.SetCookie(w, &http.Cookie{
		Name:     adminCookieName,
		Value:    "",
		Path:     "/admin",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   -1,
	})
	http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
}

func (a *App) AdminDashboard(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}

	stats, err := a.uploadService.AdminStats()
	if err != nil {
		http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
		return
	}
	allBoxes, err := a.uploadService.AdminBoxes(0)
	if err != nil {
		http.Error(w, "unable to load boxes", http.StatusInternalServerError)
		return
	}

	overview := buildAdminOverview(allBoxes, stats)
	recent := a.recentBoxViews(allBoxes, 8)

	a.renderPage(w, r, http.StatusOK, "admin.html", web.PageData{
		Title:       "Admin overview",
		Description: "Warpbox admin overview.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Stats:     stats,
			Boxes:     recent,
			Overview:  overview,
			Section:   "overview",
			PageTitle: "Admin overview",
		},
	})
}

// recentBoxViews renders the newest boxes (already sorted newest-first by the
// service) into display rows, resolving owner labels.
func (a *App) recentBoxViews(boxes []services.AdminBox, limit int) []adminBoxView {
	if limit > 0 && len(boxes) > limit {
		boxes = boxes[:limit]
	}
	cache := map[string]string{}
	rows := make([]adminBoxView, 0, len(boxes))
	for _, box := range boxes {
		rows = append(rows, adminBoxView{
			ID:             box.ID,
			Owner:          a.boxOwnerLabel(box.OwnerID, cache),
			CreatedAt:      box.CreatedAt.Format("Jan 2, 2006 15:04"),
			ExpiresAt:      boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04"),
			FileCount:      box.FileCount,
			TotalSizeLabel: box.TotalSizeLabel,
			DownloadCount:  box.DownloadCount,
			MaxDownloads:   box.MaxDownloads,
			Protected:      box.Protected,
			Expired:        box.Expired,
		})
	}
	return rows
}

// buildAdminOverview computes the last-14-day upload/storage series plus a few
// status distributions for the overview dashboard.
func buildAdminOverview(boxes []services.AdminBox, stats services.AdminStats) adminOverview {
	const days = 14
	const chartMaxHeightPx = 150
	now := time.Now().UTC()
	today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)

	counts := make([]int, days)
	bytes := make([]int64, days)
	labels := make([]string, days)
	for i := 0; i < days; i++ {
		day := today.AddDate(0, 0, -(days - 1 - i))
		labels[i] = day.Format("Jan 2")
	}

	for _, box := range boxes {
		created := box.CreatedAt.UTC()
		day := time.Date(created.Year(), created.Month(), created.Day(), 0, 0, 0, 0, time.UTC)
		offset := int(today.Sub(day).Hours() / 24)
		idx := days - 1 - offset
		if idx < 0 || idx >= days {
			continue
		}
		counts[idx]++
		bytes[idx] += box.TotalSize
	}

	maxCount := 0
	var maxBytes int64
	for i := 0; i < days; i++ {
		if counts[i] > maxCount {
			maxCount = counts[i]
		}
		if bytes[i] > maxBytes {
			maxBytes = bytes[i]
		}
	}

	uploadDays := make([]adminChartBar, days)
	storageDays := make([]adminChartBar, days)
	for i := 0; i < days; i++ {
		uploadDays[i] = adminChartBar{
			Label:    labels[i],
			Value:    strconv.Itoa(counts[i]),
			HeightPx: scaleHeightPx(int64(counts[i]), int64(maxCount), chartMaxHeightPx),
			RawValue: int64(counts[i]),
		}
		storageDays[i] = adminChartBar{
			Label:    labels[i],
			Value:    helpers.FormatBytes(bytes[i]),
			HeightPx: scaleHeightPx(bytes[i], maxBytes, chartMaxHeightPx),
			RawValue: bytes[i],
		}
	}

	activeBoxes := stats.TotalBoxes - stats.ExpiredBoxes
	if activeBoxes < 0 {
		activeBoxes = 0
	}
	maxStatusValue := maxInt(activeBoxes, stats.ExpiredBoxes, stats.ProtectedBoxes)
	statusBars := []adminStatBar{
		{Label: "Active", Value: strconv.Itoa(activeBoxes), RawValue: activeBoxes, WidthPercent: percentOf(activeBoxes, maxStatusValue)},
		{Label: "Expired", Value: strconv.Itoa(stats.ExpiredBoxes), RawValue: stats.ExpiredBoxes, WidthPercent: percentOf(stats.ExpiredBoxes, maxStatusValue)},
		{Label: "Password-protected", Value: strconv.Itoa(stats.ProtectedBoxes), RawValue: stats.ProtectedBoxes, WidthPercent: percentOf(stats.ProtectedBoxes, maxStatusValue)},
	}

	return adminOverview{
		UploadDays:  uploadDays,
		StorageDays: storageDays,
		StatusBars:  statusBars,
	}
}

func scaleHeightPx(value, max int64, maxHeightPx int) int {
	if max <= 0 || value <= 0 {
		return 0
	}
	height := int(value * int64(maxHeightPx) / max)
	if height < 8 {
		height = 8
	}
	if height > maxHeightPx {
		return maxHeightPx
	}
	return height
}

func percentOf(value, total int) int {
	if total <= 0 || value <= 0 {
		return 0
	}
	return value * 100 / total
}

func maxInt(values ...int) int {
	max := 0
	for _, value := range values {
		if value > max {
			max = value
		}
	}
	return max
}

func (a *App) AdminUsers(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	stats, err := a.uploadService.AdminStats()
	if err != nil {
		http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
		return
	}
	users, err := a.authService.ListUsers()
	if err != nil {
		http.Error(w, "unable to load users", http.StatusInternalServerError)
		return
	}
	rows := make([]adminUserView, 0, len(users))
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		http.Error(w, "unable to load settings", http.StatusInternalServerError)
		return
	}
	for _, user := range users {
		storageUsed, _ := a.uploadService.UserActiveStorageUsed(user.ID)
		usage, _ := a.settingsService.UsageForUser(user.ID, time.Now().UTC())
		policy := a.settingsService.EffectivePolicyForUser(settings, user)
		quota := "unlimited"
		if policy.StorageQuotaSet {
			quota = formatMB(policy.StorageQuotaMB)
		}
		rows = append(rows, adminUserView{
			ID:             user.ID,
			Username:       user.Username,
			Email:          user.Email,
			Role:           user.Role,
			Status:         user.Status,
			StorageUsed:    services.FormatMegabytesFromBytes(storageUsed),
			StorageQuota:   quota,
			DailyUsed:      services.FormatMegabytesFromBytes(usage.UploadedBytes),
			StorageBackend: policy.StorageBackendID,
			CreatedAt:      user.CreatedAt.Format("Jan 2 15:04"),
		})
	}
	a.renderPage(w, r, http.StatusOK, "admin_users.html", web.PageData{
		Title:       "Admin users",
		Description: "Manage Warpbox users and invites.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Stats:         stats,
			Users:         rows,
			Section:       "users",
			PageTitle:     "Users",
			LastInviteURL: r.URL.Query().Get("invite"),
		},
	})
}

func (a *App) AdminEditUser(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	user, err := a.authService.UserByID(r.PathValue("userID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		http.Error(w, "unable to load settings", http.StatusInternalServerError)
		return
	}
	storage, err := a.storageBackendViews()
	if err != nil {
		http.Error(w, "unable to load storage", http.StatusInternalServerError)
		return
	}
	edit, err := a.adminUserEdit(user, settings)
	if err != nil {
		http.Error(w, "unable to load user policy", http.StatusInternalServerError)
		return
	}
	a.renderPage(w, r, http.StatusOK, "admin_user_edit.html", web.PageData{
		Title:       "Edit user",
		Description: "Edit a Warpbox user.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			UserEdit:      edit,
			Storage:       storage,
			Section:       "users",
			PageTitle:     "Edit user",
			LastInviteURL: r.URL.Query().Get("invite"),
			Error:         r.URL.Query().Get("error"),
		},
	})
}

func (a *App) AdminSettings(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		http.Error(w, "unable to load settings", http.StatusInternalServerError)
		return
	}
	storage, err := a.storageBackendViews()
	if err != nil {
		http.Error(w, "unable to load storage", http.StatusInternalServerError)
		return
	}
	a.renderPage(w, r, http.StatusOK, "admin_settings.html", web.PageData{
		Title:       "Admin settings",
		Description: "Manage Warpbox upload policy.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Settings:  settings,
			Storage:   storage,
			Section:   "settings",
			PageTitle: "Settings",
		},
	})
}

func (a *App) AdminSettingsPost(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
		return
	}
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		http.Error(w, "unable to load settings", http.StatusInternalServerError)
		return
	}
	settings.AnonymousUploadsEnabled = r.FormValue("anonymous_uploads_enabled") == "on"
	if value := parsePositiveInt(r.FormValue("usage_retention_days")); value > 0 {
		settings.UsageRetentionDays = value
	}
	if value := parsePositiveFloat(r.FormValue("local_storage_max_gb")); value > 0 {
		settings.LocalStorageMaxGB = value
	}
	if value := parsePositiveInt(r.FormValue("anonymous_max_days")); value > 0 {
		settings.AnonymousMaxDays = value
	}
	if value := parsePositiveInt(r.FormValue("user_max_days")); value > 0 {
		settings.UserMaxDays = value
	}
	if value := parsePositiveInt(r.FormValue("anonymous_daily_boxes")); value > 0 {
		settings.AnonymousDailyBoxes = value
	}
	if value := parsePositiveInt(r.FormValue("user_daily_boxes")); value > 0 {
		settings.UserDailyBoxes = value
	}
	if value := parsePositiveInt(r.FormValue("anonymous_active_boxes")); value > 0 {
		settings.AnonymousActiveBoxes = value
	}
	if value := parsePositiveInt(r.FormValue("user_active_boxes")); value > 0 {
		settings.UserActiveBoxes = value
	}
	if value := parsePositiveInt(r.FormValue("short_window_requests")); value > 0 {
		settings.ShortWindowRequests = value
	}
	if value := parsePositiveInt(r.FormValue("short_window_seconds")); value > 0 {
		settings.ShortWindowSeconds = value
	}
	if value := r.FormValue("anonymous_storage_backend"); value != "" {
		settings.AnonymousStorageBackend = value
	}
	if value := r.FormValue("user_storage_backend"); value != "" {
		settings.UserStorageBackend = value
	}
	if settings.AnonymousMaxUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_max_upload_mb")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if settings.AnonymousDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_daily_upload_mb")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if settings.UserDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("user_daily_upload_mb")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if settings.DefaultUserStorageMB, err = services.ParseMegabytesValue(r.FormValue("default_user_storage_mb")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if settings.UsageRetentionDays <= 0 {
		http.Error(w, "usage retention days must be positive", http.StatusBadRequest)
		return
	}
	if _, err := a.uploadService.Storage().BackendConfig(settings.AnonymousStorageBackend); err != nil {
		http.Error(w, "anonymous storage backend not found", http.StatusBadRequest)
		return
	}
	if _, err := a.uploadService.Storage().BackendConfig(settings.UserStorageBackend); err != nil {
		http.Error(w, "user storage backend not found", http.StatusBadRequest)
		return
	}
	if err := a.settingsService.UpdateUploadPolicy(settings); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	a.logger.Info("admin settings updated", "source", "admin", "severity", "user_activity", "code", 2310, "ip", uploadClientIP(r))
	http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
}

func (a *App) AdminLogs(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	view, err := a.adminLogsView(r)
	if err != nil {
		http.Error(w, "unable to load logs", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin viewed logs", "source", "admin", "severity", "user_activity", "code", 2350, "ip", uploadClientIP(r), "date_filter", view.Date)
	a.renderPage(w, r, http.StatusOK, "admin_logs.html", web.PageData{
		Title:       "Admin logs",
		Description: "Browse Warpbox JSON logs.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Section:   "logs",
			PageTitle: "Logs",
			Logs:      view,
		},
	})
}

func (a *App) AdminBans(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	view, err := a.adminBansView(r)
	if err != nil {
		http.Error(w, "unable to load bans", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin viewed bans", "source", "admin", "severity", "user_activity", "code", 2351, "ip", uploadClientIP(r))
	a.renderPage(w, r, http.StatusOK, "admin_bans.html", web.PageData{
		Title:       "Admin bans",
		Description: "IP ban controls.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Section:   "bans",
			PageTitle: "Bans",
			Bans:      view,
		},
	})
}

func (a *App) AdminCreateBan(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Unable to read ban form."), http.StatusSeeOther)
		return
	}
	expiresAt, err := time.ParseInLocation("2006-01-02T15:04", r.FormValue("expires_at"), time.Local)
	if err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Expiration date and time is invalid."), http.StatusSeeOther)
		return
	}
	createdBy := "admin"
	if user, ok := a.currentUser(r); ok {
		createdBy = user.ID
	}
	if services.ProtectedBanTarget(r.FormValue("target"), a.cfg.TrustedProxies) {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Refusing to ban loopback or trusted proxy addresses."), http.StatusSeeOther)
		return
	}
	ban, err := a.banService.CreateManualBan(r.FormValue("target"), r.FormValue("reason"), createdBy, expiresAt.UTC())
	if err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("manual ban created", "source", "admin", "severity", "user_activity", "code", 2360, "ip", uploadClientIP(r), "ban_id", ban.ID, "target", ban.Normalized)
	http.Redirect(w, r, "/admin/bans?notice="+url.QueryEscape("Ban created."), http.StatusSeeOther)
}

func (a *App) AdminUnban(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := a.banService.Unban(r.PathValue("banID"), time.Now().UTC()); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("ban removed", "source", "admin", "severity", "user_activity", "code", 2361, "ip", uploadClientIP(r), "ban_id", r.PathValue("banID"))
	http.Redirect(w, r, "/admin/bans?notice="+url.QueryEscape("Ban removed."), http.StatusSeeOther)
}

func (a *App) AdminBanSettingsPost(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Unable to read settings form."), http.StatusSeeOther)
		return
	}
	settings := services.BanSettings{
		AutoBanEnabled:             r.FormValue("auto_ban_enabled") == "on",
		AutoBanDurationHours:       parsePositiveInt(r.FormValue("auto_ban_duration_hours")),
		MaliciousPathThreshold:     parsePositiveInt(r.FormValue("malicious_path_threshold")),
		AdminLoginFailureThreshold: parsePositiveInt(r.FormValue("admin_login_failure_threshold")),
		UserLoginFailureThreshold:  parsePositiveInt(r.FormValue("user_login_failure_threshold")),
		AbuseWindowHours:           parsePositiveInt(r.FormValue("abuse_window_hours")),
	}
	if err := a.banService.UpdateSettings(settings); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("ban settings updated", "source", "admin", "severity", "user_activity", "code", 2362, "ip", uploadClientIP(r), "auto_ban_enabled", settings.AutoBanEnabled)
	http.Redirect(w, r, "/admin/bans?notice="+url.QueryEscape("Ban settings saved."), http.StatusSeeOther)
}

func (a *App) AdminBanRulesPost(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Unable to read rules form."), http.StatusSeeOther)
		return
	}
	patterns := splitRulePatterns(r.FormValue("patterns"))
	if err := a.banService.SaveRules(patterns, time.Now().UTC()); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("ban rules updated", "source", "admin", "severity", "user_activity", "code", 2363, "ip", uploadClientIP(r), "rules", len(patterns))
	http.Redirect(w, r, "/admin/bans?notice="+url.QueryEscape("Malicious path rules saved."), http.StatusSeeOther)
}

func (a *App) AdminBanRuleDelete(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := a.banService.DeleteRule(r.PathValue("ruleID")); err != nil {
		http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("ban rule deleted", "source", "admin", "severity", "user_activity", "code", 2364, "ip", uploadClientIP(r), "rule_id", r.PathValue("ruleID"))
	http.Redirect(w, r, "/admin/bans?notice="+url.QueryEscape("Rule deleted."), http.StatusSeeOther)
}

func (a *App) AdminStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		http.Error(w, "unable to load settings", http.StatusInternalServerError)
		return
	}
	views, err := a.storageBackendViews()
	if err != nil {
		http.Error(w, "unable to load storage", http.StatusInternalServerError)
		return
	}
	a.renderPage(w, r, http.StatusOK, "admin_storage.html", web.PageData{
		Title:       "Admin storage",
		Description: "Manage Warpbox storage backends.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Settings:  settings,
			Storage:   views,
			Section:   "storage",
			PageTitle: "Storage",
			Notice:    r.URL.Query().Get("notice"),
			Error:     r.URL.Query().Get("error"),
		},
	})
}

func (a *App) AdminNewStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	a.renderPage(w, r, http.StatusOK, "admin_storage_new.html", web.PageData{
		Title:       "Add storage",
		Description: "Choose a Warpbox storage provider.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Section:      "storage",
			PageTitle:    "Add storage",
			StorageTypes: adminStorageProviderOptions(),
			Error:        r.URL.Query().Get("error"),
		},
	})
}

func (a *App) AdminNewStorageProvider(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	rawProvider := adminStorageProviderFromRequest(r)
	if !validAdminStorageProvider(rawProvider) {
		http.NotFound(w, r)
		return
	}
	provider := normalizeAdminStorageProvider(rawProvider)
	a.renderStorageForm(w, r, http.StatusOK, adminStorageFormView{
		Mode:          "create",
		Provider:      provider,
		ProviderLabel: adminStorageProviderLabel(provider),
		Action:        "/admin/storage/new/" + provider,
		BackHref:      "/admin/storage/new",
		Config: services.StorageBackendConfig{
			Provider: provider,
			Type:     adminStorageTypeForProvider(provider),
			UseSSL:   true,
		},
	}, r.URL.Query().Get("error"))
}

func (a *App) AdminEditStorageForm(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID"))
	if err != nil || cfg.ID == services.StorageBackendLocal {
		http.NotFound(w, r)
		return
	}
	provider := normalizeAdminStorageProvider(cfg.Provider)
	a.renderStorageForm(w, r, http.StatusOK, adminStorageFormView{
		Mode:          "edit",
		Provider:      provider,
		ProviderLabel: adminStorageProviderLabel(provider),
		Action:        "/admin/storage/" + cfg.ID + "/edit",
		BackHref:      "/admin/storage",
		Config:        cfg,
	}, r.URL.Query().Get("error"))
}

func (a *App) AdminStorageTests(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	tests, err := a.uploadService.Storage().ListSpeedTests(cfg.ID, 100)
	if err != nil {
		http.Error(w, "unable to load storage tests", http.StatusInternalServerError)
		return
	}
	var usage int64
	if cfg.Enabled {
		if backend, err := a.uploadService.Storage().Backend(cfg.ID); err == nil {
			usage, _ = backend.Usage(context.Background())
		}
	}
	a.renderPage(w, r, http.StatusOK, "admin_storage_tests.html", web.PageData{
		Title:       cfg.Name + " tests",
		Description: "Storage speed-test history.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Section:   "storage",
			PageTitle: cfg.Name + " tests",
			StorageTest: adminStorageTestView{
				Config:     cfg,
				UsageLabel: services.FormatMegabytesFromBytes(usage),
				Tests:      tests,
				CanRun:     cfg.Enabled && cfg.LastTestSuccess,
			},
			Notice: r.URL.Query().Get("notice"),
			Error:  r.URL.Query().Get("error"),
		},
	})
}

func (a *App) AdminStorageTestsJSON(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	tests, err := a.uploadService.Storage().ListSpeedTests(cfg.ID, 100)
	if err != nil {
		http.Error(w, "unable to load storage tests", http.StatusInternalServerError)
		return
	}
	payload := struct {
		Tests []adminStorageSpeedTestJSON `json:"tests"`
	}{Tests: adminStorageSpeedTestsJSON(tests)}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(payload)
}

func (a *App) renderStorageForm(w http.ResponseWriter, r *http.Request, status int, form adminStorageFormView, message string) {
	a.renderPage(w, r, status, "admin_storage_form.html", web.PageData{
		Title:       form.ProviderLabel + " storage",
		Description: "Configure Warpbox storage.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminPageData{
			Section:     "storage",
			PageTitle:   form.ProviderLabel + " storage",
			StorageForm: form,
			Error:       message,
		},
	})
}

func (a *App) AdminCreateStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	rawProvider := adminStorageProviderFromRequest(r)
	if !validAdminStorageProvider(rawProvider) {
		http.NotFound(w, r)
		return
	}
	provider := normalizeAdminStorageProvider(rawProvider)
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/storage/new/"+provider, http.StatusSeeOther)
		return
	}
	_, err := a.uploadService.Storage().CreateBackend(a.storageConfigFromForm(r, provider))
	if err != nil {
		http.Redirect(w, r, "/admin/storage/new/"+provider+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("storage backend created", "source", "admin", "severity", "user_activity", "code", 2320, "ip", uploadClientIP(r), "provider", provider)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape("Storage backend added."), http.StatusSeeOther)
}

func (a *App) AdminEditStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/storage", http.StatusSeeOther)
		return
	}
	provider := normalizeAdminStorageProvider(r.FormValue("provider"))
	_, err := a.uploadService.Storage().UpdateBackend(r.PathValue("backendID"), a.storageConfigFromForm(r, provider))
	if err != nil {
		http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/edit?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("storage backend updated", "source", "admin", "severity", "user_activity", "code", 2321, "ip", uploadClientIP(r), "backend_id", r.PathValue("backendID"), "provider", provider)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape("Storage backend updated."), http.StatusSeeOther)
}

func (a *App) AdminTestStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	next := "/admin/storage"
	if r.FormValue("next") == "tests" {
		next = "/admin/storage/" + r.PathValue("backendID") + "/tests"
	}
	if _, err := a.uploadService.Storage().TestBackend(r.PathValue("backendID")); err != nil {
		a.logger.Warn("storage connection test failed", "source", "admin", "severity", "warn", "code", 4320, "ip", uploadClientIP(r), "backend_id", r.PathValue("backendID"), "error", err.Error())
		http.Redirect(w, r, next+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("storage connection test passed", "source", "admin", "severity", "user_activity", "code", 2322, "ip", uploadClientIP(r), "backend_id", r.PathValue("backendID"))
	http.Redirect(w, r, next+"?notice="+url.QueryEscape("Storage connection test passed."), http.StatusSeeOther)
}

func (a *App) AdminStartStorageSpeedTest(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape("Unable to read speed test form."), http.StatusSeeOther)
		return
	}
	options := services.StorageSpeedTestOptions{
		Mode:             r.FormValue("mode"),
		CustomFileCount:  parsePositiveInt(r.FormValue("custom_file_count")),
		CustomFileSizeMB: parsePositiveFloat(r.FormValue("custom_file_size_mb")),
	}
	test, err := a.uploadService.Storage().StartSpeedTestWithOptions(r.PathValue("backendID"), options)
	if err != nil {
		http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/tests?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	go a.uploadService.Storage().RunSpeedTest(context.Background(), test.ID)
	a.logger.Info("storage speed test started", "source", "admin", "severity", "user_activity", "code", 2323, "ip", uploadClientIP(r), "backend_id", r.PathValue("backendID"), "test_id", test.ID, "mode", test.Mode)
	http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/tests?notice="+url.QueryEscape("Storage speed test started in the background."), http.StatusSeeOther)
}

func (a *App) AdminDeleteStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	id := r.PathValue("backendID")
	cfg, err := a.uploadService.Storage().BackendConfig(id)
	if err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	if cfg.ID == services.StorageBackendLocal {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape("local storage cannot be deleted"), http.StatusSeeOther)
		return
	}
	deletedBoxes, err := a.uploadService.DeleteBoxesForStorageBackend(id, "storage-delete")
	if err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	resetAnonymous, resetUsersDefault, err := a.settingsService.ResetStorageBackend(id)
	if err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	clearedUsers, err := a.authService.ClearStorageBackendOverrides(id)
	if err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	if err := a.uploadService.Storage().DeleteBackend(id, false); err != nil {
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	notice := fmt.Sprintf("Storage backend deleted. Removed %d related boxes and cleared %d user overrides.", deletedBoxes, clearedUsers)
	if resetAnonymous || resetUsersDefault {
		notice += " Global storage defaults were reset to local."
	}
	a.logger.Info("storage backend deleted", "source", "admin", "severity", "user_activity", "code", 2325, "ip", uploadClientIP(r), "backend_id", id)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(notice), http.StatusSeeOther)
}

func (a *App) AdminRunStorageCleanup(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	cleaned, err := jobs.RunCleanupNow(a.uploadService, a.logger)
	if err != nil {
		a.logger.Warn("admin cleanup run failed", "source", "admin", "severity", "warn", "code", 4340, "ip", uploadClientIP(r), "error", err.Error())
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("admin ran cleanup", "source", "admin", "severity", "user_activity", "code", 2340, "ip", uploadClientIP(r), "cleaned", cleaned)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(fmt.Sprintf("Cleanup finished. Removed %d unavailable boxes.", cleaned)), http.StatusSeeOther)
}

func (a *App) AdminRunStorageThumbnails(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	result, err := jobs.RunThumbnailsNow(a.uploadService, a.logger)
	if err != nil {
		a.logger.Warn("admin thumbnail run failed", "source", "admin", "severity", "warn", "code", 4341, "ip", uploadClientIP(r), "error", err.Error())
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	message := fmt.Sprintf("Thumbnail pass finished. Scanned %d files, generated %d, failed %d.", result.Scanned, result.Generated, result.Failed)
	a.logger.Info("admin ran thumbnail generation", "source", "admin", "severity", "user_activity", "code", 2341, "ip", uploadClientIP(r), "scanned", result.Scanned, "generated", result.Generated, "failed", result.Failed)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(message), http.StatusSeeOther)
}

func (a *App) AdminVerifyStorageBackends(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	configs, err := a.uploadService.Storage().ListBackendConfigs()
	if err != nil {
		a.logger.Warn("admin storage verification failed", "source", "admin", "severity", "warn", "code", 4342, "ip", uploadClientIP(r), "error", err.Error())
		http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	passed := 0
	failed := 0
	for _, cfg := range configs {
		if !cfg.Enabled {
			continue
		}
		if _, err := a.uploadService.Storage().TestBackend(cfg.ID); err != nil {
			failed++
			continue
		}
		passed++
	}
	message := fmt.Sprintf("Storage verification finished. %d passed, %d failed.", passed, failed)
	a.logger.Info("admin verified storage backends", "source", "admin", "severity", "user_activity", "code", 2342, "ip", uploadClientIP(r), "passed", passed, "failed", failed)
	http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(message), http.StatusSeeOther)
}

func (a *App) AdminUpdateUserQuota(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
		return
	}
	var quota *float64
	if r.FormValue("storage_quota_mb") != "" {
		parsed, err := services.ParseMegabytesValue(r.FormValue("storage_quota_mb"))
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		quota = &parsed
	}
	if err := a.authService.SetUserStorageQuota(r.PathValue("userID"), quota); err != nil {
		http.Error(w, "unable to update quota", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin updated user quota", "source", "admin", "severity", "user_activity", "code", 2330, "ip", uploadClientIP(r), "user_id", r.PathValue("userID"))
	http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}

func (a *App) AdminUpdateUserPolicy(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
		return
	}
	policy := services.UserPolicy{
		MaxUploadMB:         optionalMB(r.FormValue("max_upload_mb")),
		DailyUploadMB:       optionalMB(r.FormValue("daily_upload_mb")),
		StorageQuotaMB:      optionalMBAllowZero(r.FormValue("storage_quota_mb")),
		MaxDays:             optionalIntAllowUnlimited(r.FormValue("max_days")),
		DailyBoxes:          optionalIntAllowUnlimited(r.FormValue("daily_boxes")),
		ActiveBoxes:         optionalIntAllowUnlimited(r.FormValue("active_boxes")),
		ShortWindowRequests: optionalIntAllowUnlimited(r.FormValue("short_window_requests")),
	}
	if backendID := r.FormValue("storage_backend_id"); backendID != "" {
		if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
			http.Error(w, "storage backend not found", http.StatusBadRequest)
			return
		}
		policy.StorageBackendID = &backendID
	}
	if err := a.authService.SetUserPolicy(r.PathValue("userID"), policy); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	a.logger.Info("admin updated user policy", "source", "admin", "severity", "user_activity", "code", 2331, "ip", uploadClientIP(r), "user_id", r.PathValue("userID"))
	http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}

func (a *App) AdminUpdateUser(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit", http.StatusSeeOther)
		return
	}
	policy := services.UserPolicy{
		MaxUploadMB:         optionalMB(r.FormValue("max_upload_mb")),
		DailyUploadMB:       optionalMB(r.FormValue("daily_upload_mb")),
		StorageQuotaMB:      optionalMBAllowZero(r.FormValue("storage_quota_mb")),
		MaxDays:             optionalIntAllowUnlimited(r.FormValue("max_days")),
		DailyBoxes:          optionalIntAllowUnlimited(r.FormValue("daily_boxes")),
		ActiveBoxes:         optionalIntAllowUnlimited(r.FormValue("active_boxes")),
		ShortWindowRequests: optionalIntAllowUnlimited(r.FormValue("short_window_requests")),
	}
	if backendID := r.FormValue("storage_backend_id"); backendID != "" {
		if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
			http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?error="+url.QueryEscape("storage backend not found"), http.StatusSeeOther)
			return
		}
		policy.StorageBackendID = &backendID
	}
	if _, err := a.authService.UpdateUserAdminFields(
		r.PathValue("userID"),
		r.FormValue("username"),
		r.FormValue("email"),
		r.FormValue("role"),
		r.FormValue("status"),
		policy,
	); err != nil {
		http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
		return
	}
	a.logger.Info("admin updated user", "source", "admin", "severity", "user_activity", "code", 2332, "ip", uploadClientIP(r), "user_id", r.PathValue("userID"))
	http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit", http.StatusSeeOther)
}

func (a *App) AdminUpdateUserStorage(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
		return
	}
	if backendID := r.FormValue("storage_backend_id"); backendID != "" {
		if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil {
			http.Error(w, "storage backend not found", http.StatusBadRequest)
			return
		}
	}
	if err := a.authService.SetUserStorageBackend(r.PathValue("userID"), r.FormValue("storage_backend_id")); err != nil {
		http.Error(w, "unable to update user storage", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin updated user storage", "source", "admin", "severity", "user_activity", "code", 2333, "ip", uploadClientIP(r), "user_id", r.PathValue("userID"), "backend_id", r.FormValue("storage_backend_id"))
	http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}

func (a *App) AdminCreateInvite(w http.ResponseWriter, r *http.Request) {
	admin, ok := a.requireAdminUser(w, r)
	if !ok || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
		return
	}
	result, err := a.authService.CreateInvite(r.FormValue("email"), r.FormValue("role"), admin.ID, 7*24*time.Hour)
	if err != nil {
		http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
		return
	}
	a.logger.Info("invite created", "source", "admin", "severity", "user_activity", "code", 2404, "admin_id", admin.ID, "ip", uploadClientIP(r), "email", r.FormValue("email"), "role", r.FormValue("role"))
	http.Redirect(w, r, "/admin/users?invite="+url.QueryEscape(result.URL), http.StatusSeeOther)
}

func (a *App) AdminDisableUser(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	disabled := r.URL.Query().Get("disabled") != "false"
	if err := a.authService.DisableUser(r.PathValue("userID"), disabled); err != nil {
		http.Error(w, "unable to update user", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin changed user disabled state", "source", "admin", "severity", "user_activity", "code", 2334, "ip", uploadClientIP(r), "user_id", r.PathValue("userID"), "disabled", disabled)
	http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
}

func (a *App) AdminResetUser(w http.ResponseWriter, r *http.Request) {
	admin, ok := a.requireAdminUser(w, r)
	if !ok || !a.validateCSRF(w, r) {
		return
	}
	result, err := a.authService.CreatePasswordResetInvite(r.PathValue("userID"), admin.ID)
	if err != nil {
		http.Error(w, "unable to create reset link", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin generated password reset", "source", "admin", "severity", "user_activity", "code", 2335, "ip", uploadClientIP(r), "admin_id", admin.ID, "user_id", r.PathValue("userID"))
	if r.URL.Query().Get("next") == "edit" {
		http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?invite="+url.QueryEscape(result.URL), http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/admin/users?invite="+url.QueryEscape(result.URL), http.StatusSeeOther)
}

func (a *App) AdminDeleteBox(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}

	boxID := r.PathValue("boxID")
	if err := a.uploadService.DeleteBox(boxID); err != nil {
		a.logger.Warn("admin delete failed", "source", "admin", "severity", "warn", "code", 4302, "box_id", boxID, "error", err.Error())
		http.Error(w, "unable to delete box", http.StatusInternalServerError)
		return
	}
	a.logger.Info("admin deleted box", "source", "admin", "severity", "user_activity", "code", 2304, "ip", uploadClientIP(r), "box_id", boxID)
	http.Redirect(w, r, "/admin/files", http.StatusSeeOther)
}

func (a *App) AdminViewBox(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}

	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if a.uploadService.IsProtected(box) {
		http.SetCookie(w, &http.Cookie{
			Name:     unlockCookieName(box.ID),
			Value:    a.uploadService.UnlockToken(box),
			Path:     "/d/" + box.ID,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
			Secure:   r.TLS != nil,
			Expires:  box.ExpiresAt,
		})
		a.logger.Info("admin bypassed box password", "source", "admin", "severity", "user_activity", "code", 2302, "box_id", box.ID)
	}

	http.Redirect(w, r, "/d/"+box.ID, http.StatusSeeOther)
}

func (a *App) renderAdminLogin(w http.ResponseWriter, r *http.Request, status int, message string) {
	a.renderPage(w, r, status, "admin_login.html", web.PageData{
		Title:       "Admin login",
		Description: "Sign in to the Warpbox admin console.",
		Data: adminPageData{
			Error: message,
		},
	})
}

func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
	if a.isAdmin(r) {
		return true
	}
	http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
	return false
}

func (a *App) isAdmin(r *http.Request) bool {
	if user, ok := a.currentUser(r); ok && user.Role == services.UserRoleAdmin {
		return true
	}
	if a.cfg.AdminToken == "" {
		return false
	}
	cookie, err := r.Cookie(adminCookieName)
	if err != nil {
		return false
	}
	return cookie.Value == adminCookieValue(a.cfg.AdminToken)
}

func (a *App) requireAdminUser(w http.ResponseWriter, r *http.Request) (services.User, bool) {
	user, ok := a.currentUser(r)
	if ok && user.Role == services.UserRoleAdmin {
		return user, true
	}
	if a.cfg.AdminToken != "" && a.isAdmin(r) {
		return services.User{ID: "env-admin", Role: services.UserRoleAdmin, Status: services.UserStatusActive}, true
	}
	http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther)
	return services.User{}, false
}

func (a *App) currentPublicUser(r *http.Request) any {
	if user, ok := a.currentUser(r); ok {
		return a.authService.PublicUser(user)
	}
	return nil
}

func adminCookieValue(token string) string {
	sum := sha256.Sum256([]byte("warpbox-admin:" + token))
	return hex.EncodeToString(sum[:])
}

func parsePositiveInt(value string) int {
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return 0
	}
	return parsed
}

func parsePositiveFloat(value string) float64 {
	parsed, err := strconv.ParseFloat(value, 64)
	if err != nil {
		return 0
	}
	return parsed
}

func optionalMB(value string) *float64 {
	if value == "" {
		return nil
	}
	parsed, err := services.ParseMegabytesLimitValue(value)
	if err != nil {
		return nil
	}
	return &parsed
}

func optionalMBAllowZero(value string) *float64 {
	if value == "" {
		return nil
	}
	parsed, err := strconv.ParseFloat(value, 64)
	// 0 and -1 both mean unlimited; reject other negatives.
	if err != nil || (parsed < 0 && parsed != -1) {
		return nil
	}
	return &parsed
}

func optionalInt(value string) *int {
	if value == "" {
		return nil
	}
	parsed, err := strconv.Atoi(value)
	if err != nil || parsed <= 0 {
		return nil
	}
	return &parsed
}

// optionalIntAllowUnlimited is like optionalInt but also accepts -1 (unlimited).
func optionalIntAllowUnlimited(value string) *int {
	if value == "" {
		return nil
	}
	parsed, err := strconv.Atoi(value)
	if err != nil || (parsed <= 0 && parsed != -1) {
		return nil
	}
	return &parsed
}

func formatMB(value float64) string {
	return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
}

func (a *App) adminBansView(r *http.Request) (adminBansView, error) {
	settings, err := a.banService.Settings()
	if err != nil {
		return adminBansView{}, err
	}
	records, err := a.banService.ListBans()
	if err != nil {
		return adminBansView{}, err
	}
	rules, err := a.banService.ListRules()
	if err != nil {
		return adminBansView{}, err
	}
	now := time.Now().UTC()
	query := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
	statusFilter := strings.TrimSpace(r.URL.Query().Get("status"))
	rows := []adminBanView{}
	active, expired, unbanned := 0, 0, 0
	for _, record := range records {
		status := record.Status(now)
		switch status {
		case "active":
			active++
		case "expired":
			expired++
		case "unbanned":
			unbanned++
		}
		if statusFilter != "" && status != statusFilter {
			continue
		}
		search := strings.ToLower(strings.Join([]string{record.Target, record.Normalized, record.Reason, record.Source, status}, " "))
		if query != "" && !strings.Contains(search, query) {
			continue
		}
		rows = append(rows, adminBanView{
			ID:          record.ID,
			Target:      record.Normalized,
			Reason:      record.Reason,
			Source:      record.Source,
			Status:      status,
			CreatedAt:   record.CreatedAt.Format("Jan 2 15:04"),
			ExpiresAt:   record.ExpiresAt.Format("Jan 2 15:04"),
			LastMatched: formatOptionalTime(record.LastMatchedAt),
		})
	}
	return adminBansView{
		Bans:          rows,
		Rules:         rules,
		Settings:      settings,
		Query:         r.URL.Query().Get("q"),
		Status:        statusFilter,
		ActiveCount:   active,
		ExpiredCount:  expired,
		UnbannedCount: unbanned,
		RulePatterns:  joinRulePatterns(rules),
		Notice:        r.URL.Query().Get("notice"),
		Error:         r.URL.Query().Get("error"),
	}, nil
}

func formatOptionalTime(value *time.Time) string {
	if value == nil {
		return "Never"
	}
	return value.Format("Jan 2 15:04")
}

func joinRulePatterns(rules []services.BanRule) string {
	patterns := make([]string, 0, len(rules))
	for _, rule := range rules {
		if rule.Enabled {
			patterns = append(patterns, rule.Pattern)
		}
	}
	return strings.Join(patterns, "\n")
}

func splitRulePatterns(value string) []string {
	lines := strings.Split(value, "\n")
	patterns := make([]string, 0, len(lines))
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line != "" {
			patterns = append(patterns, line)
		}
	}
	return patterns
}

func (a *App) adminLogsView(r *http.Request) (adminLogsView, error) {
	logDir := filepath.Join(a.cfg.DataDir, "logs")
	dates, err := availableLogDates(logDir)
	if err != nil {
		return adminLogsView{}, err
	}
	selectedDate := strings.TrimSpace(r.URL.Query().Get("date"))
	if selectedDate == "" && len(dates) > 0 {
		selectedDate = dates[0]
	} else if selectedDate != "" && selectedDate != "all" && !containsString(dates, selectedDate) {
		selectedDate = ""
	}
	severity := strings.TrimSpace(r.URL.Query().Get("severity"))
	source := strings.TrimSpace(r.URL.Query().Get("source"))
	query := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
	sortOrder := strings.TrimSpace(r.URL.Query().Get("sort"))
	if sortOrder != "asc" {
		sortOrder = "desc"
	}

	files := []string{}
	if selectedDate == "all" {
		for _, date := range dates {
			files = append(files, filepath.Join(logDir, date+".log"))
		}
	} else if selectedDate != "" {
		files = append(files, filepath.Join(logDir, selectedDate+".log"))
	}

	entries := []adminLogEntry{}
	for _, file := range files {
		fileEntries, err := readLogEntries(file)
		if err != nil && !os.IsNotExist(err) {
			return adminLogsView{}, err
		}
		for _, entry := range fileEntries {
			if severity != "" && entry.Severity != severity {
				continue
			}
			if source != "" && entry.Source != source {
				continue
			}
			if query != "" && !strings.Contains(strings.ToLower(entry.searchText()), query) {
				continue
			}
			entries = append(entries, entry)
		}
	}

	sort.Slice(entries, func(i, j int) bool {
		left := entries[i].Date + " " + entries[i].Time
		right := entries[j].Date + " " + entries[j].Time
		if sortOrder == "asc" {
			return left < right
		}
		return left > right
	})

	perPage := normalizePageSize(r.URL.Query().Get("per"), adminLogsDefaultPageSize, adminLogsPageSizes)
	total := len(entries)
	totalPages := (total + perPage - 1) / perPage
	if totalPages < 1 {
		totalPages = 1
	}
	page := 1
	if parsed, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && parsed > 1 {
		page = parsed
	}
	if page > totalPages {
		page = totalPages
	}
	start := (page - 1) * perPage
	if start > total {
		start = total
	}
	end := start + perPage
	if end > total {
		end = total
	}
	rangeFrom := 0
	if total > 0 {
		rangeFrom = start + 1
	}

	state := adminLogsQuery{
		Date:     selectedDate,
		Severity: severity,
		Source:   source,
		Query:    r.URL.Query().Get("q"),
		Sort:     sortOrder,
		Per:      perPage,
	}
	links := make([]adminFilesPageLink, 0, 5)
	for p := page - 2; p <= page+2; p++ {
		if p < 1 || p > totalPages {
			continue
		}
		links = append(links, adminFilesPageLink{Page: p, Href: adminLogsHref(state, p), Active: p == page})
	}

	return adminLogsView{
		Entries:        entries[start:end],
		Dates:          dates,
		Date:           selectedDate,
		Severity:       severity,
		Source:         source,
		Query:          r.URL.Query().Get("q"),
		Sort:           sortOrder,
		TotalShown:     end - start,
		Total:          total,
		Page:           page,
		PerPage:        perPage,
		PerPageOptions: adminLogsPageSizes,
		TotalPages:     totalPages,
		RangeFrom:      rangeFrom,
		RangeTo:        end,
		PageLinks:      links,
		HasPrev:        page > 1,
		HasNext:        page < totalPages,
		PrevHref:       adminLogsHref(state, page-1),
		NextHref:       adminLogsHref(state, page+1),
	}, nil
}

type adminLogsQuery struct {
	Date     string
	Severity string
	Source   string
	Query    string
	Sort     string
	Per      int
}

func adminLogsHref(state adminLogsQuery, page int) string {
	values := url.Values{}
	if state.Date != "" {
		values.Set("date", state.Date)
	}
	if state.Severity != "" {
		values.Set("severity", state.Severity)
	}
	if state.Source != "" {
		values.Set("source", state.Source)
	}
	if state.Query != "" {
		values.Set("q", state.Query)
	}
	if state.Sort != "" && state.Sort != "desc" {
		values.Set("sort", state.Sort)
	}
	if state.Per > 0 && state.Per != adminLogsDefaultPageSize {
		values.Set("per", strconv.Itoa(state.Per))
	}
	if page > 1 {
		values.Set("page", strconv.Itoa(page))
	}
	if len(values) == 0 {
		return "/admin/logs"
	}
	return "/admin/logs?" + values.Encode()
}

func availableLogDates(logDir string) ([]string, error) {
	matches, err := filepath.Glob(filepath.Join(logDir, "*.log"))
	if err != nil {
		return nil, err
	}
	dates := make([]string, 0, len(matches))
	for _, match := range matches {
		name := strings.TrimSuffix(filepath.Base(match), ".log")
		if name != "" {
			dates = append(dates, name)
		}
	}
	sort.Sort(sort.Reverse(sort.StringSlice(dates)))
	return dates, nil
}

func containsString(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func readLogEntries(file string) ([]adminLogEntry, error) {
	handle, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer handle.Close()
	scanner := bufio.NewScanner(handle)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	entries := []adminLogEntry{}
	for scanner.Scan() {
		line := scanner.Bytes()
		var raw map[string]any
		if err := json.Unmarshal(line, &raw); err != nil {
			continue
		}
		if isHealthCheckLogEntry(raw) {
			continue
		}
		entries = append(entries, logEntryFromMap(raw))
	}
	return entries, scanner.Err()
}

func isHealthCheckLogEntry(raw map[string]any) bool {
	path := strings.TrimSpace(firstLogString(raw, "path", "route"))
	if path == "" {
		return false
	}
	fields := strings.Fields(path)
	if len(fields) > 0 {
		path = fields[len(fields)-1]
	}
	if idx := strings.IndexByte(path, '?'); idx >= 0 {
		path = path[:idx]
	}
	return path == "/health"
}

func logEntryFromMap(raw map[string]any) adminLogEntry {
	entry := adminLogEntry{
		Date:     logString(raw, "date"),
		Time:     logString(raw, "time"),
		Source:   logString(raw, "source"),
		Severity: logString(raw, "severity"),
		Code:     logAnyString(raw["code"]),
		Message:  logString(raw, "log"),
		Method:   logString(raw, "method"),
		Path:     logString(raw, "path"),
		Status:   logAnyString(raw["status"]),
		IP:       services.IPOnly(firstLogString(raw, "ip", "client_ip", "remote_addr")),
		UserID:   logString(raw, "user_id"),
	}
	entry.Details = logDetails(raw)
	return entry
}

func logDetails(raw map[string]any) string {
	ignore := map[string]bool{
		"date": true, "time": true, "source": true, "severity": true, "code": true, "log": true,
		"method": true, "path": true, "status": true, "ip": true, "client_ip": true, "remote_addr": true, "user_id": true,
	}
	details := map[string]any{}
	for key, value := range raw {
		if !ignore[key] {
			details[key] = value
		}
	}
	if len(details) == 0 {
		return ""
	}
	data, err := json.Marshal(details)
	if err != nil {
		return ""
	}
	return string(data)
}

func (e adminLogEntry) searchText() string {
	return strings.Join([]string{e.Date, e.Time, e.Source, e.Severity, e.Code, e.Message, e.Method, e.Path, e.Status, e.IP, e.UserID, e.Details}, " ")
}

func logString(raw map[string]any, key string) string {
	return logAnyString(raw[key])
}

func firstLogString(raw map[string]any, keys ...string) string {
	for _, key := range keys {
		if value := logString(raw, key); value != "" {
			return value
		}
	}
	return ""
}

func logAnyString(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	case float64:
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case bool:
		return strconv.FormatBool(typed)
	case nil:
		return ""
	default:
		return fmt.Sprintf("%v", typed)
	}
}

func adminStorageSpeedTestsJSON(tests []services.StorageSpeedTest) []adminStorageSpeedTestJSON {
	rows := make([]adminStorageSpeedTestJSON, 0, len(tests))
	for _, test := range tests {
		rows = append(rows, adminStorageSpeedTestJSON{
			ID:            test.ID,
			Mode:          test.Mode,
			ModeLabel:     test.ModeLabel(),
			Status:        test.Status,
			Stage:         test.Stage,
			Progress:      test.ProgressPercent,
			CustomLabel:   storageSpeedCustomLabel(test),
			StartedLabel:  test.StartedLabel(),
			FinishedLabel: test.FinishedLabel(),
			Files:         test.FilesWritten,
			SizeLabel:     test.TotalSizeLabel(),
			WriteSpeed:    test.WriteSpeedLabel(),
			ReadSpeed:     test.ReadSpeedLabel(),
			Error:         test.Error,
		})
	}
	return rows
}

func storageSpeedCustomLabel(test services.StorageSpeedTest) string {
	if test.Mode != services.StorageSpeedModeCustom {
		return ""
	}
	return fmt.Sprintf("%d files × %s each", test.CustomFileCount, services.FormatMegabytesLabel(test.CustomFileSizeMB))
}

func (a *App) storageConfigFromForm(r *http.Request, provider string) services.StorageBackendConfig {
	cfg := services.StorageBackendConfig{
		Provider: provider,
		Name:     r.FormValue("name"),
	}
	switch provider {
	case services.StorageProviderSFTP:
		cfg.Host = r.FormValue("host")
		cfg.Port = parsePositiveInt(r.FormValue("port"))
		cfg.Username = r.FormValue("username")
		cfg.Password = r.FormValue("password")
		cfg.PrivateKey = r.FormValue("private_key")
		cfg.HostKey = r.FormValue("host_key")
		cfg.RemotePath = r.FormValue("remote_path")
	case services.StorageProviderSMB:
		cfg.Host = r.FormValue("host")
		cfg.Port = parsePositiveInt(r.FormValue("port"))
		cfg.Share = r.FormValue("share")
		cfg.Domain = r.FormValue("domain")
		cfg.Username = r.FormValue("username")
		cfg.Password = r.FormValue("password")
		cfg.RemotePath = r.FormValue("remote_path")
	case services.StorageProviderWebDAV:
		cfg.Endpoint = r.FormValue("endpoint")
		cfg.Username = r.FormValue("username")
		cfg.Password = r.FormValue("password")
		cfg.RemotePath = r.FormValue("remote_path")
	case services.StorageProviderContabo:
		cfg.Endpoint = r.FormValue("endpoint")
		cfg.Region = r.FormValue("region")
		cfg.Bucket = r.FormValue("bucket")
		cfg.AccessKey = r.FormValue("access_key")
		cfg.SecretKey = r.FormValue("secret_key")
		cfg.UseSSL = true
		cfg.PathStyle = true
	default:
		cfg.Endpoint = r.FormValue("endpoint")
		cfg.Region = r.FormValue("region")
		cfg.Bucket = r.FormValue("bucket")
		cfg.AccessKey = r.FormValue("access_key")
		cfg.SecretKey = r.FormValue("secret_key")
		cfg.UseSSL = r.FormValue("use_ssl") == "on"
		cfg.PathStyle = r.FormValue("path_style") == "on"
	}
	return cfg
}

func adminStorageProviderOptions() []adminStorageProviderView {
	return []adminStorageProviderView{
		{Provider: services.StorageProviderS3, Label: "S3 Bucket", Description: "Generic S3-compatible object storage.", Icon: "cloud"},
		{Provider: services.StorageProviderContabo, Label: "Contabo Object Storage", Description: "Contabo COS with TLS and path-style lookup locked on.", Icon: "cloud"},
		{Provider: services.StorageProviderSFTP, Label: "SFTP", Description: "SSH file transfer to a server or NAS.", Icon: "database"},
		{Provider: services.StorageProviderSMB, Label: "Samba / SMB", Description: "Windows share or network attached storage.", Icon: "folder"},
		{Provider: services.StorageProviderWebDAV, Label: "WebDAV", Description: "Nextcloud, ownCloud, or any WebDAV endpoint.", Icon: "sync"},
	}
}

func normalizeAdminStorageProvider(provider string) string {
	switch provider {
	case services.StorageProviderContabo:
		return services.StorageProviderContabo
	case services.StorageProviderSFTP:
		return services.StorageProviderSFTP
	case services.StorageProviderSMB:
		return services.StorageProviderSMB
	case services.StorageProviderWebDAV:
		return services.StorageProviderWebDAV
	default:
		return services.StorageProviderS3
	}
}

func adminStorageProviderFromRequest(r *http.Request) string {
	if provider := r.PathValue("provider"); provider != "" {
		return provider
	}
	return path.Base(r.URL.Path)
}

func validAdminStorageProvider(provider string) bool {
	switch provider {
	case services.StorageProviderS3, services.StorageProviderContabo, services.StorageProviderSFTP, services.StorageProviderSMB, services.StorageProviderWebDAV:
		return true
	default:
		return false
	}
}

func adminStorageProviderLabel(provider string) string {
	switch normalizeAdminStorageProvider(provider) {
	case services.StorageProviderContabo:
		return "Contabo Object Storage"
	case services.StorageProviderSFTP:
		return "SFTP"
	case services.StorageProviderSMB:
		return "Samba / SMB"
	case services.StorageProviderWebDAV:
		return "WebDAV"
	default:
		return "S3 Bucket"
	}
}

func adminStorageTypeForProvider(provider string) string {
	switch normalizeAdminStorageProvider(provider) {
	case services.StorageProviderSFTP:
		return services.StorageBackendSFTP
	case services.StorageProviderSMB:
		return services.StorageBackendSMB
	case services.StorageProviderWebDAV:
		return services.StorageBackendWebDAV
	default:
		return services.StorageBackendS3
	}
}

func (a *App) storageBackendViews() ([]services.StorageBackendView, error) {
	configs, err := a.uploadService.Storage().ListBackendConfigs()
	if err != nil {
		return nil, err
	}
	views := make([]services.StorageBackendView, 0, len(configs))
	for _, cfg := range configs {
		var usage int64
		if backend, err := a.uploadService.Storage().BackendConfig(cfg.ID); err == nil && backend.Enabled {
			if concrete, err := a.uploadService.Storage().Backend(cfg.ID); err == nil {
				usage, _ = concrete.Usage(context.Background())
			}
		}
		inUse, inUseReason, _ := a.storageBackendUseReason(cfg.ID)
		speedTests, _ := a.uploadService.Storage().ListSpeedTests(cfg.ID, 25)
		views = append(views, services.StorageBackendView{
			Config:       cfg,
			UsageBytes:   usage,
			UsageLabel:   services.FormatMegabytesFromBytes(usage),
			InUse:        inUse,
			InUseReason:  inUseReason,
			SpeedTests:   speedTests,
			CanSpeedTest: cfg.LastTestSuccess,
		})
	}
	return views, nil
}

func (a *App) adminUserEdit(user services.User, settings services.UploadPolicySettings) (adminUserEditView, error) {
	storageUsed, err := a.uploadService.UserActiveStorageUsed(user.ID)
	if err != nil {
		return adminUserEditView{}, err
	}
	usage, err := a.settingsService.UsageForUser(user.ID, time.Now().UTC())
	if err != nil {
		return adminUserEditView{}, err
	}
	effective := a.settingsService.EffectivePolicyForUser(settings, user)
	view := adminUserEditView{
		ID:                   user.ID,
		Username:             user.Username,
		Email:                user.Email,
		Role:                 user.Role,
		Status:               user.Status,
		StorageUsed:          services.FormatMegabytesFromBytes(storageUsed),
		DailyUsed:            services.FormatMegabytesFromBytes(usage.UploadedBytes),
		EffectiveDaily:       services.FormatMegabytesLabel(effective.DailyUploadMB),
		EffectiveMaxDays:     effective.MaxDays,
		EffectiveDailyBoxes:  effective.DailyBoxes,
		EffectiveActiveBoxes: effective.ActiveBoxes,
		EffectiveBackend:     effective.StorageBackendID,
		MaxUploadMB:          floatPtrString(user.Policy.MaxUploadMB),
		DailyUploadMB:        floatPtrString(user.Policy.DailyUploadMB),
		StorageQuotaMB:       floatPtrString(user.Policy.StorageQuotaMB),
		MaxDays:              intPtrString(user.Policy.MaxDays),
		DailyBoxes:           intPtrString(user.Policy.DailyBoxes),
		ActiveBoxes:          intPtrString(user.Policy.ActiveBoxes),
		ShortWindowRequests:  intPtrString(user.Policy.ShortWindowRequests),
		StorageBackendID:     stringPtrString(user.Policy.StorageBackendID),
	}
	if effective.StorageQuotaSet {
		view.EffectiveStorage = services.FormatMegabytesLabel(effective.StorageQuotaMB)
	} else {
		view.EffectiveStorage = "unlimited"
	}
	return view, nil
}

func (a *App) storageBackendInUse(id string) (bool, error) {
	inUse, _, err := a.storageBackendUseReason(id)
	return inUse, err
}

func (a *App) storageBackendUseReason(id string) (bool, string, error) {
	settings, err := a.settingsService.UploadPolicy()
	if err != nil {
		return false, "", err
	}
	if settings.AnonymousStorageBackend == id {
		return true, "selected as the global anonymous storage backend", nil
	}
	if settings.UserStorageBackend == id {
		return true, "selected as the global user storage backend", nil
	}
	boxes, err := a.uploadService.ListBoxes(0)
	if err != nil {
		return false, "", err
	}
	for _, box := range boxes {
		if a.uploadService.BoxStorageBackendID(box) == id {
			return true, "used by existing boxes", nil
		}
	}
	users, err := a.authService.ListUsers()
	if err != nil {
		return false, "", err
	}
	for _, user := range users {
		if user.Policy.StorageBackendID != nil && *user.Policy.StorageBackendID == id {
			return true, "assigned to one or more users", nil
		}
	}
	return false, "", nil
}

func floatPtrString(value *float64) string {
	if value == nil {
		return ""
	}
	return strconv.FormatFloat(*value, 'f', -1, 64)
}

func intPtrString(value *int) string {
	if value == nil {
		return ""
	}
	return strconv.Itoa(*value)
}

func stringPtrString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
