package manage

import (
	"bytes"
	"encoding/json"
	"errors"
	"html/template"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"git.az-it.net/az/morz-infoboard/server/backend/internal/config"
	"git.az-it.net/az/morz-infoboard/server/backend/internal/fileutil"
	"git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier"
	"git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext"
	"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
)

// jsonSafe serializes v to a JSON string safe for inline use in a <script> block.
// It returns template.JS so the template engine does not HTML-escape it again.
func jsonSafe(v any) template.JS {
	b, err := json.Marshal(v)
	if err != nil {
		return template.JS("null")
	}
	return template.JS(b) //nolint:gosec
}

// renderTemplate rendert t mit data in einen Buffer und schreibt das Ergebnis erst
// dann in w, wenn kein Fehler aufgetreten ist. W7: Verhindert halb-gerendertes HTML.
func renderTemplate(w http.ResponseWriter, t *template.Template, data any) {
	var buf bytes.Buffer
	if err := t.Execute(&buf, data); err != nil {
		http.Error(w, "Interner Fehler (Template)", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	buf.WriteTo(w) //nolint:errcheck
}

// requireScreenAccess prüft, ob der eingeloggte User Zugriff auf den Screen hat.
// Admins dürfen alles. Tenant-User dürfen nur Screens ihres eigenen Tenants bearbeiten.
// Gibt true zurück wenn Zugriff erlaubt ist; schreibt 403 und gibt false zurück wenn nicht.
func requireScreenAccess(w http.ResponseWriter, r *http.Request, screen *store.Screen) bool {
	u := reqcontext.UserFromContext(r.Context())
	if u == nil {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return false
	}
	if u.Role == "admin" {
		return true
	}
	// Tenant-User: Screen muss zum eigenen Tenant gehören.
	// Wir vergleichen über TenantSlug→TenantID, aber der Screen hat TenantID.
	// Da uns der Tenant-Slug des Users bekannt ist und wir keinen TenantStore
	// hier haben, vergleichen wir TenantID des Screens mit dem user.TenantID-Feld.
	// store.User hat TenantSlug aber nicht TenantID — deswegen muss der
	// aufrufende Handler nach GetBySlug bereits die TenantID des Screens bekannt haben.
	// Wir nutzen u.TenantSlug und vertrauen darauf dass der Screen bereits geladen ist.
	// Den eigentlichen Vergleich machen wir via TenantID des Screens vs.
	// dem TenantID-Feld im User (das über reqcontext gespeichert ist).
	if u.TenantID != "" && u.TenantID != screen.TenantID {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return false
	}
	return true
}

var tmplFuncs = template.FuncMap{
	// screenUsersJSON serializes a []*store.User slice to JSON for inline JS.
	"screenUsersJSON": func(users []*store.User) template.JS {
		type entry struct {
			ID       string `json:"id"`
			Username string `json:"username"`
		}
		out := make([]entry, 0, len(users))
		for _, u := range users {
			out = append(out, entry{ID: u.ID, Username: u.Username})
		}
		return jsonSafe(out)
	},
	// screenUserMapJSON serializes map[string][]*store.ScreenUserEntry to JSON.
	"screenUserMapJSON": func(m map[string][]*store.ScreenUserEntry) template.JS {
		type entry struct {
			ID       string `json:"id"`
			Username string `json:"username"`
		}
		out := map[string][]entry{}
		for screenID, users := range m {
			entries := make([]entry, 0, len(users))
			for _, u := range users {
				entries = append(entries, entry{ID: u.ID, Username: u.Username})
			}
			out[screenID] = entries
		}
		return jsonSafe(out)
	},
	"typeIcon": func(t string) string {
		switch t {
		case "image":
			return "🖼"
		case "video":
			return "🎬"
		case "pdf":
			return "📄"
		case "web":
			return "🌐"
		default:
			return "📁"
		}
	},
	"orientationLabel": func(o string) string {
		if o == "portrait" {
			return "Hochformat"
		}
		return "Querformat"
	},
	"shortSrc": func(s string) string {
		if len(s) > 60 {
			return s[:57] + "..."
		}
		return s
	},
	"formatDT": func(t *time.Time) string {
		if t == nil {
			return ""
		}
		return t.Format("2006-01-02T15:04")
	},
	"formatDateDE": func(t *time.Time) string {
		if t == nil {
			return ""
		}
		return t.Format("02.01.2006 15:04")
	},
	"not_expired": func(t *time.Time) bool {
		return t != nil && time.Now().Before(*t)
	},
}

// HandleAdminUI renders the admin overview page (screens + users tabs).
func HandleAdminUI(tenants *store.TenantStore, screens *store.ScreenStore, auth *store.AuthStore, cfg config.Config) http.HandlerFunc {
	t := template.Must(template.New("admin").Funcs(tmplFuncs).Parse(adminTmpl))
	return func(w http.ResponseWriter, r *http.Request) {
		allScreens, err := screens.ListAll(r.Context())
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}
		allTenants, err := tenants.List(r.Context())
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		// Default tenant slug for user management.
		tenantSlug := "morz"
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenantSlug = u.TenantSlug
		}
		screenUsers, err := auth.ListScreenUsers(r.Context(), tenantSlug)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		// Build per-screen user lists for the modal.
		screenUserMap := map[string][]*store.ScreenUserEntry{}
		for _, sc := range allScreens {
			users, err := screens.GetScreenUsers(r.Context(), sc.ID)
			if err != nil {
				continue
			}
			screenUserMap[sc.ID] = users
		}

		activeTab := r.URL.Query().Get("tab")
		if activeTab == "" {
			activeTab = "screens"
		}

		// M2: ?screen= Parameter weitergeben damit das Template den Screen-Modal öffnen kann.
		autoOpenScreen := r.URL.Query().Get("screen")

		// M6: CSRF-Token an Template-Daten weitergeben.
		csrfToken := setCSRFCookie(w, r, cfg.DevMode)

		renderTemplate(w, t, map[string]any{
			"Screens":         allScreens,
			"Tenants":         allTenants,
			"ScreenUsers":     screenUsers,
			"ScreenUserMap":   screenUserMap,
			"ActiveTab":       activeTab,
			"AutoOpenScreen":  autoOpenScreen,
			"CSRFToken":       csrfToken,
		})
	}
}

