package web

import (
	"context"
	"fmt"
	"html/template"
	"io/fs"
	"net/http"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"

	"gitgud.io/mike/mpv-manager/internal/webassets"
	"gitgud.io/mike/mpv-manager/pkg/config"
	"gitgud.io/mike/mpv-manager/pkg/constants"
	"gitgud.io/mike/mpv-manager/pkg/hotkeys"
	"gitgud.io/mike/mpv-manager/pkg/installer"
	"gitgud.io/mike/mpv-manager/pkg/locale"
	"gitgud.io/mike/mpv-manager/pkg/log"
	"gitgud.io/mike/mpv-manager/pkg/platform"
	"gitgud.io/mike/mpv-manager/pkg/version"
)

// createAppCardData creates AppCardTemplateData for use in templates
func createAppCardData(app AppGroupInfo, platform *platform.Platform) AppCardTemplateData {
	return AppCardTemplateData{
		App:      app,
		Platform: platform,
	}
}

type Server struct {
	platform    *platform.Platform
	config      *config.Config
	installer   *installer.Installer
	releaseInfo *installer.ReleaseInfo
	templates   *template.Template
	sessions    *SessionManager
	sessionMux  sync.RWMutex
	Addr        string

	// HTTP server for graceful shutdown
	httpServer *http.Server

	// Job Manager for new job-based API (installs, updates, uninstalls)
	jobManager *JobManager

	// Version check cache
	versionCache    *VersionCache
	versionCacheMux sync.RWMutex
}

type VersionCache struct {
	LastChecked       time.Time
	UpdateMap         map[string]version.UpdateItem // methodID -> UpdateItem
	PackageVersionMap map[string]AppVersionInfo     // methodID -> {current, available}
}

type AppVersionInfo struct {
	CurrentVersion   string
	AvailableVersion string
}

// PackageVersionResult holds result of a package version check
type PackageVersionResult struct {
	MethodID         string
	CurrentVersion   string
	AvailableVersion string
	Found            bool // true if the package was found installed
}

func NewServer(p *platform.Platform, cfg *config.Config, inst *installer.Installer, releaseInfo *installer.ReleaseInfo) *Server {
	templatesFS, err := fs.Sub(webassets.TemplatesFS, "templates")
	if err != nil {
		log.Error("Failed to create templates filesystem: " + err.Error())
	}

	tmpl := template.New("")

	tmpl = tmpl.Funcs(template.FuncMap{
		"getCodecColor":           getCodecColor,
		"formatCodecName":         formatCodecName,
		"getCodecBadgeColor":      getCodecBadgeColor,
		"FormatLanguageDisplay":   FormatLanguageDisplay,
		"GetFlagDisplay":          GetFlagDisplay,
		"IsCommonLanguage":        IsCommonLanguage,
		"toJSON":                  toJSON,
		"toString":                toString,
		"getVendorColor":          getVendorColor,
		"getVendorClass":          getVendorClass,
		"getVendorDisplay":        getVendorDisplay,
		"isX86Architecture":       isX86Architecture,
		"isARMArchitecture":       isARMArchitecture,
		"getAppLogo":              getAppLogo,
		"getPackageTypeLogo":      getPackageTypeLogo,
		"getDistroLogo":           getDistroLogo,
		"getHWADescription":       platform.GetHwaDescription,
		"getHWABadgeColor":        getHWABadgeColor,
		"getHWABadgeText":         getHWABadgeText,
		"getGPUAPIDescription":    getGPUAPIDescription,
		"createAppCardData":       createAppCardData,
		"capitalize":              capitalize,
		"capitalizeArchitecture":  capitalizeArchitecture,
		"getOSLogo":               getOSLogo,
		"getOSDisplayName":        getOSDisplayName,
		"getDistroFamilyLogo":     getDistroFamilyLogo,
		"GetLanguageEntry":        GetLanguageEntry,
		"GetLanguageCountryCode":  GetLanguageCountryCode,
		"GetRegionalLanguageName": GetRegionalLanguageName,
		"GetRegionalNativeName":   GetRegionalNativeName,
		"cleanAppVersion":         cleanAppVersion,
		"categoryIcon":            categoryIcon,
		"formatKeys":              formatKeys,
		"canChangeUI":             canChangeUI,
	})

	tmpl, err = tmpl.ParseFS(templatesFS, "*.html")
	if err != nil {
		log.Error("Failed to parse HTML templates: " + err.Error())
		panic(fmt.Sprintf("Failed to parse HTML templates: %v", err))
	}

	server := &Server{
		platform:    p,
		config:      cfg,
		installer:   inst,
		releaseInfo: releaseInfo,
		templates:   tmpl,
		sessions:    NewSessionManager(),
		jobManager:  NewJobManager(),
	}

	// Initialize version cache at startup
	server.initVersionCache()

	return server
}