// HandleCreateScreenUser creates a new screen user (role: screen_user or restricted) for the default tenant.
func HandleCreateScreenUser(auth *store.AuthStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad form", http.StatusBadRequest)
			return
		}
		username := strings.TrimSpace(r.FormValue("username"))
		password := r.FormValue("password")
		if username == "" || password == "" {
			http.Redirect(w, r, "/admin?tab=users&msg=error_empty", http.StatusSeeOther)
			return
		}

		role := r.FormValue("role")
		if role != "screen_user" && role != "restricted" {
			role = "screen_user"
		}

		tenantSlug := "morz"
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenantSlug = u.TenantSlug
		}

		_, err := auth.CreateScreenUser(r.Context(), tenantSlug, username, password, role)
		if err != nil {
			slog.Error("create screen user failed", "event", "create_screen_user_failed",
				"tenant_slug", tenantSlug, "username", username, "err", err)
			http.Redirect(w, r, "/admin?tab=users&msg=error_exists", http.StatusSeeOther)
			return
		}
		http.Redirect(w, r, "/admin?tab=users&msg=user_added", http.StatusSeeOther)
	}
}

// HandleDeleteScreenUser deletes a screen_user by ID.
func HandleDeleteScreenUser(auth *store.AuthStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		userID := r.PathValue("userID")
		if err := auth.DeleteUser(r.Context(), userID); err != nil {
			slog.Error("delete screen user failed", "event", "delete_screen_user_failed",
				"user_id", userID, "err", err)
			http.Redirect(w, r, "/admin?tab=users&msg=error_db", http.StatusSeeOther)
			return
		}
		http.Redirect(w, r, "/admin?tab=users&msg=user_deleted", http.StatusSeeOther)
	}
}

// HandleAddUserToScreen grants a user access to a specific screen.
func HandleAddUserToScreen(screens *store.ScreenStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenID := r.PathValue("screenID")
		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad form", http.StatusBadRequest)
			return
		}
		userID := strings.TrimSpace(r.FormValue("user_id"))
		if userID == "" {
			http.Redirect(w, r, "/admin?tab=users&screen="+screenID+"&msg=error_empty", http.StatusSeeOther)
			return
		}
		if err := screens.AddUserToScreen(r.Context(), userID, screenID); err != nil {
			slog.Error("add user to screen failed", "event", "add_user_to_screen_failed",
				"screen_id", screenID, "user_id", userID, "err", err)
			http.Redirect(w, r, "/admin?tab=users&screen="+screenID+"&msg=error_db", http.StatusSeeOther)
			return
		}
		http.Redirect(w, r, "/admin?tab=users&screen="+screenID+"&msg=user_added_to_screen", http.StatusSeeOther)
	}
}

// HandleRemoveUserFromScreen removes a user's access to a specific screen.
func HandleRemoveUserFromScreen(screens *store.ScreenStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenID := r.PathValue("screenID")
		userID := r.PathValue("userID")
		if err := screens.RemoveUserFromScreen(r.Context(), userID, screenID); err != nil {
			slog.Error("remove user from screen failed", "event", "remove_user_from_screen_failed",
				"screen_id", screenID, "user_id", userID, "err", err)
			http.Redirect(w, r, "/admin?tab=users&screen="+screenID+"&msg=error_db", http.StatusSeeOther)
			return
		}
		http.Redirect(w, r, "/admin?tab=users&screen="+screenID+"&msg=user_removed_from_screen", http.StatusSeeOther)
	}
}

type screenCard struct {
	Screen          *store.Screen
	DisplayState    string
	OverrideOnUntil *time.Time
}

// HandleScreenOverview renders a card-based overview of all accessible screens for a screen_user.
func HandleScreenOverview(screens *store.ScreenStore, schedules *store.ScreenScheduleStore, overrides *store.GlobalOverrideStore, notifier *mqttnotifier.Notifier, cfg config.Config) http.HandlerFunc {
	t := template.Must(template.New("screenOverview").Funcs(tmplFuncs).Parse(screenOverviewTmpl))
	return func(w http.ResponseWriter, r *http.Request) {
		u := reqcontext.UserFromContext(r.Context())
		if u == nil {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}
		var accessible []*store.Screen
		var err error
		if u.Role == "admin" {
			accessible, err = screens.ListAll(r.Context())
		} else {
			accessible, err = screens.GetAccessibleScreens(r.Context(), u.ID)
		}
		if err != nil || len(accessible) == 0 {
			http.Redirect(w, r, "/login?error=no_screens", http.StatusSeeOther)
			return
		}
		if len(accessible) == 1 {
			http.Redirect(w, r, "/manage/"+accessible[0].Slug, http.StatusSeeOther)
			return
		}
		for _, sc := range accessible {
			notifier.RequestScreenshot(sc.Slug)
		}
		csrfToken := setCSRFCookie(w, r, cfg.DevMode)
		cards := make([]screenCard, 0, len(accessible))
		for _, sc := range accessible {
			ds, _ := screens.GetDisplayState(r.Context(), sc.ID)
			sched, _ := schedules.Get(r.Context(), sc.ID)
			var overrideOnUntil *time.Time
			if sched != nil && sched.OverrideOnUntil != nil && time.Now().Before(*sched.OverrideOnUntil) {
				overrideOnUntil = sched.OverrideOnUntil
			}
			cards = append(cards, screenCard{Screen: sc, DisplayState: ds, OverrideOnUntil: overrideOnUntil})
		}

		var activeOverride *store.GlobalOverride
		if o, err := overrides.Get(r.Context()); err == nil && o != nil && time.Now().Before(o.Until) {
			activeOverride = o
		}

		renderTemplate(w, t, map[string]any{
			"Cards":          cards,
			"CSRFToken":      csrfToken,
			"GlobalOverride": activeOverride,
			"UserRole":       u.Role,
		})
	}
}