func (s *Server) ListenAndServe(addr string) error {
	s.Addr = addr

	staticFS, err := fs.Sub(webassets.StaticFS, "static")
	if err != nil {
		log.Error("Failed to create static filesystem: " + err.Error())
	}
	fileServer := http.FileServer(http.FS(staticFS))

	mux := http.NewServeMux()

	handler := securityMiddleware(corsMiddleware(mux))

	mux.HandleFunc("/", s.handleHome)
	mux.HandleFunc("/dashboard", s.handleDashboard)
	mux.HandleFunc("/apps", s.handleApps)
	mux.HandleFunc("/tasks", s.handleTasks)
	mux.HandleFunc("/hwaccel", s.handleHWAccel)
	mux.HandleFunc("/config", s.handleConfig)
	mux.HandleFunc("/settings", s.handleSettings)
	mux.HandleFunc("/system", s.handleSystem)
	mux.HandleFunc("/logs", s.handleLogs)
	mux.HandleFunc("/languages", s.handleLanguages)
	mux.HandleFunc("/hotkeys", s.handleHotkeys)

	mux.HandleFunc("/api/platform", s.handlePlatformAPI)
	mux.HandleFunc("/api/apps", s.handleAppsAPI)
	mux.HandleFunc("/api/install-methods", s.handleInstallMethodsAPI)
	mux.HandleFunc("/api/config/settings", s.handleConfigSettingsAPI)
	mux.HandleFunc("/api/config/backups", s.handleConfigBackupsAPI)
	mux.HandleFunc("/api/config/backups/list", s.handleConfigBackupsListAPI)
	mux.HandleFunc("/api/config/apply", s.handleConfigApplyAPI)
	mux.HandleFunc("/api/config/reset", s.handleConfigResetAPI)
	mux.HandleFunc("/api/config/restore", s.handleConfigRestoreAPI)
	mux.HandleFunc("/api/config/backup/delete", s.handleConfigBackupDeleteAPI)
	mux.HandleFunc("/api/install", s.handleInstallAPI)
	mux.HandleFunc("/api/install/stream", s.handleInstallStreamSSE)
	mux.HandleFunc("/api/uninstall", s.handleUninstallAPI)
	mux.HandleFunc("/api/file-associations/setup", s.handleFileAssociationsSetupAPI)
	mux.HandleFunc("/api/file-associations/remove", s.handleFileAssociationsRemoveAPI)
	mux.HandleFunc("/api/shortcuts/create", s.handleShortcutsCreateAPI)
	mux.HandleFunc("/api/logs", s.handleLogsAPI)
	mux.HandleFunc("/api/logs/clear", s.handleLogsClearAPI)
	mux.HandleFunc("/api/logs/download", s.handleLogsDownloadAPI)
	mux.HandleFunc("/api/logs/errors", s.handleRecentErrorsAPI)
	mux.HandleFunc("/api/apps/check-updates", s.handleCheckAppUpdatesAPI)
	mux.HandleFunc("/api/apps/refresh-installed", s.handleRefreshInstalledAppsAPI)
	mux.HandleFunc("/api/apps/update", s.handleAppUpdateAPI)
	mux.HandleFunc("/api/apps/adopt", s.handleAdoptAppAPI)
	mux.HandleFunc("/api/manager/update", s.handleManagerUpdateAPI)
	mux.HandleFunc("/api/languages/load", s.handleLanguagesLoadAPI)
	mux.HandleFunc("/api/languages/save", s.handleLanguagesSaveAPI)
	mux.HandleFunc("/api/languages/major", s.handleMajorLanguagesAPI)
	mux.HandleFunc("/api/languages/regional", s.handleRegionalVariantsAPI)
	mux.HandleFunc("/api/languages/regions", s.handleAllRegionsAPI)
	mux.HandleFunc("/api/modal", s.handleModalAPI)
	mux.HandleFunc("/api/shutdown", s.handleShutdownAPI)
	mux.HandleFunc("/api/server/status", s.handleServerStatusAPI)
	mux.HandleFunc("/api/settings", s.handleSettingsAPI)
	mux.HandleFunc("/api/settings/shortcut", s.handleSettingsShortcutAPI)
	mux.HandleFunc("/api/settings/reset", s.handleSettingsResetAPI)

	// Job API endpoints (new job-based system)
	mux.HandleFunc("/api/jobs", s.handleJobsListAPI)
	mux.HandleFunc("/api/jobs/stream", s.handleJobStreamSSE)
	mux.HandleFunc("/api/jobs/", s.handleJobRoutes) // Handles GET /{id} and DELETE /{id}

	// Job-based install/update/uninstall endpoints (new)
	mux.HandleFunc("/api/install/job", s.handleInstallJobAPI)
	mux.HandleFunc("/api/apps/update/job", s.handleAppUpdateJobAPI)
	mux.HandleFunc("/api/uninstall/job", s.handleUninstallJobAPI)

	// UI selection endpoints (for ModernZ UI support)
	mux.HandleFunc("/api/ui/options", s.handleUIOptionsAPI)
	mux.HandleFunc("/api/ui/change", s.handleUIChangeAPI)

	// Keyring endpoints (for sudo password storage)
	mux.HandleFunc("/api/keyring/status", s.handleKeyringStatusAPI)
	mux.HandleFunc("/api/keyring/auth", s.handleKeyringAuthRoutes) // Handles POST (store) and DELETE (clear)

	// Notice endpoint (for temporary UI banners)
	mux.HandleFunc("/api/notice", s.handleNoticeAPI)

	// Hotkey preset endpoints
	mux.HandleFunc("/api/hotkeys/presets", s.handleHotkeyPresetsAPI)
	mux.HandleFunc("/api/hotkeys/apply", s.handleHotkeyApplyPresetAPI)
	mux.HandleFunc("/api/hotkeys/reset", s.handleHotkeyResetAPI)
	mux.HandleFunc("/api/hotkeys/current", s.handleHotkeyCurrentAPI)

	mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
	mux.HandleFunc("/favicon.ico", s.handleFavicon)

	log.Info("Web server listening on " + addr)

	// Create and store http.Server for graceful shutdown
	s.httpServer = &http.Server{
		Addr:    addr,
		Handler: handler,
	}

	if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Error("Web server failed to start: " + err.Error())
		return err
	}
	return nil
}

// Shutdown gracefully shuts down the server
func (s *Server) Shutdown() {
	if s.httpServer != nil {
		log.Info("Shutting down HTTP server...")
		// Create a context with timeout for graceful shutdown
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		if err := s.httpServer.Shutdown(ctx); err != nil {
			log.Error("Error shutting down server: " + err.Error())
		}
	}
}

func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) {
	// Serve favicon.png from static directory
	data, err := webassets.StaticFS.ReadFile("static/favicon.png")
	if err != nil {
		log.Debug("Failed to read favicon: " + err.Error())
		http.NotFound(w, r)
		return
	}

	w.Header().Set("Content-Type", "image/png")
	w.Write(data)
}

func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, "/dashboard", http.StatusFound)
}