// HandleManageUI renders the playlist management UI for a specific screen.
func HandleManageUI(
	tenants *store.TenantStore,
	screens *store.ScreenStore,
	schedules *store.ScreenScheduleStore,
	media *store.MediaStore,
	playlists *store.PlaylistStore,
	cfg config.Config,
	notifier *mqttnotifier.Notifier,
) http.HandlerFunc {
	t := template.Must(template.New("manage").Funcs(tmplFuncs).Parse(manageTmpl))
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")

		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "Screen nicht gefunden: "+screenSlug, http.StatusNotFound)
			return
		}

		notifier.RequestScreenshot(screen.Slug)

		// K2: Tenant-Isolation — nur eigener Tenant oder Admin.
		if !requireScreenAccess(w, r, screen) {
			return
		}

		var tenant *store.Tenant
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenant, _ = tenants.Get(r.Context(), u.TenantSlug)
		}
		if tenant == nil {
			tenant = &store.Tenant{ID: screen.TenantID, Name: "MORZ"}
		}

		playlist, err := playlists.GetOrCreateForScreen(r.Context(), screen.TenantID, screen.ID, screen.Name)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		items, err := playlists.ListItems(r.Context(), playlist.ID)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		assets, err := media.List(r.Context(), screen.TenantID)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		// Build set of already-added asset IDs to mark them in library.
		addedAssets := map[string]bool{}
		for _, it := range items {
			if it.MediaAssetID != "" {
				addedAssets[it.MediaAssetID] = true
			}
		}

		// M6: CSRF-Token an Template-Daten weitergeben.
		csrfToken := setCSRFCookie(w, r, cfg.DevMode)

		displayState, _ := screens.GetDisplayState(r.Context(), screen.ID)

		schedule, _ := schedules.Get(r.Context(), screen.ID)
		if schedule == nil {
			schedule = &store.ScreenSchedule{ScreenID: screen.ID}
		}

		// Determine back-navigation based on ?from= query parameter.
		backLink := "/admin"
		backLabel := "← Admin"
		if r.URL.Query().Get("from") == "tenant" {
			tenantSlug := ""
			if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
				tenantSlug = u.TenantSlug
			}
			if tenantSlug != "" {
				backLink = "/tenant/" + tenantSlug + "/dashboard"
				backLabel = "← Dashboard"
			}
		}

		isAdmin := false
		var accessibleScreens []*store.Screen
		if u := reqcontext.UserFromContext(r.Context()); u != nil {
			switch u.Role {
			case "admin":
				isAdmin = true
				accessibleScreens, _ = screens.ListAll(r.Context())
			case "screen_user", "restricted":
				accessibleScreens, _ = screens.GetAccessibleScreens(r.Context(), u.ID)
			default:
				// tenant_user und ähnliche Rollen: alle Screens des eigenen Tenants.
				if u.TenantID != "" {
					accessibleScreens, _ = screens.List(r.Context(), u.TenantID)
				}
			}
		}

		// M5: Server-Timezone ermitteln — bevorzugt aus TZ-Env-Variable, sonst aus der
		// lokalen Zeit-Location des Servers.
		serverTimezone := os.Getenv("TZ")
		if serverTimezone == "" {
			serverTimezone = time.Now().Location().String()
		}

		userRole := ""
		if u := reqcontext.UserFromContext(r.Context()); u != nil {
			userRole = u.Role
		}

		renderTemplate(w, t, map[string]any{
			"Screen":            screen,
			"Tenant":            tenant,
			"Playlist":          playlist,
			"Items":             items,
			"Assets":            assets,
			"AddedAssets":       addedAssets,
			"BackLink":          backLink,
			"BackLabel":         backLabel,
			"IsAdmin":           isAdmin,
			"AccessibleScreens": accessibleScreens,
			"ServerTimezone":    serverTimezone,
			"CSRFToken":         csrfToken,
			"DisplayState":      displayState,
			"Schedule":          schedule,
			"UserRole":          userRole,
		})
	}
}