func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
	installedApps := config.GetInstalledApps()

	installerCheck := version.CheckForUpdate()
	managerUpdate := &ManagerUpdateInfo{
		CurrentVersion:  installerCheck.CurrentVersion,
		LatestVersion:   installerCheck.LatestVersion,
		UpdateAvailable: installerCheck.UpdateAvailable,
	}

	// Get client updates from cache
	s.versionCacheMux.RLock()
	var updateItems []version.UpdateItem
	updateMap := make(map[string]version.UpdateItem)
	for _, update := range s.versionCache.UpdateMap {
		updateItems = append(updateItems, update)
		updateMap[update.MethodID] = update
	}
	clientUpdates := s.convertClientUpdates(updateItems)
	s.versionCacheMux.RUnlock()

	// Group installed apps with logos and version info
	appGroups := s.groupInstalledAppsWithUpdates(installedApps, updateMap)

	data := DashboardData{
		Title:              "Dashboard - MPV.Rocks Manager",
		AppVersion:         version.CurrentVersion,
		Platform:           s.platform,
		InstalledApps:      installedApps,
		InstalledAppGroups: appGroups,
		ManagerUpdate:      managerUpdate,
		ClientUpdates:      clientUpdates,
		Config:             s.getConfigSettings(),
		Nav:                getActiveNav("/dashboard"),
		SelfUpdateDisabled: IsSelfUpdateDisabled(),
	}

	if err := s.templates.ExecuteTemplate(w, "dashboard", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) getConfigSettings() *ConfigSettings {
	audioLangs := config.GetConfigValue(constants.ConfigKeyAudioLanguage)
	subtitleLangs := config.GetConfigValue(constants.ConfigKeySubtitleLanguage)

	// Read hardware acceleration from mpv.conf instead of hardcoding
	hwaValue := getMPVConfigValue(constants.ConfigKeyHardwareDecoder)
	if hwaValue == "" {
		hwaValue = "auto" // Default if not configured
	}

	var audioLang, subtitleLang string
	if len(audioLangs) > 0 {
		audioLang = audioLangs[0]
	}
	if len(subtitleLangs) > 0 {
		subtitleLang = subtitleLangs[0]
	}

	// Read additional settings from mpv.conf
	videoOutputDriver := getMPVConfigValue(constants.ConfigKeyVideoOutput)
	videoScaleFilter := getMPVConfigValue(constants.ConfigKeyScaleFilter)
	ditherAlgorithm := getMPVConfigValue(constants.ConfigKeyDitherAlgorithm)
	savePositionOnQuit := getMPVConfigValue(constants.ConfigKeySavePositionOnQuit)
	border := getMPVConfigValue(constants.ConfigKeyBorder)
	if border == "" {
		border = "yes" // Default to native window decorations
	}
	profile := getMPVConfigValue(constants.ConfigKeyProfile)

	// Load screenshot settings
	screenshotFormat := getMPVConfigValue(constants.ConfigKeyScreenshotFormat)
	screenshotTagColorspace := getMPVConfigValue(constants.ConfigKeyScreenshotTagColorspace)
	screenshotHighBitDepth := getMPVConfigValue(constants.ConfigKeyScreenshotHighBitDepth)
	screenshotTemplate := getMPVConfigValue(constants.ConfigKeyScreenshotTemplate)
	screenshotDirectory := getMPVConfigValue(constants.ConfigKeyScreenshotDirectory)
	screenshotJpegQuality := getMPVConfigValue(constants.ConfigKeyScreenshotJpegQuality)
	screenshotPngCompression := getMPVConfigValue(constants.ConfigKeyScreenshotPngCompression)
	screenshotWebpLossless := getMPVConfigValue(constants.ConfigKeyScreenshotWebpLossless)
	screenshotWebpQuality := getMPVConfigValue(constants.ConfigKeyScreenshotWebpQuality)
	screenshotJxlDistance := getMPVConfigValue(constants.ConfigKeyScreenshotJxlDistance)
	screenshotAvifEncoder := getMPVConfigValue(constants.ConfigKeyScreenshotAvifEncoder)
	screenshotAvifPixfmt := getMPVConfigValue(constants.ConfigKeyScreenshotAvifPixfmt)
	screenshotAvifOpts := getMPVConfigValue(constants.ConfigKeyScreenshotAvifOpts)

	return &ConfigSettings{
		AudioLanguage:            audioLang,
		AudioLanguages:           audioLangs,
		SubtitleLanguage:         subtitleLang,
		SubtitleLanguages:        subtitleLangs,
		HardwareAccel:            hwaValue,
		VideoOutputDriver:        videoOutputDriver,
		VideoScaleFilter:         videoScaleFilter,
		DitherAlgorithm:          ditherAlgorithm,
		SavePositionOnQuit:       savePositionOnQuit,
		Border:                   border,
		Profile:                  profile,
		ConfigFile:               getConfigFilePath(),
		ScreenshotFormat:         screenshotFormat,
		ScreenshotTagColorspace:  screenshotTagColorspace,
		ScreenshotHighBitDepth:   screenshotHighBitDepth,
		ScreenshotTemplate:       screenshotTemplate,
		ScreenshotDirectory:      screenshotDirectory,
		ScreenshotJpegQuality:    screenshotJpegQuality,
		ScreenshotPngCompression: screenshotPngCompression,
		ScreenshotWebpLossless:   screenshotWebpLossless,
		ScreenshotWebpQuality:    screenshotWebpQuality,
		ScreenshotJxlDistance:    screenshotJxlDistance,
		ScreenshotAvifEncoder:    screenshotAvifEncoder,
		ScreenshotAvifPixfmt:     screenshotAvifPixfmt,
		ScreenshotAvifOpts:       screenshotAvifOpts,
	}
}