// HandleCreateScreenUI handles form POST to create a screen, then redirects.
func HandleCreateScreenUI(tenants *store.TenantStore, screens *store.ScreenStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Redirect(w, r, "/admin?tab=screens&msg=error_bad_form", http.StatusSeeOther)
			return
		}
		slug := strings.TrimSpace(r.FormValue("slug"))
		name := strings.TrimSpace(r.FormValue("name"))
		orientation := r.FormValue("orientation")
		if slug == "" || name == "" {
			http.Redirect(w, r, "/admin?tab=screens&msg=error_empty", http.StatusSeeOther)
			return
		}
		if orientation == "" {
			orientation = "landscape"
		}

		tenantSlug := "morz"
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenantSlug = u.TenantSlug
		}
		tenant, err := tenants.Get(r.Context(), tenantSlug)
		if err != nil {
			slog.Error("create screen: tenant not found", "event", "create_screen_tenant_not_found",
				"tenant_slug", tenantSlug, "err", err)
			http.Redirect(w, r, "/admin?tab=screens&msg=error_tenant", http.StatusSeeOther)
			return
		}

		_, err = screens.Create(r.Context(), tenant.ID, slug, name, orientation)
		if err != nil {
			slog.Error("create screen failed", "event", "create_screen_failed",
				"tenant_slug", tenantSlug, "slug", slug, "err", err)
			http.Redirect(w, r, "/admin?tab=screens&msg=error_exists", http.StatusSeeOther)
			return
		}
		http.Redirect(w, r, "/admin?tab=screens&msg=added", http.StatusSeeOther)
	}
}

// HandleProvisionUI creates a screen in DB and shows the Ansible setup instructions.
func HandleProvisionUI(tenants *store.TenantStore, screens *store.ScreenStore) http.HandlerFunc {
	t := template.Must(template.New("provision").Funcs(tmplFuncs).Parse(provisionTmpl))
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Redirect(w, r, "/admin?tab=screens&msg=error_bad_form", http.StatusSeeOther)
			return
		}
		slug := strings.TrimSpace(r.FormValue("slug"))
		name := strings.TrimSpace(r.FormValue("name"))
		ip := strings.TrimSpace(r.FormValue("ip"))
		sshUser := strings.TrimSpace(r.FormValue("ssh_user"))
		orientation := r.FormValue("orientation")

		if slug == "" || ip == "" {
			http.Redirect(w, r, "/admin?tab=screens&msg=error_empty", http.StatusSeeOther)
			return
		}
		if name == "" {
			name = slug
		}
		if sshUser == "" {
			sshUser = "morz"
		}
		if orientation == "" {
			orientation = "landscape"
		}

		tenantSlug := "morz"
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenantSlug = u.TenantSlug
		}
		tenant, err := tenants.Get(r.Context(), tenantSlug)
		if err != nil {
			slog.Error("provision screen: tenant not found", "event", "provision_screen_tenant_not_found",
				"tenant_slug", tenantSlug, "err", err)
			http.Redirect(w, r, "/admin?tab=screens&msg=error_tenant", http.StatusSeeOther)
			return
		}

		screen, err := screens.Upsert(r.Context(), tenant.ID, slug, name, orientation)
		if err != nil {
			slog.Error("provision screen failed", "event", "provision_screen_failed",
				"tenant_slug", tenantSlug, "slug", slug, "err", err)
			http.Redirect(w, r, "/admin?tab=screens&msg=error_db", http.StatusSeeOther)
			return
		}

		renderTemplate(w, t, map[string]any{
			"Screen":      screen,
			"IP":          ip,
			"SSHUser":     sshUser,
			"Orientation": orientation,
		})
	}
}

// HandleDeleteScreenUI handles DELETE for a screen, then redirects.
func HandleDeleteScreenUI(screens *store.ScreenStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("screenId")
		if err := screens.Delete(r.Context(), id); err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, "/admin?msg=deleted", http.StatusSeeOther)
	}
}

// HandleUploadMediaUI handles form upload from the manage UI and redirects back.
func HandleUploadMediaUI(media *store.MediaStore, screens *store.ScreenStore, uploadDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")

		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}

		// K2: Tenant-Isolation.
		if !requireScreenAccess(w, r, screen) {
			return
		}

		// W3: MaxBytesReader begrenzt Uploads auf maxUploadSize.
		r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

		if err := r.ParseMultipartForm(maxUploadSize); err != nil {
			http.Error(w, "Upload zu groß oder ungültig", http.StatusBadRequest)
			return
		}

		assetType := strings.TrimSpace(r.FormValue("type"))
		title := strings.TrimSpace(r.FormValue("title"))

		// Bestimme tenantSlug für N6 (tenant-spezifisches Upload-Verzeichnis).
		tenantSlug := ""
		if u := reqcontext.UserFromContext(r.Context()); u != nil && u.TenantSlug != "" {
			tenantSlug = u.TenantSlug
		}
		if tenantSlug == "" {
			tenantSlug = "default"
		}

		switch assetType {
		case "web":
			url := strings.TrimSpace(r.FormValue("url"))
			if url == "" {
				http.Error(w, "URL erforderlich", http.StatusBadRequest)
				return
			}
			if title == "" {
				title = url
			}
			_, err = media.Create(r.Context(), screen.TenantID, title, "web", "", url, "", 0)
		case "image", "video", "pdf":
			file, header, ferr := r.FormFile("file")
			if ferr != nil {
				http.Error(w, "Datei erforderlich", http.StatusBadRequest)
				return
			}
			defer file.Close()
			if title == "" {
				title = strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename))
			}
			mimeType := header.Header.Get("Content-Type")
			// V1+N6: Gemeinsame Upload-Funktion, tenant-spezifisches Verzeichnis.
			storagePath, size, ferr := fileutil.SaveUploadedFile(file, header.Filename, title, uploadDir, tenantSlug)
			if ferr != nil {
				http.Error(w, "Speicherfehler", http.StatusInternalServerError)
				return
			}
			_, err = media.Create(r.Context(), screen.TenantID, title, assetType, storagePath, "", mimeType, size)
		default:
			http.Error(w, "Unbekannter Typ", http.StatusBadRequest)
			return
		}

		if err != nil {
			http.Error(w, "DB-Fehler", http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, "/manage/"+screenSlug+"?msg=uploaded", http.StatusSeeOther)
	}
}

// HandleAddItemUI handles form POST to add a playlist item, then redirects.
func HandleAddItemUI(playlists *store.PlaylistStore, media *store.MediaStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")
		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad form", http.StatusBadRequest)
			return
		}

		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}

		// K2: Tenant-Isolation.
		if !requireScreenAccess(w, r, screen) {
			return
		}

		playlist, err := playlists.GetOrCreateForScreen(r.Context(), screen.TenantID, screen.ID, screen.Name)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}

		mediaAssetID := r.FormValue("media_asset_id")
		itemType := r.FormValue("type")
		src := r.FormValue("src")
		title := r.FormValue("title")
		durationSeconds := 20
		if d, err := strconv.Atoi(strings.TrimSpace(r.FormValue("duration_seconds"))); err == nil && d > 0 {
			durationSeconds = d
		}
		validFrom, _ := parseOptionalTime(r.FormValue("valid_from"))
		validUntil, _ := parseOptionalTime(r.FormValue("valid_until"))

		if mediaAssetID != "" {
			asset, err := media.Get(r.Context(), mediaAssetID)
			if err != nil {
				http.Error(w, "Medium nicht gefunden", http.StatusBadRequest)
				return
			}
			itemType = asset.Type
			if asset.StoragePath != "" {
				src = asset.StoragePath
			} else {
				src = asset.OriginalURL
			}
			if title == "" {
				title = asset.Title
			}
		}

		if itemType == "" || src == "" {
			http.Error(w, "type und src erforderlich", http.StatusBadRequest)
			return
		}

		_, err = playlists.AddItem(r.Context(), playlist.ID, mediaAssetID,
			itemType, src, title, durationSeconds, validFrom, validUntil)
		if err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}
		notifier.NotifyChanged(screenSlug)
		http.Redirect(w, r, "/manage/"+screenSlug+"?msg=added", http.StatusSeeOther)
	}
}