func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
	installedApps := config.GetInstalledApps()

	installMethods := s.getInstallMethods()

	// Filter out already installed methods
	availableMethods := filterInstalledMethods(installMethods, installedApps)

	// Get update map and client updates from cache
	s.versionCacheMux.RLock()
	var updateItems []version.UpdateItem
	updateMap := make(map[string]version.UpdateItem)
	for _, update := range s.versionCache.UpdateMap {
		updateItems = append(updateItems, update)
		updateMap[update.MethodID] = update
	}
	clientUpdates := s.convertClientUpdates(updateItems)
	s.versionCacheMux.RUnlock()

	appGroups := s.groupInstalledAppsWithUpdates(installedApps, updateMap)

	data := DashboardData{
		Title:              "MPV Player Apps - MPV.Rocks Manager",
		AppVersion:         version.CurrentVersion,
		Platform:           s.platform,
		InstalledApps:      installedApps,
		InstallMethods:     availableMethods,
		InstalledAppGroups: appGroups,
		ClientUpdates:      clientUpdates,
		Nav:                getActiveNav("/apps"),
	}

	if err := s.templates.ExecuteTemplate(w, "apps", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// filterInstalledMethods removes install methods that are already installed
func filterInstalledMethods(methods []installer.InstallMethod, installedApps []config.InstalledApp) []installer.InstallMethod {
	var filtered []installer.InstallMethod

	// Create a set of installed method IDs
	installedMethodIDs := make(map[string]bool)
	for _, app := range installedApps {
		installedMethodIDs[app.InstallMethod] = true
	}

	// Filter out installed methods
	for _, method := range methods {
		if !installedMethodIDs[method.ID] {
			filtered = append(filtered, method)
		}
	}

	return filtered
}

func (s *Server) getInstallMethods() []installer.InstallMethod {
	// Create installation handler for the current platform
	handler := installer.NewInstallationHandler(s.platform, s.installer)
	if handler == nil || handler.PlatformInstaller == nil {
		return []installer.InstallMethod{}
	}

	return handler.PlatformInstaller.GetAvailableMethods(true)
}

func (s *Server) handleHWAccel(w http.ResponseWriter, r *http.Request) {
	// Get HWA options based on platform and GPU brand
	hwaOptions := platform.GetHWAOptions(s.platform.OSType, s.platform.GPUInfo.Brand)

	// Get recommended HWA decoder based on platform and GPU
	recommendedHWA := platform.GetRecommendedHWADecoder(s.platform.OSType, s.platform.GPUInfo.Brand)

	// Get current HWA value from config using centralized config function
	currentHWA := "auto"
	values := config.GetConfigValue(constants.ConfigKeyHardwareDecoder)
	if len(values) > 0 {
		currentHWA = values[0]
	}

	data := DashboardData{
		Title:          "Hardware Acceleration - MPV.Rocks Manager",
		AppVersion:     version.CurrentVersion,
		Platform:       s.platform,
		HWAOptions:     hwaOptions,
		CurrentHWA:     currentHWA,
		RecommendedHWA: recommendedHWA,
		Nav:            getActiveNav("/hwaccel"),
	}

	if err := s.templates.ExecuteTemplate(w, "hwaccel", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
	data := DashboardData{
		Title:         "MPV Config Management - MPV.Rocks Manager",
		AppVersion:    version.CurrentVersion,
		Platform:      s.platform,
		Nav:           getActiveNav("/config"),
		ConfigBackups: getConfigBackups(),
		Config:        s.getConfigSettings(),
	}

	if err := s.templates.ExecuteTemplate(w, "config", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
	// Build system info with dynamic values
	systemInfo := &SystemInfo{
		AppName:        constants.AppDisplayName,
		AppVersion:     version.CurrentVersion,
		GoVersion:      runtime.Version(),
		BuildTime:      version.BuildTime,
		RepositoryURL:  constants.RepositoryURL,
		RepositoryName: constants.RepositoryURL,
	}

	// Get platform-specific paths
	if runtime.GOOS == constants.OSWindows {
		// Windows: Use detected MPV directory (AppData for fresh installs, legacy for existing)
		mpvBaseDir := constants.GetWindowsMPVBaseDir()
		systemInfo.InstallPath = mpvBaseDir + string(filepath.Separator)
		systemInfo.MPVConfigPath = filepath.Join(mpvBaseDir, constants.PortableConfigDir, constants.MPVConfigFileName)
		systemInfo.LogPath = filepath.Join(mpvBaseDir, constants.PortableConfigDir, "mpv-manager.log")
		systemInfo.BackupPath = filepath.Join(mpvBaseDir, constants.PortableConfigDir, constants.ConfigBackupsDir) + string(filepath.Separator)
	} else {
		// Linux/macOS: Use XDG config home
		configDir := constants.GetXDGConfigHome()
		systemInfo.InstallPath = configDir + string(filepath.Separator) + constants.AppNameMPV + string(filepath.Separator)
		systemInfo.MPVConfigPath = filepath.Join(configDir, constants.AppNameMPV, constants.MPVConfigFileName)
		systemInfo.LogPath = filepath.Join(configDir, constants.AppNameMPV, "mpv-manager.log")
		systemInfo.BackupPath = filepath.Join(configDir, constants.AppNameMPV, constants.ConfigBackupsDir) + string(filepath.Separator)
	}

	// If build time is empty, show "Development build"
	if systemInfo.BuildTime == "" {
		systemInfo.BuildTime = "Development build"
	}

	data := DashboardData{
		Title:      "System Information - MPV.Rocks Manager",
		AppVersion: version.CurrentVersion,
		Platform:   s.platform,
		Nav:        getActiveNav("/system"),
		SystemInfo: systemInfo,
	}

	if err := s.templates.ExecuteTemplate(w, "system", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
	data := DashboardData{
		Title:      "Logs Viewer - MPV.Rocks Manager",
		AppVersion: version.CurrentVersion,
		Platform:   s.platform,
		Nav:        getActiveNav("/logs"),
	}

	if err := s.templates.ExecuteTemplate(w, "logs", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleTasks(w http.ResponseWriter, r *http.Request) {
	data := DashboardData{
		Title:      "Tasks - MPV.Rocks Manager",
		AppVersion: version.CurrentVersion,
		Platform:   s.platform,
		Nav:        getActiveNav("/tasks"),
	}

	if err := s.templates.ExecuteTemplate(w, "tasks", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleHotkeys(w http.ResponseWriter, r *http.Request) {
	// Build hotkeys page data
	categories := buildHotkeyCategories()
	categoryCounts := buildCategoryCounts()
	totalCount := len(hotkeys.Hotkeys)

	data := HotkeysPageData{
		Title:          "Keyboard Shortcuts - MPV.Rocks Manager",
		Nav:            getActiveNav("/hotkeys"),
		AppVersion:     version.CurrentVersion,
		Categories:     categories,
		CategoryCounts: categoryCounts,
		TotalCount:     totalCount,
	}

	if err := s.templates.ExecuteTemplate(w, "hotkeys", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// buildHotkeyCategories builds the categories with hotkeys for display
func buildHotkeyCategories() []HotkeyCategoryData {
	var categories []HotkeyCategoryData

	for _, cat := range hotkeys.Categories {
		catHotkeys := hotkeys.GetHotkeysByCategory(cat)
		if len(catHotkeys) == 0 {
			continue
		}

		var items []HotkeyItem
		for _, h := range catHotkeys {
			items = append(items, HotkeyItem{
				ID:          h.ID,
				Keys:        h.Keys,
				Description: h.Description,
				Category:    h.Category,
				Priority:    h.Priority,
			})
		}

		categories = append(categories, HotkeyCategoryData{
			Category: cat,
			Hotkeys:  items,
			Count:    len(items),
		})
	}

	return categories
}

// buildCategoryCounts builds category count items for filter buttons
func buildCategoryCounts() []CategoryCountItem {
	counts := hotkeys.GetHotkeysCountByCategory()
	var items []CategoryCountItem

	for _, cat := range hotkeys.Categories {
		if count, ok := counts[cat]; ok && count > 0 {
			items = append(items, CategoryCountItem{
				Category: cat,
				Count:    count,
			})
		}
	}

	return items
}

// parseMPVLanguages parses comma-separated language codes from mpv.conf
func parseMPVLanguages(value string) []string {
	if value == "" {
		return []string{}
	}

	// Split by comma and trim whitespace
	parts := strings.Split(value, ",")
	var langs []string
	for _, part := range parts {
		trimmed := strings.TrimSpace(part)
		if trimmed != "" {
			langs = append(langs, trimmed)
		}
	}
	return langs
}

func (s *Server) handleLanguages(w http.ResponseWriter, r *http.Request) {
	// Load current priorities from mpv.conf first, then fall back to config
	audioMPV := getMPVConfigValue("alang")
	subtitleMPV := getMPVConfigValue("slang")

	// Parse MPV config values (comma-separated)
	audioPriority := parseMPVLanguages(audioMPV)
	subtitlePriority := parseMPVLanguages(subtitleMPV)

	// If mpv.conf is empty, use config file preferences
	if len(audioPriority) == 0 {
		audioPriority = config.GetConfigValue(constants.ConfigKeyAudioLanguage)
	}
	if len(subtitlePriority) == 0 {
		subtitlePriority = config.GetConfigValue(constants.ConfigKeySubtitleLanguage)
	}

	locales, _ := LoadLocales()

	// Convert to LanguagePriorityData format
	audioPriorityData := make([]LanguagePriorityData, 0)

	for i, loc := range audioPriority {
		// Find locale entry
		localeEntry, regionEntry := locale.FindByLocale(loc, locales)
		if localeEntry != nil && regionEntry != nil {
			audioPriorityData = append(audioPriorityData, LanguagePriorityData{
				Index:         i,
				LanguageCode:  localeEntry.LanguageCode,
				LanguageName:  localeEntry.LanguageName,
				LanguageLocal: localeEntry.LanguageLocal,
				Flag:          regionEntry.Flag,
				Locale:        regionEntry.Locale,
			})
		} else {
			// Try to find by language code (major language only)
			langEntry := locale.FindByLanguageCode(loc, locales)
			if langEntry != nil {
				audioPriorityData = append(audioPriorityData, LanguagePriorityData{
					Index:         i,
					LanguageCode:  langEntry.LanguageCode,
					LanguageName:  langEntry.LanguageName,
					LanguageLocal: langEntry.LanguageLocal,
					Flag:          langEntry.LanguageCode, // Will use language code flag
					Locale:        langEntry.LanguageCode,
				})
			} else {
				// Fallback for unknown locales
				audioPriorityData = append(audioPriorityData, LanguagePriorityData{
					Index:         i,
					LanguageCode:  loc,
					LanguageName:  loc,
					LanguageLocal: loc,
					Flag:          "🏳️",
					Locale:        loc,
				})
			}
		}
	}

	subtitlePriorityData := make([]LanguagePriorityData, 0)
	for i, loc := range subtitlePriority {
		// Find locale entry
		localeEntry, regionEntry := locale.FindByLocale(loc, locales)
		if localeEntry != nil && regionEntry != nil {
			subtitlePriorityData = append(subtitlePriorityData, LanguagePriorityData{
				Index:         i,
				LanguageCode:  localeEntry.LanguageCode,
				LanguageName:  localeEntry.LanguageName,
				LanguageLocal: localeEntry.LanguageLocal,
				Flag:          regionEntry.Flag,
				Locale:        regionEntry.Locale,
			})
		} else {
			// Try to find by language code (major language only)
			langEntry := locale.FindByLanguageCode(loc, locales)
			if langEntry != nil {
				subtitlePriorityData = append(subtitlePriorityData, LanguagePriorityData{
					Index:         i,
					LanguageCode:  langEntry.LanguageCode,
					LanguageName:  langEntry.LanguageName,
					LanguageLocal: langEntry.LanguageLocal,
					Flag:          langEntry.LanguageCode, // Will use language code flag
					Locale:        langEntry.LanguageCode,
				})
			} else {
				// Fallback for unknown locales
				subtitlePriorityData = append(subtitlePriorityData, LanguagePriorityData{
					Index:         i,
					LanguageCode:  loc,
					LanguageName:  loc,
					LanguageLocal: loc,
					Flag:          "🏳️",
					Locale:        loc,
				})
			}
		}
	}

	data := LanguagesPageData{
		Title:                 "Language Preferences - MPV.Rocks Manager",
		AppVersion:            version.CurrentVersion,
		Config:                s.getConfigSettings(),
		MajorLanguages:        []MajorLanguageData{},   // Loaded via JS
		SelectedMajorLanguage: nil,                     // Set via JS
		RegionalVariants:      []RegionalVariantData{}, // Loaded via JS
		AudioPriority:         audioPriorityData,
		SubtitlePriority:      subtitlePriorityData,
		Nav:                   getActiveNav("/languages"),
	}

	if err := s.templates.ExecuteTemplate(w, "languages", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func getActiveNav(path string) []NavItem {
	navItems := make([]NavItem, len(NavItems))
	copy(navItems, NavItems)

	for i := range navItems {
		if navItems[i].URL == path {
			navItems[i].Active = true
		} else {
			navItems[i].Active = false
		}
	}

	return navItems
}

func (s *Server) checkForUpdates() *UpdateStatus {
	return &UpdateStatus{
		ManagerUpdateAvailable: false,
		ManagerCurrentVersion:  "0.1.0",
		ManagerLatestVersion:   "0.1.0",
		AppUpdatesAvailable:    0,
		Apps:                   []AppUpdateInfo{},
	}
}

func (s *Server) groupInstallMethods(methods []installer.InstallMethod) map[string][]installer.InstallMethod {
	groups := make(map[string][]installer.InstallMethod)

	for _, method := range methods {
		groups[method.AppType] = append(groups[method.AppType], method)
	}

	return groups
}

func (s *Server) groupInstalledApps(apps []config.InstalledApp) map[string][]AppGroupInfo {
	groups := make(map[string][]AppGroupInfo)

	for _, app := range apps {
		groupKey := app.AppName
		logo := getLogoForApp(app.InstallMethod, app.AppName)
		typeTag := getTypeTag(app.InstallMethod)
		typeLogo := getTypeLogo(typeTag, s.platform)

		// Determine version to display:
		// - Package manager apps: use cached version from startup query
		// - Portable/binary apps: use app.AppVersion from config
		displayVersion := app.AppVersion
		if isPackageManagerApp(app.InstallMethod) {
			s.versionCacheMux.RLock()
			if versionInfo, ok := s.versionCache.PackageVersionMap[app.InstallMethod]; ok {
				if versionInfo.CurrentVersion != "" && versionInfo.CurrentVersion != "Unknown" {
					displayVersion = versionInfo.CurrentVersion
				}
			}
			s.versionCacheMux.RUnlock()
		}

		groups[groupKey] = append(groups[groupKey], AppGroupInfo{
			App:           app,
			Logo:          logo,
			TypeTag:       typeTag,
			TypeLogo:      typeLogo,
			UIType:        app.UIType,
			Version:       displayVersion,
			AppName:       app.AppName,
			AppType:       app.AppType,
			InstallMethod: app.InstallMethod,
			Platform:      s.platform,
			Managed:       app.Managed,
		})
	}

	return groups
}

func (s *Server) groupInstalledAppsWithUpdates(apps []config.InstalledApp, updateMap map[string]version.UpdateItem) map[string][]AppGroupInfo {
	groups := make(map[string][]AppGroupInfo)

	for _, app := range apps {
		groupKey := app.AppName
		logo := getLogoForApp(app.InstallMethod, app.AppName)
		typeTag := getTypeTag(app.InstallMethod)
		typeLogo := getTypeLogo(typeTag, s.platform)

		updateAvailable := false
		updateChecked := false
		var currentVersion, availableVersion string

		// Check for update in updateMap
		if updateItem, ok := updateMap[app.InstallMethod]; ok {
			// Handle both update-available and current-version cases
			// UpdateType "update" means there's an update available
			// UpdateType "current-version" means we have version info but no update
			if updateItem.UpdateType == "update" {
				updateAvailable = true
			}
			currentVersion = updateItem.CurrentVersion
			availableVersion = updateItem.AvailableVersion
			updateChecked = true
		} else {
			// Package manager apps: use cached version info
			if isPackageManagerApp(app.InstallMethod) {
				s.versionCacheMux.RLock()
				if versionInfo, ok := s.versionCache.PackageVersionMap[app.InstallMethod]; ok {
					currentVersion = versionInfo.CurrentVersion
					availableVersion = versionInfo.AvailableVersion
					updateChecked = true
				}
				s.versionCacheMux.RUnlock()
			}
		}

		// Determine version to display:
		// - Package manager apps: use currentVersion from cache (queried at startup)
		// - Portable/binary apps: use app.AppVersion (saved to config at install time)
		displayVersion := app.AppVersion
		if isPackageManagerApp(app.InstallMethod) && currentVersion != "" && currentVersion != "Unknown" {
			displayVersion = currentVersion
		}

		groups[groupKey] = append(groups[groupKey], AppGroupInfo{
			App:              app,
			Logo:             logo,
			TypeTag:          typeTag,
			TypeLogo:         typeLogo,
			UpdateAvailable:  updateAvailable,
			UpdateChecked:    updateChecked,
			CurrentVersion:   currentVersion,
			AvailableVersion: availableVersion,
			Platform:         s.platform,
			AppName:          app.AppName,
			AppType:          app.AppType,
			InstallMethod:    app.InstallMethod,
			UIType:           app.UIType,
			Version:          displayVersion,
			Managed:          app.Managed,
		})
	}

	return groups
}

func isPackageManagerApp(methodID string) bool {
	pmApps := []string{
		constants.MethodMPVFlatpak, constants.MethodMPVPackage,
		constants.MethodCelluloidFlatpak,
		constants.MethodCelluloidPackage, constants.MethodMPVBrew,
	}
	for _, pmApp := range pmApps {
		if methodID == pmApp {
			return true
		}
	}
	return false
}

func getLogoForApp(method, appName string) string {
	// Check based on install method ID to determine the app
	switch {
	case strings.Contains(method, constants.MethodMPVBinary), strings.Contains(method, constants.MethodMPVPackage),
		strings.Contains(method, constants.MethodMPVFlatpak),
		strings.Contains(method, constants.MethodMPVBrew), strings.Contains(method, constants.MethodMPVApp):
		return "mpv.avif"
	case strings.Contains(method, constants.MethodCelluloidPackage), strings.Contains(method, constants.MethodCelluloidFlatpak):
		return "celluloid.avif"
	case strings.Contains(method, constants.MethodIINA):
		return "iina.avif"
	case strings.Contains(method, constants.MethodMPCQT):
		return "mpc.avif"
	case strings.Contains(method, constants.MethodUOSC):
		return "uosc.avif"
	case strings.Contains(method, constants.MethodModernZ):
		return "modernz.avif"
	case strings.Contains(method, constants.MethodFFmpeg):
		return "ffmpeg.avif"
	}

	// Fallback: try to match by app name (legacy support)
	switch strings.ToLower(appName) {
	case "mpv":
		return "mpv.avif"
	case "celluloid":
		return "celluloid.avif"
	case "iina":
		return "iina.avif"
	case "mpc-qt", "mpc-hc":
		return "mpc.avif"
	case "infuse":
		return "infuse.avif"
	case "jellyfin":
		return "jellyfin.avif"
	case "uosc":
		return "uosc.avif"
	case "modernz":
		return "modernz.avif"
	case "ffmpeg":
		return "ffmpeg.avif"
	}

	return "mpv.avif"
}

func getTypeTag(method string) string {
	switch method {
	case "mpv-binary", "mpv-binary-v3", "mpv-app":
		return "Portable"
	case "mpv-package", "celluloid-package", "mpv-brew":
		return "Package Manager"
	case "mpv-flatpak", "celluloid-flatpak":
		return "Flatpak"
	case "mpc-qt", "iina":
		return "Installed App"
	case "uosc":
		return "UI Component"
	case "modernz":
		return "UI Component"
	case "ffmpeg":
		return "Utility"
	default:
		return ""
	}
}

// getTypeLogo returns the logo filename for the install method type
func getTypeLogo(typeTag string, p *platform.Platform) string {
	switch typeTag {
	case "Flatpak":
		return "flatpak.avif"
	case "Portable":
		return "portable.avif"
	case "Package Manager":
		// On macOS, return brew icon since Homebrew is the package manager
		if p != nil && p.IsDarwin() {
			return "brew.avif"
		}
		// On Linux, return empty string - caller should use distro logo instead
		return ""
	case "Installed App":
		// Return platform-specific icon for installed apps
		if p != nil {
			if p.IsDarwin() {
				return "mac-app.avif"
			}
			if p.IsWindows() {
				return "win-app.avif"
			}
		}
		// No fallback for Linux - Installed App type is only for macOS/Windows
		return ""
	default:
		return ""
	}
}

// canChangeUI returns true if the installed app supports changing UI overlays
// This is true for MPV binary/portable installs, Flatpak, and package manager MPV
func canChangeUI(installMethod string) bool {
	switch installMethod {
	case constants.MethodMPVBinary, constants.MethodMPVBinaryV3, constants.MethodMPVApp:
		// Windows portable and macOS MPV.app
		return true
	case constants.MethodMPVFlatpak, constants.MethodMPVPackage, constants.MethodMPVBrew:
		// Flatpak (with filesystem override) and package manager MPV use ~/.config/mpv/
		return true
	default:
		return false
	}
}

// cleanAppVersion removes package name prefix from version string for display
// e.g., "mpv 1:0.41.0-2.1" → "1:0.41.0-2.1"
//
//	"celluloid 0.29-1.1" → "0.29-1.1"
func cleanAppVersion(version string) string {
	if version == "" {
		return ""
	}

	// Try to extract version after package name
	// Common formats: "pkg 1.0.0", "pkg 1:0.41.0-2.1"
	fields := strings.Fields(version)
	if len(fields) > 1 {
		// Return everything after the first field (package name)
		return strings.Join(fields[1:], " ")
	}

	return version
}

// compareVersions compares two version strings and returns true if available is newer than current
// This is a simple implementation that handles common version formats
func compareVersions(current, available string) bool {
	// Empty versions mean no update available
	if current == "" || available == "" {
		return false
	}

	// Package manager messages mean no update available
	if available == "Check via Package Manager" || available == "Unknown" {
		return false
	}

	// If versions are the same, no update
	if current == available {
		return false
	}

	// If current is "Unknown", we can't determine if update is available
	// (available might be a placeholder like "Check via Package Manager")
	if current == "Unknown" {
		return false
	}

	// Versions are different - consider this an update
	// This handles cases where versions actually differ
	return true
}

// getPackageManagerType returns the package manager type for a given method ID
func getPackageManagerType(methodID string) string {
	switch {
	case strings.Contains(methodID, "flatpak"):
		return "Flatpak"
	case strings.Contains(methodID, "package"):
		return "Package Manager"
	case strings.Contains(methodID, "brew"):
		return "Brew"
	default:
		return ""
	}
}

func (s *Server) convertClientUpdates(updates []version.UpdateItem) []ClientUpdateInfo {
	var result []ClientUpdateInfo

	s.versionCacheMux.RLock()
	defer s.versionCacheMux.RUnlock()

	for _, item := range updates {
		logo := getLogoForApp(item.MethodID, item.AppName)

		// Check if this is a package manager app
		versionInfo, isPackageManager := s.versionCache.PackageVersionMap[item.MethodID]

		var updateAvailable bool
		var currentVersion, availableVersion string
		var packageManager string

		if isPackageManager {
			// For package manager apps, compare versions
			currentVersion = versionInfo.CurrentVersion
			availableVersion = versionInfo.AvailableVersion
			updateAvailable = compareVersions(currentVersion, availableVersion)
			packageManager = getPackageManagerType(item.MethodID)
		} else {
			// For release-based apps, update is available if UpdateType is "update"
			currentVersion = item.CurrentVersion
			availableVersion = item.AvailableVersion
			updateAvailable = item.UpdateType == "update"
		}

		// Only add apps that actually have updates available
		// This prevents showing "Up to Date" buttons in the Available Updates section
		if !updateAvailable {
			continue
		}

		result = append(result, ClientUpdateInfo{
			AppName:          item.AppName,
			CurrentVersion:   cleanAppVersion(currentVersion),
			AvailableVersion: cleanAppVersion(availableVersion),
			MethodID:         item.MethodID,
			Logo:             logo,
			UpdateAvailable:  true,
			PackageManager:   packageManager,
		})
	}

	return result
}

// getAppLogo returns the logo filename for an app (template function)

func getAppLogo(appName string) string {
	return getLogoForApp("", appName)
}

// initVersionCache initializes the version check cache at startup
// This is a blocking operation that:
// 1. Detects newly installed apps (not tracked in config)
// 2. Removes stale apps (no longer installed)
// 3. Checks for updates
func (s *Server) initVersionCache() {
	log.Info("Initializing version check cache...")

	// Step 1: Detect and sync installed apps (Linux only)
	// This finds apps installed outside of mpv-manager and adds them to tracking
	if runtime.GOOS == "linux" {
		log.Info("Detecting installed MPV apps...")
		added, removed := installer.DetectAndSyncApps()
		if added > 0 {
			log.Info(fmt.Sprintf("Added %d newly detected apps to config", added))
		}
		if removed > 0 {
			log.Info(fmt.Sprintf("Removed %d stale apps from config", removed))
		}
	}

	// Step 2: Get installed apps (now includes any newly detected)
	installedApps := config.GetInstalledApps()
	appUpdates := version.CheckForAppUpdates(s.releaseInfo, installedApps)

	s.versionCacheMux.Lock()
	defer s.versionCacheMux.Unlock()

	// Initialize cache structure
	s.versionCache = &VersionCache{
		LastChecked:       time.Now(),
		UpdateMap:         make(map[string]version.UpdateItem),
		PackageVersionMap: make(map[string]AppVersionInfo),
	}

	// Store update information from CheckForAppUpdates
	for _, update := range appUpdates.AppsToUpdate {
		s.versionCache.UpdateMap[update.MethodID] = update
	}

	// Step 3: Query package manager versions concurrently
	// Also detects and removes stale apps (no longer installed)
	versionMap, staleApps := s.concurrentlyCheckPackageVersions(installedApps)
	for methodID, versionInfo := range versionMap {
		s.versionCache.PackageVersionMap[methodID] = versionInfo
	}

	// Remove stale apps from config (apps that were uninstalled externally)
	// This is a secondary check in case detection missed something
	if len(staleApps) > 0 {
		log.Info(fmt.Sprintf("Removing %d stale apps from config (no longer installed)", len(staleApps)))
		for _, methodID := range staleApps {
			if err := config.RemoveInstalledAppByMethod(methodID); err != nil {
				log.Warn(fmt.Sprintf("Failed to remove stale app %s: %v", methodID, err))
			} else {
				log.Info("Removed stale app from config: " + methodID)
			}
		}
	}

	log.Info(fmt.Sprintf("Version check cache initialized with %d updates and %d package versions",
		len(s.versionCache.UpdateMap), len(s.versionCache.PackageVersionMap)))
}

// concurrentlyCheckPackageVersions checks all package manager versions concurrently
// Returns version map and list of method IDs for apps that are no longer installed
func (s *Server) concurrentlyCheckPackageVersions(installedApps []config.InstalledApp) (map[string]AppVersionInfo, []string) {
	// Collect package manager apps
	var pmApps []config.InstalledApp
	for _, app := range installedApps {
		if isPackageManagerApp(app.InstallMethod) {
			pmApps = append(pmApps, app)
		}
	}

	// If no package manager apps, return empty map
	if len(pmApps) == 0 {
		return make(map[string]AppVersionInfo), nil
	}

	// Create channel for results with buffer
	resultChan := make(chan PackageVersionResult, len(pmApps))

	// Start goroutines for concurrent version checking
	for _, app := range pmApps {
		go func(app config.InstalledApp) {
			currentVersion := GetPackageVersion(app.InstallMethod, app.AppName)
			availableVersion := GetAvailableVersion(app.InstallMethod, app.AppName)

			// Detect if package is not installed
			// Empty currentVersion for package manager apps means not installed
			found := currentVersion != "" && currentVersion != "Unknown"

			if currentVersion == "" {
				currentVersion = "Unknown"
			}
			if availableVersion == "" {
				availableVersion = "Check via Package Manager"
			}

			resultChan <- PackageVersionResult{
				MethodID:         app.InstallMethod,
				CurrentVersion:   currentVersion,
				AvailableVersion: availableVersion,
				Found:            found,
			}
		}(app)
	}

	// Collect results with timeout (10 seconds total)
	versionMap := make(map[string]AppVersionInfo)
	var staleApps []string
	timeout := time.After(10 * time.Second)

	for i := 0; i < len(pmApps); i++ {
		select {
		case result := <-resultChan:
			versionMap[result.MethodID] = AppVersionInfo{
				CurrentVersion:   result.CurrentVersion,
				AvailableVersion: result.AvailableVersion,
			}
			if !result.Found {
				staleApps = append(staleApps, result.MethodID)
			}
		case <-timeout:
			log.Warn("Timeout while checking package versions, some results may be missing")
			// Fill in remaining apps as unknown
			for _, app := range pmApps {
				if _, ok := versionMap[app.InstallMethod]; !ok {
					versionMap[app.InstallMethod] = AppVersionInfo{
						CurrentVersion:   "Unknown",
						AvailableVersion: "Check via Package Manager",
					}
				}
			}
			break
		}
	}

	return versionMap, staleApps
}

// refreshVersionCache refreshes the version check cache
func (s *Server) refreshVersionCache() {
	log.Info("Refreshing version check cache...")

	// Detect and sync installed apps (Linux only)
	if runtime.GOOS == "linux" {
		added, removed := installer.DetectAndSyncApps()
		if added > 0 {
			log.Info(fmt.Sprintf("Added %d newly detected apps to config", added))
		}
		if removed > 0 {
			log.Info(fmt.Sprintf("Removed %d stale apps from config", removed))
		}
	}

	// Get installed apps (now includes any newly detected)
	installedApps := config.GetInstalledApps()
	appUpdates := version.CheckForAppUpdates(s.releaseInfo, installedApps)

	s.versionCacheMux.Lock()
	defer s.versionCacheMux.Unlock()

	// Update last checked time
	s.versionCache.LastChecked = time.Now()

	// Clear and rebuild update map
	s.versionCache.UpdateMap = make(map[string]version.UpdateItem)
	for _, update := range appUpdates.AppsToUpdate {
		s.versionCache.UpdateMap[update.MethodID] = update
	}

	// Query package manager versions for package manager apps concurrently
	// Also detects and removes stale apps (no longer installed)
	versionMap, staleApps := s.concurrentlyCheckPackageVersions(installedApps)
	for methodID, versionInfo := range versionMap {
		s.versionCache.PackageVersionMap[methodID] = versionInfo
	}

	// Remove stale apps from config (apps that were uninstalled externally)
	// This is a secondary check in case detection missed something
	if len(staleApps) > 0 {
		log.Info(fmt.Sprintf("Removing %d stale apps from config (no longer installed)", len(staleApps)))
		for _, methodID := range staleApps {
			if err := config.RemoveInstalledAppByMethod(methodID); err != nil {
				log.Warn(fmt.Sprintf("Failed to remove stale app %s: %v", methodID, err))
			} else {
				log.Info("Removed stale app from config: " + methodID)
			}
		}
	}

	log.Info(fmt.Sprintf("Version check cache refreshed: %d updates, %d package versions",
		len(s.versionCache.UpdateMap), len(s.versionCache.PackageVersionMap)))
}

// handleSettings renders the settings page
func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
	// Build system info with config file path
	systemInfo := &SystemInfo{}
	configPath := getConfigFilePath()
	systemInfo.MPVConfigPath = configPath

	data := DashboardData{
		Title:      "Settings - MPV.Rocks Manager",
		AppVersion: version.CurrentVersion,
		Platform:   s.platform,
		Nav:        getActiveNav("/settings"),
		SystemInfo: systemInfo,
	}

	if err := s.templates.ExecuteTemplate(w, "settings", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// SetPendingNotice delegates to the installer package's notice function
func (s *Server) SetPendingNotice(notice string) {
	installer.SetPendingNotice(notice)
}

// GetAndClearPendingNotice delegates to the installer package's notice function
func (s *Server) GetAndClearPendingNotice() string {
	return installer.GetAndClearPendingNotice()
}

// GetPendingNotice delegates to the installer package's notice function
func (s *Server) GetPendingNotice() string {
	return installer.GetPendingNotice()
}