// HandleDeleteItemUI removes a playlist item and redirects back.
func HandleDeleteItemUI(playlists *store.PlaylistStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")
		itemID := r.PathValue("itemId")

		// K2: Tenant-Isolation.
		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}
		if !requireScreenAccess(w, r, screen) {
			return
		}

		if err := playlists.DeleteItem(r.Context(), itemID); err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}
		notifier.NotifyChanged(screenSlug)
		http.Redirect(w, r, "/manage/"+screenSlug+"?msg=deleted", http.StatusSeeOther)
	}
}

// HandleReorderUI accepts JSON body with ordered IDs (HTMX/fetch).
func HandleReorderUI(playlists *store.PlaylistStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")
		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}
		// K2: Tenant-Isolation.
		if !requireScreenAccess(w, r, screen) {
			return
		}
		playlist, err := playlists.GetByScreen(r.Context(), screen.ID)
		if err != nil {
			http.Error(w, "playlist nicht gefunden", http.StatusNotFound)
			return
		}
		var ids []string
		if err := json.NewDecoder(r.Body).Decode(&ids); err != nil {
			http.Error(w, "JSON erwartet: array von item-IDs", http.StatusBadRequest)
			return
		}
		if err := playlists.Reorder(r.Context(), playlist.ID, ids); err != nil {
			if errors.Is(err, store.ErrReorderMismatch) {
				http.Error(w, "item list mismatch", http.StatusBadRequest)
			} else {
				slog.Error("reorder failed", "err", err)
				http.Error(w, "db error", http.StatusInternalServerError)
			}
			return
		}
		notifier.NotifyChanged(screenSlug)
		w.WriteHeader(http.StatusNoContent)
	}
}

// HandleUpdateItemUI handles form PATCH/POST to update a single item.
func HandleUpdateItemUI(playlists *store.PlaylistStore, screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")
		itemID := r.PathValue("itemId")

		// K2: Tenant-Isolation.
		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}
		if !requireScreenAccess(w, r, screen) {
			return
		}

		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad form", http.StatusBadRequest)
			return
		}
		title := r.FormValue("title")
		durationSeconds := 20
		if d, err := strconv.Atoi(strings.TrimSpace(r.FormValue("duration_seconds"))); err == nil && d > 0 {
			durationSeconds = d
		}
		enabled := r.FormValue("enabled") == "true"
		validFrom, _ := parseOptionalTime(r.FormValue("valid_from"))
		validUntil, _ := parseOptionalTime(r.FormValue("valid_until"))

		if err := playlists.UpdateItem(r.Context(), itemID, title, durationSeconds, enabled, validFrom, validUntil); err != nil {
			http.Error(w, "db error", http.StatusInternalServerError)
			return
		}
		notifier.NotifyChanged(screenSlug)
		if r.Header.Get("X-Requested-With") == "fetch" {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		http.Redirect(w, r, "/manage/"+screenSlug+"?msg=saved", http.StatusSeeOther)
	}
}

// HandleDeleteMediaUI deletes media and redirects back.
func HandleDeleteMediaUI(media *store.MediaStore, screens *store.ScreenStore, uploadDir string, notifier *mqttnotifier.Notifier) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		screenSlug := r.PathValue("screenSlug")
		mediaID := r.PathValue("mediaId")

		// K2: Tenant-Isolation.
		screen, err := screens.GetBySlug(r.Context(), screenSlug)
		if err != nil {
			http.Error(w, "screen nicht gefunden", http.StatusNotFound)
			return
		}
		if !requireScreenAccess(w, r, screen) {
			return
		}

		asset, err := media.Get(r.Context(), mediaID)
		if err == nil && asset.StoragePath != "" {
			os.Remove(filepath.Join(uploadDir, filepath.Base(asset.StoragePath))) //nolint:errcheck
		}
		media.Delete(r.Context(), mediaID) //nolint:errcheck

		notifier.NotifyChanged(screenSlug)
		http.Redirect(w, r, "/manage/"+screenSlug+"?msg=deleted", http.StatusSeeOther)
	}
}
