package web

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"time"

	"gitgud.io/mike/mpv-manager/internal/assets"
	"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/keyring"
	"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"
)

// APIResponse is a standard JSON response structure for API endpoints
type APIResponse struct {
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
}

// sendJSONError sends a JSON error response with proper Content-Type header
func sendJSONError(w http.ResponseWriter, statusCode int, message string) {
	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	w.WriteHeader(statusCode)
	json.NewEncoder(w).Encode(APIResponse{
		Status:  "error",
		Message: message,
	})
}

func (s *Server) handleServerStatusAPI(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "online",
	})
}

func (s *Server) handlePlatformAPI(w http.ResponseWriter, r *http.Request) {
	response := map[string]interface{}{
		"os":      string(s.platform.OSType),
		"arch":    s.platform.Arch,
		"distro":  s.platform.Distro,
		"version": s.platform.Version,
		"cpu": map[string]interface{}{
			"vendor":             s.platform.CPUInfo.Vendor,
			"vendor_display":     s.platform.CPUInfo.VendorDisplay,
			"architecture_level": s.platform.CPUInfo.ArchitectureLevel,
			"model":              s.platform.CPUInfo.Model,
			"features":           s.platform.CPUInfo.Features,
			"supports_avx2":      s.platform.CPUInfo.SupportsAVX2,
			"supports_avx512":    s.platform.CPUInfo.SupportsAVX512,
			"supports_neon":      s.platform.CPUInfo.SupportsNEON,
		},
		"gpu": map[string]interface{}{
			"models": s.platform.GPUInfo.Models,
			"vendor": s.platform.GPUInfo.Brand,
			"codecs": s.platform.GPUInfo.SupportedCodecs,
		},
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(response)
}

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

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"apps": installedApps,
	})
}

func (s *Server) handleInstallMethodsAPI(w http.ResponseWriter, r *http.Request) {
	methods := s.getInstallMethods()

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"methods": methods,
	})
}

func (s *Server) handleUpdatesAPI(w http.ResponseWriter, r *http.Request) {
	installerCheck := version.CheckForUpdate()
	installedApps := config.GetInstalledApps()
	appUpdates := version.CheckForAppUpdates(s.releaseInfo, installedApps)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"installer": map[string]interface{}{
			"current_version":  installerCheck.CurrentVersion,
			"latest_version":   installerCheck.LatestVersion,
			"update_available": installerCheck.UpdateAvailable,
		},
		"apps":          appUpdates.AppsToUpdate,
		"total_updates": len(appUpdates.AppsToUpdate),
	})
}

func (s *Server) handleConfigSettingsAPI(w http.ResponseWriter, r *http.Request) {
	audioLangs := config.GetConfigValue(constants.ConfigKeyAudioLanguage)
	subtitleLangs := config.GetConfigValue(constants.ConfigKeySubtitleLanguage)
	hwaValue := "auto"

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

	// 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)

	// Load additional settings
	savePositionOnQuit := getMPVConfigValue(constants.ConfigKeySavePositionOnQuit)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"audio_language":             audioLang,
		"subtitle_language":          subtitleLang,
		"hardware_acceleration":      hwaValue,
		"config_file":                getConfigFilePath(),
		"save_position_on_quit":      savePositionOnQuit,
		"screenshot_format":          screenshotFormat,
		"screenshot_tag_colorspace":  screenshotTagColorspace,
		"screenshot_high_bit_depth":  screenshotHighBitDepth,
		"screenshot_template":        screenshotTemplate,
		"screenshot_directory":       screenshotDirectory,
		"screenshot_jpeg_quality":    screenshotJpegQuality,
		"screenshot_png_compression": screenshotPngCompression,
		"screenshot_webp_lossless":   screenshotWebpLossless,
		"screenshot_webp_quality":    screenshotWebpQuality,
		"screenshot_jxl_distance":    screenshotJxlDistance,
		"screenshot_avif_encoder":    screenshotAvifEncoder,
		"screenshot_avif_pixfmt":     screenshotAvifPixfmt,
		"screenshot_avif_opts":       screenshotAvifOpts,
	})
}

func (s *Server) handleConfigBackupsAPI(w http.ResponseWriter, r *http.Request) {
	backups := getConfigBackups()

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"backups": backups,
	})
}

// handleConfigBackupsListAPI returns HTML fragment for backup list
// Used for HTMX partial refresh after restore/delete operations
func (s *Server) handleConfigBackupsListAPI(w http.ResponseWriter, r *http.Request) {
	backups := getConfigBackups()

	data := struct {
		ConfigBackups []ConfigBackup
	}{
		ConfigBackups: backups,
	}

	w.Header().Set("Content-Type", constants.ContentTypeHTML)
	err := s.templates.ExecuteTemplate(w, "config-backups-list", data)
	if err != nil {
		log.Error("Failed to render config-backups-list template: " + err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) handleInstallAPI(w http.ResponseWriter, r *http.Request) {
	var req InstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to decode install request: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID
	if err := ValidateMethodID(req.MethodID); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid method ID: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID is compatible with current platform
	if err := ValidateInstallMethod(req.MethodID, s.platform.IsWindows(), s.platform.IsDarwin(), s.platform.IsLinux()); err != nil {
		log.Error(constants.LogPrefixAPI + "Method not available on this platform: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Install request received: method_id=" + req.MethodID + ", ui_type=" + req.UIType)

	// Determine UI type, with backwards compatibility for install_uosc
	uiType := req.UIType
	if uiType == "" {
		// Fall back to install_uosc for backwards compatibility
		if req.InstallUOSC {
			uiType = constants.UITypeUOSC
		} else {
			uiType = constants.UITypeNone
		}
	}

	session := s.sessions.Create(req.MethodID)

	go s.runInstallation(session, req.MethodID, uiType, false)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"session_id": session.ID,
		"status":     "started",
	})
}

type InstallRequest struct {
	MethodID    string `json:"method_id"`
	InstallUOSC bool   `json:"install_uosc"`
	UIType      string `json:"ui_type,omitempty"` // "uosc", "modernz", or "none"
}

func (s *Server) runInstallation(session *Session, methodID string, uiType string, isUpdate bool) {
	p := s.platform
	inst := s.installer

	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)

	// Create a context for this installation (sessions don't support cancellation yet)
	ctx := context.Background()
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	go func() {
		for output := range outputChan {
			session.OutputChan <- output
		}
	}()

	go func() {
		for err := range errorChan {
			session.ErrorChan <- err
		}
	}()

	handler := installer.NewInstallationHandler(p, inst)
	err := handler.ExecuteInstall(ctx, cr, methodID, uiType, isUpdate)

	if err != nil {
		session.ErrorChan <- err
	} else if !isUpdate {
		// Only save to config for new installations, not updates
		appName := installer.GetMethodDisplayName(methodID)
		appType := constants.MethodAppType[methodID]
		installPath := getInstallPathForMethod(methodID, p, config.GetInstallPath())

		// Only save version for portable/binary apps we manage
		// Package manager apps (flatpak, brew, apt) are checked at startup
		appVersion := ""
		if appType == "app" {
			appVersion = getAppVersionForMethod(methodID, inst, p)
		}

		app := config.InstalledApp{
			AppName:       appName,
			AppType:       appType,
			InstallMethod: methodID,
			InstallPath:   installPath,
			InstalledDate: time.Now().Format("2006-01-02 15:04:05"),
			AppVersion:    appVersion,
			UIType:        uiType, // Save UI type for MPV apps
			Managed:       true,   // Installed and managed by MPV Manager
		}

		if err := config.AddInstalledApp(app); err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to save installed app: " + err.Error())
		} else {
			log.Info(constants.LogPrefixAPI + "Successfully saved installed app: " + appName)

			// Update version cache for package manager apps (Flatpak, Brew)
			// This ensures version displays immediately without needing to restart the app
			if isPackageManagerApp(methodID) {
				versionInfo := GetPackageVersion(methodID, appName)
				typeTag := getTypeTag(methodID)

				s.versionCacheMux.Lock()
				if s.versionCache.PackageVersionMap == nil {
					s.versionCache.PackageVersionMap = make(map[string]AppVersionInfo)
				}
				s.versionCache.PackageVersionMap[methodID] = AppVersionInfo{
					CurrentVersion:   versionInfo,
					AvailableVersion: "Check via " + typeTag,
				}
				s.versionCacheMux.Unlock()

				log.Info(constants.LogPrefixAPI + "Updated version cache for " + methodID + ": " + versionInfo)
			}
		}
	}

	close(session.DoneChan)
}

func getInstallPathForMethod(methodID string, p *platform.Platform, defaultPath string) string {
	// Windows methods
	if methodID == constants.MethodMPVBinary || methodID == constants.MethodMPVBinaryV3 {
		return defaultPath
	}
	if methodID == constants.MethodMPCQT {
		return defaultPath
	}

	// Flatpak
	if methodID == constants.MethodMPVFlatpak || methodID == constants.MethodCelluloidFlatpak {
		return "/var/lib/flatpak/app"
	}

	// Package manager
	if methodID == constants.MethodMPVPackage || methodID == constants.MethodCelluloidPackage {
		if p != nil {
			switch p.DistroFamily {
			case "arch":
				return "pacman"
			case "debian":
				return "apt"
			case "rhel":
				return "dnf"
			}
		}
		return "package-manager"
	}

	// macOS
	if methodID == constants.MethodMPVApp {
		return "/Applications/MPV.app"
	}
	if methodID == constants.MethodIINA {
		return "/Applications/IINA.app"
	}
	if methodID == constants.MethodMPVBrew {
		return "brew"
	}

	return defaultPath
}

// extractVersionFromURL extracts version from a download URL
// e.g., .../mpv-v0.41.0-macos-26-arm.zip -> 0.41.0
func extractVersionFromURL(url, prefix string) string {
	// Match prefix + optional 'v' or 'V' + version
	re := regexp.MustCompile(prefix + `[vV]?(\d+\.\d+\.\d+)`)
	matches := re.FindStringSubmatch(url)
	if len(matches) > 1 {
		return matches[1]
	}
	return ""
}

func getAppVersionForMethod(methodID string, inst *installer.Installer, p *platform.Platform) string {
	// Get version from release info for some methods
	switch methodID {
	case constants.MethodMPVBinary, constants.MethodMPVBinaryV3:
		return inst.ReleaseInfo.MpvVersion
	case constants.MethodMPVApp:
		// For macOS MPV.app, extract version from download URL since MpvVersion may not be set
		if inst.ReleaseInfo.MpvVersion != "" {
			return inst.ReleaseInfo.MpvVersion
		}
		// Extract from URL based on architecture
		var url string
		if p.Arch == "arm64" {
			url = inst.ReleaseInfo.MacOS.ARMLatest.URL
		} else {
			url = inst.ReleaseInfo.MacOS.Intel15.URL
		}
		return extractVersionFromURL(url, "mpv-")
	case constants.MethodMPCQT:
		return inst.ReleaseInfo.MPCQT.AppVersion
	case constants.MethodIINA:
		return inst.ReleaseInfo.IINA.AppVersion
	}

	// Get version from package manager for Flatpak/Brew
	switch methodID {
	case constants.MethodMPVFlatpak:
		return inst.GetFlatpakVersion(constants.FlatpakMPV)
	case constants.MethodCelluloidFlatpak:
		return inst.GetFlatpakVersion(constants.FlatpakCelluloid)
	case constants.MethodMPVPackage, constants.MethodCelluloidPackage:
		packageName := constants.PackageMPV
		if methodID == constants.MethodCelluloidPackage {
			packageName = constants.PackageCelluloid
		}
		return inst.GetPackageVersion(packageName)
	case constants.MethodMPVBrew:
		return inst.GetBrewVersion(constants.PackageMPV)
	}

	return ""
}

func (s *Server) handleUninstallAPI(w http.ResponseWriter, r *http.Request) {
	var req UninstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to decode uninstall request: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate app name
	if err := ValidateAppName(req.AppName); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid app name: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Normalize and validate app type (legacy types like "flatpak" are normalized to "app")
	normalizedAppType := NormalizeAppType(req.AppType)
	if err := ValidateAppType(req.AppType); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid app type: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Use normalized type
	req.AppType = normalizedAppType

	// Validate install method
	if err := ValidateMethodID(req.InstallMethod); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid install method: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Uninstall request received: app_name=" + req.AppName + ", app_type=" + req.AppType + ", install_method=" + req.InstallMethod)

	// Create session for uninstallation
	session := s.sessions.Create(req.InstallMethod + "-uninstall")

	// Run uninstallation in background
	go s.runUninstallation(session, req)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"session_id": session.ID,
		"status":     "uninstall_started",
	})
}

func (s *Server) runUninstallation(session *Session, req UninstallRequest) {
	p := s.platform
	inst := s.installer

	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)

	// Create a context for this uninstallation (sessions don't support cancellation yet)
	ctx := context.Background()
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	// Start streaming output to session
	go func() {
		for output := range outputChan {
			session.OutputChan <- output
		}
	}()

	// Start streaming errors to session
	go func() {
		for err := range errorChan {
			session.ErrorChan <- err
		}
	}()

	app := config.InstalledApp{
		AppName:       req.AppName,
		AppType:       req.AppType,
		InstallMethod: req.InstallMethod,
	}

	handler := installer.NewInstallationHandler(p, inst)
	err := handler.ExecuteUninstall(ctx, cr, &app)

	if err != nil {
		session.ErrorChan <- err
	} else {
		// Remove from installed apps config
		if err := config.RemoveInstalledApp(req.AppName); err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to remove app from config: " + err.Error())
			session.ErrorChan <- err
		} else {
			log.Info(constants.LogPrefixAPI + "Successfully uninstalled: " + req.AppName)
		}
	}

	close(session.DoneChan)
}

type UninstallRequest struct {
	AppName       string `json:"app_name"`
	AppType       string `json:"app_type"`
	InstallMethod string `json:"install_method"`
}

func (s *Server) handleFileAssociationsSetupAPI(w http.ResponseWriter, r *http.Request) {
	if !s.platform.IsWindows() {
		sendJSONError(w, http.StatusMethodNotAllowed, "File associations only available on Windows")
		return
	}

	inst := s.installer
	handler := installer.NewInstallationHandler(s.platform, inst)
	if handler == nil {
		sendJSONError(w, http.StatusInternalServerError, "Failed to create installer")
		return
	}

	outputChan := make(chan string, 100)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunner(outputChan, errorChan)

	err := handler.PlatformInstaller.SetupFileAssociationsWithOutput(cr)

	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "success",
		"message": "File associations registered successfully",
	})
}

func (s *Server) handleFileAssociationsRemoveAPI(w http.ResponseWriter, r *http.Request) {
	if !s.platform.IsWindows() {
		sendJSONError(w, http.StatusMethodNotAllowed, "File associations only available on Windows")
		return
	}

	inst := s.installer
	handler := installer.NewInstallationHandler(s.platform, inst)
	if handler == nil {
		sendJSONError(w, http.StatusInternalServerError, "Failed to create installer")
		return
	}

	outputChan := make(chan string, 100)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunner(outputChan, errorChan)

	err := handler.PlatformInstaller.RemoveFileAssociationsWithOutput(cr)

	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "success",
		"message": "File associations removed successfully",
	})
}

func (s *Server) handleShortcutsCreateAPI(w http.ResponseWriter, r *http.Request) {
	if !s.platform.IsWindows() {
		sendJSONError(w, http.StatusMethodNotAllowed, "Shortcuts only available on Windows")
		return
	}

	inst := s.installer
	handler := installer.NewInstallationHandler(s.platform, inst)
	if handler == nil {
		sendJSONError(w, http.StatusInternalServerError, "Failed to create installer")
		return
	}

	// Check if MPV is installed (check install path)
	installPath := config.GetInstallPath()
	mpvPath := filepath.Join(installPath, "mpv.exe")
	if _, err := os.Stat(mpvPath); os.IsNotExist(err) {
		sendJSONError(w, http.StatusNotFound, "MPV is not installed")
		return
	}

	outputChan := make(chan string, 100)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunner(outputChan, errorChan)

	err := handler.CreateMPVShortcutWithOutput(cr)

	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "success",
		"message": "Desktop and Start Menu shortcuts created successfully",
	})
}

func (s *Server) handleLogsAPI(w http.ResponseWriter, r *http.Request) {
	logPath := log.GetFilePath()

	logContent, err := os.ReadFile(logPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Write(logContent)
}

func (s *Server) handleLogsClearAPI(w http.ResponseWriter, r *http.Request) {
	logPath := log.GetFilePath()

	if err := os.WriteFile(logPath, []byte(""), 0644); err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

func (s *Server) handleLogsDownloadAPI(w http.ResponseWriter, r *http.Request) {
	logPath := log.GetFilePath()

	logContent, err := os.ReadFile(logPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Header().Set("Content-Disposition", "attachment; filename=mpv-manager.log")
	w.Write(logContent)
}

type LogError struct {
	Timestamp string `json:"timestamp"`
	Message   string `json:"message"`
	Details   string `json:"details"`
}

func (s *Server) handleRecentErrorsAPI(w http.ResponseWriter, r *http.Request) {
	logPath := log.GetFilePath()
	content, err := os.ReadFile(logPath)
	if err != nil {
		log.Error("Failed to read log file: " + err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	errors := parseLogErrors(string(content), 5)

	w.Header().Set("Content-Type", constants.ContentTypeHTML)
	err = s.templates.ExecuteTemplate(w, "recent-errors", errors)
	if err != nil {
		log.Error("Failed to render recent-errors template: " + err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func parseLogErrors(logContent string, maxErrors int) []LogError {
	var errors []LogError
	lines := strings.Split(logContent, "\n")

	// Only process the last 50 lines for efficiency
	if len(lines) > 50 {
		lines = lines[len(lines)-50:]
	}

	for i := len(lines) - 1; i >= 0; i-- {
		line := strings.TrimSpace(lines[i])
		if strings.Contains(line, "[ERROR]") || strings.Contains(line, "[FATAL]") {
			parts := strings.SplitN(line, "] ", 2)
			if len(parts) >= 2 {
				timestamp := strings.Trim(parts[0], "[")
				message := strings.TrimPrefix(parts[1], "[ERROR] ")
				message = strings.TrimPrefix(message, "[FATAL] ")

				var details string
				for j := i + 1; j < len(lines) && j < i+3; j++ {
					if strings.TrimSpace(lines[j]) != "" {
						details += strings.TrimSpace(lines[j]) + "\n"
					}
				}

				errors = append(errors, LogError{
					Timestamp: timestamp,
					Message:   message,
					Details:   strings.TrimSpace(details),
				})

				if len(errors) >= maxErrors {
					break
				}
			}
		}
	}

	for i, j := 0, len(errors)-1; i < j; i, j = i+1, j-1 {
		errors[i], errors[j] = errors[j], errors[i]
	}

	return errors
}

func (s *Server) handleConfigApplyAPI(w http.ResponseWriter, r *http.Request) {
	var req ConfigApplyRequest
	body, _ := io.ReadAll(r.Body)

	// Only try to decode JSON if body is not empty
	if len(body) > 0 {
		if err := json.Unmarshal(body, &req); err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to decode config apply request: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
	}

	// Validate audio language preference
	if req.AudioLanguage != "" {
		if err := ValidateLanguageCode(req.AudioLanguage); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid audio language code: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		config.SetConfigValue(constants.ConfigKeyAudioLanguage, []string{req.AudioLanguage}, true)
	}

	// Validate subtitle language preference
	if req.SubtitleLanguage != "" {
		if err := ValidateLanguageCode(req.SubtitleLanguage); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid subtitle language code: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		config.SetConfigValue(constants.ConfigKeySubtitleLanguage, []string{req.SubtitleLanguage}, true)
	}

	// Validate and handle hardware acceleration setting
	if req.HardwareAccel != "" {
		if err := ValidateHWAValue(req.HardwareAccel); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid hardware acceleration method: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		// Save to mpv.conf
		err := config.SetConfigValue(constants.ConfigKeyHardwareDecoder, []string{req.HardwareAccel}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply HWA setting: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
		// Also save preference to mpv-manager.json for backup/restore
		if err := config.SetHWADecoderPreference(req.HardwareAccel); err != nil {
			log.Warn(constants.LogPrefixAPI + "Failed to save HWA preference: " + err.Error())
		}
	}

	// Validate and handle video output driver setting
	if req.VideoOutputDriver != "" {
		if err := ValidateConfigValue(constants.ConfigKeyVideoOutput, req.VideoOutputDriver); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid video output driver: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyVideoOutput, []string{req.VideoOutputDriver}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply video output driver: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle video scale filter setting
	if req.VideoScaleFilter != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScaleFilter, req.VideoScaleFilter); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid video scale filter: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScaleFilter, []string{req.VideoScaleFilter}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply video scale filter: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle dither algorithm setting
	if req.DitherAlgorithm != "" {
		if err := ValidateConfigValue(constants.ConfigKeyDitherAlgorithm, req.DitherAlgorithm); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid dither algorithm: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyDitherAlgorithm, []string{req.DitherAlgorithm}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply dither algorithm: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle save-position-on-quit setting
	if req.SavePositionOnQuit != "" {
		if err := ValidateConfigValue(constants.ConfigKeySavePositionOnQuit, req.SavePositionOnQuit); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid save-position-on-quit value: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeySavePositionOnQuit, []string{req.SavePositionOnQuit}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply save-position-on-quit setting: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle border setting
	if req.Border != "" {
		if err := ValidateConfigValue(constants.ConfigKeyBorder, req.Border); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid border value: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyBorder, []string{req.Border}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply border setting: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle profile setting
	if req.Profile != "" {
		if err := ValidateConfigValue(constants.ConfigKeyProfile, req.Profile); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid profile value: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyProfile, []string{req.Profile}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply profile setting: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot format
	if req.ScreenshotFormat != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotFormat, req.ScreenshotFormat); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot format: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotFormat, []string{req.ScreenshotFormat}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot format: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot tag colorspace
	if req.ScreenshotTagColorspace != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotTagColorspace, req.ScreenshotTagColorspace); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot tag colorspace: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotTagColorspace, []string{req.ScreenshotTagColorspace}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot tag colorspace: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot high bit depth
	if req.ScreenshotHighBitDepth != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotHighBitDepth, req.ScreenshotHighBitDepth); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot high bit depth: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotHighBitDepth, []string{req.ScreenshotHighBitDepth}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot high bit depth: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot template
	if req.ScreenshotTemplate != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotTemplate, req.ScreenshotTemplate); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot template: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotTemplate, []string{req.ScreenshotTemplate}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot template: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot directory
	if req.ScreenshotDirectory != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotDirectory, req.ScreenshotDirectory); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot directory: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotDirectory, []string{req.ScreenshotDirectory}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot directory: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot jpeg quality
	if req.ScreenshotJpegQuality != "" {
		if err := ValidateScreenshotQuality(req.ScreenshotJpegQuality); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot jpeg quality: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotJpegQuality, []string{req.ScreenshotJpegQuality}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot jpeg quality: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot png compression
	if req.ScreenshotPngCompression != "" {
		if err := ValidateScreenshotCompression(req.ScreenshotPngCompression); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot png compression: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotPngCompression, []string{req.ScreenshotPngCompression}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot png compression: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot webp lossless
	if req.ScreenshotWebpLossless != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotWebpLossless, req.ScreenshotWebpLossless); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot webp lossless: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotWebpLossless, []string{req.ScreenshotWebpLossless}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot webp lossless: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot webp quality
	if req.ScreenshotWebpQuality != "" {
		if err := ValidateScreenshotQuality(req.ScreenshotWebpQuality); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot webp quality: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotWebpQuality, []string{req.ScreenshotWebpQuality}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot webp quality: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot jxl distance
	if req.ScreenshotJxlDistance != "" {
		if err := ValidateScreenshotJxlDistance(req.ScreenshotJxlDistance); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot jxl distance: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotJxlDistance, []string{req.ScreenshotJxlDistance}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot jxl distance: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot avif encoder
	if req.ScreenshotAvifEncoder != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotAvifEncoder, req.ScreenshotAvifEncoder); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot avif encoder: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotAvifEncoder, []string{req.ScreenshotAvifEncoder}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot avif encoder: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot avif pixfmt
	if req.ScreenshotAvifPixfmt != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotAvifPixfmt, req.ScreenshotAvifPixfmt); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot avif pixfmt: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotAvifPixfmt, []string{req.ScreenshotAvifPixfmt}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot avif pixfmt: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	// Validate and handle screenshot avif opts
	if req.ScreenshotAvifOpts != "" {
		if err := ValidateConfigValue(constants.ConfigKeyScreenshotAvifOpts, req.ScreenshotAvifOpts); err != nil {
			log.Error(constants.LogPrefixAPI + "Invalid screenshot avif opts: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, err.Error())
			return
		}
		err := config.SetConfigValue(constants.ConfigKeyScreenshotAvifOpts, []string{req.ScreenshotAvifOpts}, true)
		if err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to apply screenshot avif opts: " + err.Error())
			sendJSONError(w, http.StatusInternalServerError, err.Error())
			return
		}
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

// getMPVConfigValue reads a setting value from mpv.conf
func getMPVConfigValue(key string) string {
	values := config.GetConfigValue(key)
	if len(values) == 0 {
		return ""
	}
	return values[0]
}

type ConfigApplyRequest struct {
	AudioLanguage      string `json:"audio_language"`
	SubtitleLanguage   string `json:"subtitle_language"`
	HardwareAccel      string `json:"hardware_acceleration"`
	VideoOutputDriver  string `json:"video_output_driver"`
	VideoScaleFilter   string `json:"video_scale_filter"`
	DitherAlgorithm    string `json:"dither_algorithm"`
	SavePositionOnQuit string `json:"save_position_on_quit"`
	Border             string `json:"border"`
	Profile            string `json:"profile"`

	// Screenshot settings (NEW)
	ScreenshotFormat         string `json:"screenshot_format"`
	ScreenshotTagColorspace  string `json:"screenshot_tag_colorspace"`
	ScreenshotHighBitDepth   string `json:"screenshot_high_bit_depth"`
	ScreenshotTemplate       string `json:"screenshot_template"`
	ScreenshotDirectory      string `json:"screenshot_directory"`
	ScreenshotJpegQuality    string `json:"screenshot_jpeg_quality"`
	ScreenshotPngCompression string `json:"screenshot_png_compression"`
	ScreenshotWebpLossless   string `json:"screenshot_webp_lossless"`
	ScreenshotWebpQuality    string `json:"screenshot_webp_quality"`
	ScreenshotJxlDistance    string `json:"screenshot_jxl_distance"`
	ScreenshotAvifEncoder    string `json:"screenshot_avif_encoder"`
	ScreenshotAvifPixfmt     string `json:"screenshot_avif_pixfmt"`
	ScreenshotAvifOpts       string `json:"screenshot_avif_opts"`
}

func (s *Server) handleConfigResetAPI(w http.ResponseWriter, r *http.Request) {
	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunner(outputChan, errorChan)

	err := s.installer.InstallMPVConfigWithOutput(cr)

	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

func (s *Server) handleConfigRestoreAPI(w http.ResponseWriter, r *http.Request) {
	var req ConfigRestoreRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate backup path
	if err := config.ValidateBackupPath(req.BackupPath); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid backup path: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, "Invalid backup path")
		return
	}

	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunner(outputChan, errorChan)

	err := s.installer.RestoreConfigWithOutput(cr, req.BackupPath)

	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

func (s *Server) handleConfigBackupDeleteAPI(w http.ResponseWriter, r *http.Request) {
	var req ConfigBackupDeleteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate backup path
	if err := config.ValidateBackupPath(req.BackupPath); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid backup path: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, "Invalid backup path")
		return
	}

	// Delete the backup file
	if err := os.Remove(req.BackupPath); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to delete backup: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Successfully deleted backup: " + req.BackupPath)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

type ConfigRestoreRequest struct {
	BackupPath string `json:"backup_path"`
}

type ConfigBackupDeleteRequest struct {
	BackupPath string `json:"backup_path"`
}

func (s *Server) handleCheckAppUpdatesAPI(w http.ResponseWriter, r *http.Request) {
	log.Info("[API] Checking for app updates...")

	// Refresh version check cache
	s.refreshVersionCache()

	// Get update items from cache
	s.versionCacheMux.RLock()
	var updateItems []version.UpdateItem
	for _, update := range s.versionCache.UpdateMap {
		updateItems = append(updateItems, update)
	}
	s.versionCacheMux.RUnlock()

	// Convert updates to client update info (this filters correctly)
	clientUpdates := s.convertClientUpdates(updateItems)

	data := struct {
		ClientUpdates []ClientUpdateInfo
	}{
		ClientUpdates: clientUpdates,
	}

	w.Header().Set("Content-Type", constants.ContentTypeHTML)
	err := s.templates.ExecuteTemplate(w, "client-updates", data)
	if err != nil {
		log.Error("Failed to render client-updates template: " + err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Add artificial delay after response is written to give user time to see spinning animation
	log.Info("[API] Check updates complete, adding 1500ms delay for visual feedback...")
	time.Sleep(1500 * time.Millisecond)
}

// handleRefreshInstalledAppsAPI refreshes the version cache and returns the
// installed apps list, available updates, AND install methods.
// Uses HTMX OOB swap to update all three sections.
func (s *Server) handleRefreshInstalledAppsAPI(w http.ResponseWriter, r *http.Request) {
	log.Info("[API] Refreshing installed apps, updates, and install methods...")

	// Refresh version check cache
	s.refreshVersionCache()

	// Get installed apps
	installedApps := config.GetInstalledApps()

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

	// Group apps with update info
	appGroups := s.groupInstalledAppsWithUpdates(installedApps, updateMap)

	// Convert updates to client update info
	clientUpdates := s.convertClientUpdates(updateItems)

	// Get install methods
	installMethods := s.getInstallMethods()

	data := struct {
		InstalledAppGroups map[string][]AppGroupInfo
		ClientUpdates      []ClientUpdateInfo
		InstallMethods     []installer.InstallMethod
		Platform           *platform.Platform
	}{
		InstalledAppGroups: appGroups,
		ClientUpdates:      clientUpdates,
		InstallMethods:     installMethods,
		Platform:           s.platform,
	}

	w.Header().Set("Content-Type", constants.ContentTypeHTML)
	err := s.templates.ExecuteTemplate(w, "apps-refresh-response", data)
	if err != nil {
		log.Error("Failed to render apps-refresh-response template: " + err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	log.Info("[API] Apps refresh complete, adding 1500ms delay for visual feedback...")
	time.Sleep(1500 * time.Millisecond)
}

func (s *Server) handleAppUpdateAPI(w http.ResponseWriter, r *http.Request) {
	var req InstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID
	if err := ValidateMethodID(req.MethodID); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid method ID: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID is compatible with current platform
	if err := ValidateInstallMethod(req.MethodID, s.platform.IsWindows(), s.platform.IsDarwin(), s.platform.IsLinux()); err != nil {
		log.Error(constants.LogPrefixAPI + "Method not available on this platform: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// For updates, use the provided UI type or default
	uiType := req.UIType
	if uiType == "" {
		uiType = constants.DefaultUIType
	}

	session := s.sessions.Create(req.MethodID + "-update")

	go s.runInstallation(session, req.MethodID, uiType, true)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"session_id": session.ID,
		"status":     "update_started",
	})
}

func (s *Server) handleManagerUpdateAPI(w http.ResponseWriter, r *http.Request) {
	log.Info("[API] Manager update request received")

	if version.SelfUpdateDisabled == "true" {
		log.Info("[API] Manager update rejected: self-update is disabled in this build")
		sendJSONError(w, http.StatusForbidden, "Self-update is disabled in this build (installed via package manager)")
		return
	}

	check := version.CheckForUpdate()
	if !check.UpdateAvailable {
		log.Info("[API] No manager update available")
		sendJSONError(w, http.StatusBadRequest, "No update available")
		return
	}

	log.Info("[API] Update available: " + check.LatestVersion)
	executablePath, err := os.Executable()
	if err != nil {
		log.Error("[API] Failed to get executable path: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	log.Info("[API] Starting manager self-update...")
	err = version.UpdateSelfWithProgress(executablePath, func(written, total int64) {
		log.Info(fmt.Sprintf("Update progress: %d/%d bytes", written, total))
	})

	if err != nil {
		log.Error("[API] Manager update failed: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	log.Info("[API] Manager update completed successfully")
	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":     "success",
		"action":     "show_modal",
		"modal_type": constants.ModalTypeUpdateManagerSuccess,
		"message":    "Manager updated successfully. Please restart the application.",
	})
}

func getConfigBackups() []ConfigBackup {
	backupDir, err := constants.GetMPVConfigBackupDirWithHome()
	if err != nil {
		return []ConfigBackup{}
	}

	var items []ConfigBackup

	if entries, err := os.ReadDir(backupDir); err == nil {
		for _, entry := range entries {
			if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".conf") {
				info, _ := entry.Info()
				items = append(items, ConfigBackup{
					Filename:    entry.Name(),
					CreatedDate: info.ModTime().Format("2006-01-02 15:04:05"),
					FilePath:    filepath.Join(backupDir, entry.Name()),
				})
			}
		}
	}

	return items
}
func getConfigFilePath() string {
	configPath, err := constants.GetMPVConfigPathWithHome()
	if err != nil {
		return ""
	}
	return configPath
}

func (s *Server) handleShutdownAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		sendJSONError(w, http.StatusMethodNotAllowed, constants.ResponseMessageMethodNotAllowed)
		return
	}

	log.Info("Shutdown requested via Web UI")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  constants.StatusShutdown,
		"message": constants.ResponseMessageShutdown,
	})

	// Flush the response to ensure it's sent before shutdown
	if flusher, ok := w.(http.Flusher); ok {
		flusher.Flush()
	}

	// Shutdown in background to allow response to complete
	go func() {
		time.Sleep(500 * time.Millisecond)
		log.Info("Shutting down server")
		s.Shutdown()
		os.Exit(0)
	}()
}
func (s *Server) handleLanguagesLoadAPI(w http.ResponseWriter, r *http.Request) {
	audioLanguages := config.GetConfigValue(constants.ConfigKeyAudioLanguage)
	subtitleLanguages := config.GetConfigValue(constants.ConfigKeySubtitleLanguage)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"audio_languages":    audioLanguages,
		"subtitle_languages": subtitleLanguages,
	})
}

// saveLanguagePreferencesWithRetry saves audio and subtitle language preferences to mpv.conf
// with retry logic and exponential backoff (3 attempts: 1s, 2s, 3s delays)
// Returns accumulated errors if all retries fail
func saveLanguagePreferencesWithRetry(audioLanguages, subtitleLanguages []string) error {
	var errors []string
	maxRetries := 3
	delays := []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second}

	for attempt := 0; attempt < maxRetries; attempt++ {
		if attempt > 0 {
			log.Info(constants.LogPrefixAPI + fmt.Sprintf("Retrying language preferences save (attempt %d/%d)...", attempt+1, maxRetries))
			time.Sleep(delays[attempt-1])
		}

		var err error
		errors = []string{}

		// Save audio language
		if err = config.SetConfigValue(constants.ConfigKeyAudioLanguage, audioLanguages, true); err != nil {
			errors = append(errors, fmt.Sprintf("audio: %v", err))
			log.Warn(constants.LogPrefixAPI + fmt.Sprintf("Failed to save audio language (attempt %d): %v", attempt+1, err))
		} else {
			if len(audioLanguages) > 0 {
				log.Info(constants.LogPrefixAPI + "Audio language written to mpv.conf: " + strings.Join(audioLanguages, ","))
			} else {
				log.Info(constants.LogPrefixAPI + "Audio language cleared from mpv.conf")
			}
		}

		// Save subtitle language
		if err = config.SetConfigValue(constants.ConfigKeySubtitleLanguage, subtitleLanguages, true); err != nil {
			errors = append(errors, fmt.Sprintf("subtitle: %v", err))
			log.Warn(constants.LogPrefixAPI + fmt.Sprintf("Failed to save subtitle language (attempt %d): %v", attempt+1, err))
		} else {
			if len(subtitleLanguages) > 0 {
				log.Info(constants.LogPrefixAPI + "Subtitle language written to mpv.conf: " + strings.Join(subtitleLanguages, ","))
			} else {
				log.Info(constants.LogPrefixAPI + "Subtitle language cleared from mpv.conf")
			}
		}

		// If no errors, success
		if len(errors) == 0 {
			log.Info(constants.LogPrefixAPI + "Language preferences saved successfully")
			return nil
		}

		// If this is the last attempt, break and return accumulated errors
		if attempt == maxRetries-1 {
			break
		}
	}

	// Return accumulated errors
	return fmt.Errorf("failed after %d attempts: %s", maxRetries, strings.Join(errors, "; "))
}

func (s *Server) handleLanguagesSaveAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	var req LanguagesSaveRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to decode languages save request: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Languages save request received: audio_languages=" + fmt.Sprintf("%v", req.AudioLanguages) + ", subtitle_languages=" + fmt.Sprintf("%v", req.SubtitleLanguages))

	// Save language preferences with retry logic
	if err := saveLanguagePreferencesWithRetry(req.AudioLanguages, req.SubtitleLanguages); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to save language preferences after retries: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Language preferences saved successfully")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "success",
	})
}

type LanguagesSaveRequest struct {
	AudioLanguages    []string `json:"audio_languages"`
	SubtitleLanguages []string `json:"subtitle_languages"`
}

// handleAllRegionsAPI returns all regions for regional variant search
func (s *Server) handleAllRegionsAPI(w http.ResponseWriter, r *http.Request) {
	// Load locale data from assets
	locales, err := assets.ReadLocales()
	if err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	// Flatten all regions from all locales, adding parent language code
	var allRegions []ExtendedRegionalData
	for _, entry := range locales {
		for _, region := range entry.Regions {
			extendedRegion := ExtendedRegionalData{
				Locale:               region.Locale,
				CountryName:          region.CountryName,
				CountryLocal:         region.CountryLocal,
				CountryCode:          region.CountryCode,
				Flag:                 region.Flag,
				MajorRegionalVariant: region.MajorRegionalVariant,
				Population:           region.Population,
				LanguageCode:         entry.LanguageCode,
				LanguageName:         entry.LanguageName,
				LanguageLocal:        entry.LanguageLocal,
			}
			allRegions = append(allRegions, extendedRegion)
		}
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"locales": allRegions,
	})
}

// handleMajorLanguagesAPI returns list of major languages for step 1 selection
func (s *Server) handleMajorLanguagesAPI(w http.ResponseWriter, r *http.Request) {
	// Load locale data from assets
	locales, err := assets.ReadLocales()
	if err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	// Get major languages (one entry per language code, no regional variants)
	majorEntries := locale.GetMajorLanguages(locales)

	// Convert to MajorLanguageData format
	majorLanguages := make([]MajorLanguageData, len(majorEntries))
	for i := range majorEntries {
		majorLanguages[i] = MajorLanguageData{
			LanguageCode:  majorEntries[i].LanguageCode,
			LanguageName:  majorEntries[i].LanguageName,
			LanguageLocal: majorEntries[i].LanguageLocal,
			Flag:          locale.GetFlagForLanguageCode(majorEntries[i].LanguageCode),
		}
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"major_languages": majorLanguages,
	})
}

// handleRegionalVariantsAPI returns regional variants for a specific major language
func (s *Server) handleRegionalVariantsAPI(w http.ResponseWriter, r *http.Request) {
	majorCode := r.URL.Query().Get(constants.QueryParamMethodID)
	if majorCode == "" {
		sendJSONError(w, http.StatusBadRequest, "Missing major language code parameter")
		return
	}

	// Load locale data from assets
	locales, err := assets.ReadLocales()
	if err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to load locales: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, err.Error())
		return
	}

	// Find and merge all entries with matching language code
	// (handles cases like "Spanish" and "Spanish (Latin America)" both having code "es")
	majorEntry := locale.FindAndMergeByLanguageCode(majorCode, locales)
	if majorEntry == nil {
		sendJSONError(w, http.StatusNotFound, "Major language not found: "+majorCode)
		return
	}

	// Get regional variants with prioritization
	regionalVariants := locale.GetMajorRegionalVariants(majorEntry)

	// Convert to RegionalVariantData format
	variantData := make([]RegionalVariantData, len(regionalVariants))
	for i := range regionalVariants {
		variantData[i] = RegionalVariantData{
			Locale:               regionalVariants[i].Locale,
			CountryName:          regionalVariants[i].CountryName,
			CountryLocal:         regionalVariants[i].CountryLocal,
			CountryCode:          regionalVariants[i].CountryCode,
			Flag:                 regionalVariants[i].Flag,
			MajorRegionalVariant: regionalVariants[i].MajorRegionalVariant,
			Population:           regionalVariants[i].Population,
		}
	}

	// Get flag for major language
	majorLanguageFlag := locale.GetFlagForLanguageCode(majorCode)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"regional_variants": variantData,
		"major_language": map[string]interface{}{
			"language_code":  majorEntry.LanguageCode,
			"language_name":  majorEntry.LanguageName,
			"language_local": majorEntry.LanguageLocal,
			"flag":           majorLanguageFlag,
		},
	})
}

// Helper function to extract flag from locale string
func getFlagFromLocale(localeStr string) string {
	// Try to get flag for locale
	_, region := locale.FindByLocale(localeStr, []locale.LocaleEntry{})
	if region != nil && region.Flag != "" {
		return region.Flag
	}

	// Fallback: extract language code and get flag
	parts := strings.Split(localeStr, "-")
	if len(parts) > 0 {
		langCode := parts[0]
		return locale.GetFlagForLanguageCode(langCode)
	}

	return ""
}

func (s *Server) handleModalAPI(w http.ResponseWriter, r *http.Request) {
	modalType := r.URL.Query().Get(constants.QueryParamType)
	if modalType == "" {
		sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingModalType)
		return
	}

	var modalConfig ModalConfig

	switch modalType {
	case constants.ModalTypeShutdown:
		modalConfig = ModalConfig{
			Title:       "Close MPV.Rocks Installer",
			Message:     "Are you sure you want to close the MPV.Rocks Installer? The server will shut down and this tab will attempt to close automatically. If it doesn't close, please close it manually.",
			ConfirmText: "Close App",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/shutdown",
			Danger:      true,
		}
	case constants.ModalTypeUpdateManager:
		latestVersion := r.URL.Query().Get(constants.QueryParamVersion)
		if latestVersion == "" {
			latestVersion = "latest"
		}
		modalConfig = ModalConfig{
			Title:       "Update MPV Manager",
			Message:     fmt.Sprintf("Update MPV Manager to version %s?", latestVersion),
			ConfirmText: "Update Now",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/manager/update",
			Danger:      false,
		}
	case constants.ModalTypeUpdateManagerSuccess:
		modalConfig = ModalConfig{
			Title:       "Update Successful!",
			Message:     `<div class="text-center"><i class="bx bx-check-circle text-5xl text-green-400 mb-4 block"></i>MPV Manager has been updated successfully. Please close and relaunch the application to use the new version.</div>`,
			ConfirmText: "Close & Exit",
			CancelText:  "",
			ConfirmURL:  "/api/shutdown",
			Danger:      false,
			Success:     true,
		}
	case constants.ModalTypeUninstall:
		appName := r.URL.Query().Get(constants.QueryParamAppName)
		appType := r.URL.Query().Get(constants.QueryParamAppType)
		installMethod := r.URL.Query().Get(constants.QueryParamInstallMethod)

		if appName == "" {
			sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingAppName)
			return
		}

		modalConfig = ModalConfig{
			Title:       "Uninstall App",
			Message:     fmt.Sprintf("Are you sure you want to uninstall %s?", appName),
			ConfirmText: "Uninstall",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/uninstall",
			ConfirmData: fmt.Sprintf(`{"app_name": "%s", "app_type": "%s", "install_method": "%s"}`, appName, appType, installMethod),
			Danger:      true,
		}
	case constants.ModalTypeRemoveFileAssociations:
		modalConfig = ModalConfig{
			Title:       "Remove File Associations",
			Message:     "Are you sure you want to remove file associations?",
			ConfirmText: "Remove",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/file-associations/remove",
			Danger:      true,
		}
	case constants.ModalTypeResetConfig:
		modalConfig = ModalConfig{
			Title:       "Reset MPV Config",
			Message:     "This will reset your config to recommended settings. A backup will be created. Continue?",
			ConfirmText: "Reset Config",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/config/reset",
			Danger:      true,
		}
	case constants.ModalTypeRestoreConfig:
		backupPath := r.URL.Query().Get(constants.QueryParamBackupPath)
		if backupPath == "" {
			sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingBackupPath)
			return
		}

		// Validate backup path
		if err := config.ValidateBackupPath(backupPath); err != nil {
			log.Error(constants.LogPrefixAPI + " Invalid backup path: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageInvalidBackupPath)
			return
		}

		modalConfig = ModalConfig{
			Title:       "Restore Config Backup",
			Message:     "Are you sure you want to restore this configuration backup?",
			ConfirmText: "Restore",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/config/restore",
			ConfirmData: fmt.Sprintf(`{"backup_path": "%s"}`, backupPath),
			Danger:      true,
			// HTMX partial refresh configuration
			HTMXRefresh: "/api/config/backups/list",
			HTMXTarget:  "#backup-list-container",
		}
	case constants.ModalTypeApplyConfig:
		modalConfig = ModalConfig{
			Title:       "Apply Configuration Settings",
			Message:     "Apply the selected configuration settings?",
			ConfirmText: "Apply Settings",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/config/apply",
			Danger:      false,
		}
	case constants.ModalTypeDeleteBackup:
		backupPath := r.URL.Query().Get(constants.QueryParamBackupPath)
		if backupPath == "" {
			sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageMissingBackupPath)
			return
		}

		// Validate backup path
		if err := config.ValidateBackupPath(backupPath); err != nil {
			log.Error(constants.LogPrefixAPI + " Invalid backup path: " + err.Error())
			sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageInvalidBackupPath)
			return
		}

		modalConfig = ModalConfig{
			Title:       "Delete Backup",
			Message:     "Are you sure you want to delete this configuration backup?",
			ConfirmText: "Delete",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/config/backup/delete",
			ConfirmData: fmt.Sprintf(`{"backup_path": "%s"}`, backupPath),
			Danger:      true,
			// HTMX partial refresh configuration
			HTMXRefresh: "/api/config/backups/list",
			HTMXTarget:  "#backup-list-container",
		}
	case constants.ModalTypeClearLogs:
		modalConfig = ModalConfig{
			Title:       "Clear Application Log",
			Message:     "Are you sure you want to clear the application log file? This action cannot be undone.",
			ConfirmText: "Clear Log",
			CancelText:  "Cancel",
			ConfirmURL:  "/api/logs/clear",
			Danger:      true,
			// HTMX partial refresh configuration
			HTMXRefresh: "/api/logs",
			HTMXTarget:  "#log-content",
		}
	default:
		sendJSONError(w, http.StatusBadRequest, constants.ResponseMessageUnknownModalType)
		return
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(modalConfig)
}

// ============================================================================
// Job API Endpoints (New Job Manager System)
// ============================================================================

// handleJobRoutes routes /api/jobs/{id} to the appropriate handler based on method
func (s *Server) handleJobRoutes(w http.ResponseWriter, r *http.Request) {
	// Extract job ID from path: /api/jobs/{id}
	path := r.URL.Path
	prefix := "/api/jobs/"

	if path == "/api/jobs/" || path == "/api/jobs" {
		// No job ID provided
		sendJSONError(w, http.StatusBadRequest, "Job ID required")
		return
	}

	jobID := strings.TrimPrefix(path, prefix)
	if jobID == "" || strings.Contains(jobID, "/") {
		sendJSONError(w, http.StatusBadRequest, "Invalid job ID")
		return
	}

	switch r.Method {
	case http.MethodGet:
		s.handleJobGetAPI(w, r)
	case http.MethodDelete:
		s.handleJobCancelAPI(w, r)
	default:
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
	}
}

// handleJobsListAPI returns all active and recent jobs
func (s *Server) handleJobsListAPI(w http.ResponseWriter, r *http.Request) {
	activeJobs := s.jobManager.GetActiveJobs()
	recentJobs := s.jobManager.GetRecentJobs()

	// Format active jobs
	active := make([]map[string]interface{}, 0, len(activeJobs))
	for _, job := range activeJobs {
		active = append(active, jobToAPIResponse(job))
	}

	// Format recent jobs
	recent := make([]map[string]interface{}, 0, len(recentJobs))
	for _, job := range recentJobs {
		recent = append(recent, jobToAPIResponse(job))
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"active": active,
		"recent": recent,
		"total":  len(active) + len(recent),
	})
}

// handleJobGetAPI returns details for a specific job
func (s *Server) handleJobGetAPI(w http.ResponseWriter, r *http.Request) {
	// Extract job ID from path: /api/jobs/{id}
	path := r.URL.Path
	prefix := "/api/jobs/"
	if !strings.HasPrefix(path, prefix) {
		sendJSONError(w, http.StatusBadRequest, "Invalid path")
		return
	}
	jobID := strings.TrimPrefix(path, prefix)

	if jobID == "" {
		sendJSONError(w, http.StatusBadRequest, "Job ID required")
		return
	}

	job, exists := s.jobManager.GetJob(jobID)
	if !exists {
		// Also check recent jobs
		recentJobs := s.jobManager.GetRecentJobs()
		for _, rj := range recentJobs {
			if rj.ID == jobID {
				job = rj
				exists = true
				break
			}
		}
	}

	if !exists {
		sendJSONError(w, http.StatusNotFound, "Job not found")
		return
	}

	response := jobToAPIResponse(job)
	response["output"] = job.Output // Include full output for individual job

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(response)
}

// handleJobCancelAPI cancels (or removes) a job
func (s *Server) handleJobCancelAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodDelete {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	// Extract job ID from path: /api/jobs/{id}
	path := r.URL.Path
	prefix := "/api/jobs/"
	if !strings.HasPrefix(path, prefix) {
		sendJSONError(w, http.StatusBadRequest, "Invalid path")
		return
	}
	jobID := strings.TrimPrefix(path, prefix)

	if jobID == "" {
		sendJSONError(w, http.StatusBadRequest, "Job ID required")
		return
	}

	// Use the new CancelJob method which properly handles context cancellation
	err := s.jobManager.CancelJob(jobID)
	if err != nil {
		// Check if it's a "not found" error
		if strings.Contains(err.Error(), "not found") {
			sendJSONError(w, http.StatusNotFound, err.Error())
			return
		}
		// Other errors (already completed, etc.)
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Job cancelled successfully: " + jobID)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "cancelled",
		"job_id":  jobID,
		"message": "Job cancelled successfully",
	})
}

// jobToAPIResponse converts a Job to an API response map
func jobToAPIResponse(job *Job) map[string]interface{} {
	response := map[string]interface{}{
		"id":           job.ID,
		"type":         job.Type,
		"methodId":     job.MethodID,
		"appName":      job.AppName,
		"status":       job.Status,
		"progress":     job.Progress,
		"message":      job.Message,
		"startedAt":    job.StartedAt.Format(time.RFC3339),
		"durationSecs": job.Duration().Seconds(),
	}

	if job.Error != "" {
		response["error"] = job.Error
	}
	if job.ErrorDetails != "" {
		response["errorDetails"] = job.ErrorDetails
	}
	if job.CompletedAt != nil {
		response["completedAt"] = job.CompletedAt.Format(time.RFC3339)
	}

	return response
}

// ============================================================================
// Job-Based Installation (New System)
// ============================================================================

// handleInstallJobAPI creates a new job for installation (new job-based API)
// This is the new endpoint that uses JobManager instead of sessions
func (s *Server) handleInstallJobAPI(w http.ResponseWriter, r *http.Request) {
	var req InstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to decode install request: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID
	if err := ValidateMethodID(req.MethodID); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid method ID: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID is compatible with current platform
	if err := ValidateInstallMethod(req.MethodID, s.platform.IsWindows(), s.platform.IsDarwin(), s.platform.IsLinux()); err != nil {
		log.Error(constants.LogPrefixAPI + "Method not available on this platform: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Check if there's already an active job for this method
	if s.jobManager.HasActiveJob(req.MethodID) {
		log.Warn(constants.LogPrefixAPI + "Active job already exists for method: " + req.MethodID)
		sendJSONError(w, http.StatusConflict, "An installation is already in progress for this app")
		return
	}

	log.Info(constants.LogPrefixAPI + "Install job request received: method_id=" + req.MethodID + ", ui_type=" + req.UIType)

	// Determine UI type, with backwards compatibility for install_uosc
	uiType := req.UIType
	if uiType == "" {
		if req.InstallUOSC {
			uiType = constants.UITypeUOSC
		} else {
			uiType = constants.DefaultUIType
		}
	}

	// Get app name for the job
	appName := installer.GetMethodDisplayName(req.MethodID)

	// Create a new job
	job := s.jobManager.CreateJob("install", req.MethodID, appName)

	// Run the installation in a goroutine using the job system
	go s.runInstallationJob(job.ID, req.MethodID, uiType, false)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"id":     job.ID,
		"status": "started",
	})
}

// handleAppUpdateJobAPI creates a new job for app update (new job-based API)
func (s *Server) handleAppUpdateJobAPI(w http.ResponseWriter, r *http.Request) {
	var req InstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID
	if err := ValidateMethodID(req.MethodID); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid method ID: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate method ID is compatible with current platform
	if err := ValidateInstallMethod(req.MethodID, s.platform.IsWindows(), s.platform.IsDarwin(), s.platform.IsLinux()); err != nil {
		log.Error(constants.LogPrefixAPI + "Method not available on this platform: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Check if there's already an active job for this method
	if s.jobManager.HasActiveJob(req.MethodID) {
		log.Warn(constants.LogPrefixAPI + "Active job already exists for method: " + req.MethodID)
		sendJSONError(w, http.StatusConflict, "An update is already in progress for this app")
		return
	}

	appName := installer.GetMethodDisplayName(req.MethodID)

	// For updates, use the provided UI type or default
	uiType := req.UIType
	if uiType == "" {
		uiType = constants.DefaultUIType
	}

	// Create a new job
	job := s.jobManager.CreateJob("update", req.MethodID, appName)

	// Run the update in a goroutine
	go s.runInstallationJob(job.ID, req.MethodID, uiType, true)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"id":     job.ID,
		"status": "started",
	})
}

// handleUninstallJobAPI creates a new job for app uninstallation (new job-based API)
func (s *Server) handleUninstallJobAPI(w http.ResponseWriter, r *http.Request) {
	var req UninstallRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to decode uninstall request: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Validate app name
	if err := ValidateAppName(req.AppName); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid app name: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Normalize and validate app type
	normalizedAppType := NormalizeAppType(req.AppType)
	if err := ValidateAppType(req.AppType); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid app type: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}
	req.AppType = normalizedAppType

	// Validate install method
	if err := ValidateMethodID(req.InstallMethod); err != nil {
		log.Error(constants.LogPrefixAPI + "Invalid install method: " + err.Error())
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	// Check if there's already an active job for this method
	if s.jobManager.HasActiveJob(req.InstallMethod) {
		log.Warn(constants.LogPrefixAPI + "Active job already exists for method: " + req.InstallMethod)
		sendJSONError(w, http.StatusConflict, "An operation is already in progress for this app")
		return
	}

	log.Info(constants.LogPrefixAPI + "Uninstall job request received: app_name=" + req.AppName + ", install_method=" + req.InstallMethod)

	// Create a new job
	job := s.jobManager.CreateJob("uninstall", req.InstallMethod, req.AppName)

	// Run the uninstallation in a goroutine
	go s.runUninstallationJob(job.ID, req)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"id":     job.ID,
		"status": "started",
	})
}

// runUninstallationJob executes an uninstallation using the job manager system
func (s *Server) runUninstallationJob(jobID string, req UninstallRequest) {
	// Get the job to access its context
	job, exists := s.jobManager.GetJob(jobID)
	if !exists {
		log.Error(constants.LogPrefixAPI + "Job not found: " + jobID)
		return
	}

	p := s.platform
	inst := s.installer
	ctx := job.Context()

	// Update progress to indicate starting
	s.jobManager.UpdateProgress(jobID, 5, "Starting uninstallation...")

	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	// Goroutine to process output lines
	go func() {
		for output := range outputChan {
			s.jobManager.AddOutput(jobID, output)
		}
	}()

	// Goroutine to process errors
	go func() {
		for err := range errorChan {
			s.jobManager.AddOutput(jobID, "ERROR: "+err.Error())
		}
	}()

	// Check for cancellation before proceeding
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("uninstallation cancelled"), "Job was cancelled before it could start")
		return
	}

	// Update progress
	s.jobManager.UpdateProgress(jobID, 10, "Executing uninstallation commands...")

	app := config.InstalledApp{
		AppName:       req.AppName,
		AppType:       req.AppType,
		InstallMethod: req.InstallMethod,
	}

	handler := installer.NewInstallationHandler(p, inst)
	err := handler.ExecuteUninstall(ctx, cr, &app)

	// Check if cancelled during execution
	if ctx.Err() != nil {
		s.jobManager.SetError(jobID, fmt.Errorf("uninstallation cancelled"), "Job was cancelled during execution")
		log.Error(constants.LogPrefixAPI + "Uninstallation job cancelled: " + jobID)
		return
	}

	if err != nil {
		errorDetails := ""
		if strings.Contains(err.Error(), "exit status") {
			errorDetails = "Command execution failed. Check the output for details."
		} else if strings.Contains(err.Error(), "permission denied") {
			errorDetails = "Permission denied. Try running with elevated privileges."
		}

		s.jobManager.SetError(jobID, err, errorDetails)
		log.Error(constants.LogPrefixAPI + "Uninstallation job failed: " + err.Error())
		return
	}

	// Remove from installed apps config
	if err := config.RemoveInstalledApp(req.AppName); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to remove app from config: " + err.Error())
		s.jobManager.AddOutput(jobID, "Warning: Failed to remove app from configuration")
	} else {
		log.Info(constants.LogPrefixAPI + "Successfully uninstalled: " + req.AppName)
		s.jobManager.AddOutput(jobID, "App removed from configuration successfully")
	}

	// Mark job as complete
	s.jobManager.UpdateProgress(jobID, 100, "Uninstallation completed successfully")
	s.jobManager.CompleteJob(jobID)
	log.Info(constants.LogPrefixAPI + "Uninstallation job completed: " + jobID)
}

// runInstallationJob executes an installation using the job manager system
func (s *Server) runInstallationJob(jobID, methodID string, uiType string, isUpdate bool) {
	// Get the job to access its context
	job, exists := s.jobManager.GetJob(jobID)
	if !exists {
		log.Error(constants.LogPrefixAPI + "Job not found: " + jobID)
		return
	}

	p := s.platform
	inst := s.installer
	ctx := job.Context()

	// Update progress to indicate starting
	s.jobManager.UpdateProgress(jobID, 5, "Starting installation...")

	outputChan := make(chan string, 1000)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	// Goroutine to process output lines
	go func() {
		for output := range outputChan {
			// Add output to job (this will also try to parse progress)
			s.jobManager.AddOutput(jobID, output)
		}
	}()

	// Goroutine to process errors
	go func() {
		for err := range errorChan {
			s.jobManager.AddOutput(jobID, "ERROR: "+err.Error())
		}
	}()

	// Check for cancellation before proceeding
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("installation cancelled"), "Job was cancelled before it could start")
		return
	}

	// Update progress
	s.jobManager.UpdateProgress(jobID, 10, "Executing installation commands...")

	handler := installer.NewInstallationHandler(p, inst)
	err := handler.ExecuteInstall(ctx, cr, methodID, uiType, isUpdate)

	// Check if cancelled during execution
	if ctx.Err() != nil {
		s.jobManager.SetError(jobID, fmt.Errorf("installation cancelled"), "Job was cancelled during execution")
		log.Error(constants.LogPrefixAPI + "Installation job cancelled: " + jobID)
		return
	}

	if err != nil {
		// Extract error details for more context
		errorDetails := ""
		if strings.Contains(err.Error(), "exit status") {
			errorDetails = "Command execution failed. Check the output for details."
		} else if strings.Contains(err.Error(), "permission denied") {
			errorDetails = "Permission denied. Try running with elevated privileges."
		} else if strings.Contains(err.Error(), "network") || strings.Contains(err.Error(), "connection") {
			errorDetails = "Network error. Check your internet connection."
		}

		s.jobManager.SetError(jobID, err, errorDetails)
		log.Error(constants.LogPrefixAPI + "Installation job failed: " + err.Error())
		return
	}

	// Success path
	if !isUpdate {
		// Only save to config for new installations, not updates
		appName := installer.GetMethodDisplayName(methodID)
		appType := constants.MethodAppType[methodID]
		installPath := getInstallPathForMethod(methodID, p, config.GetInstallPath())

		// Only save version for portable/binary apps we manage
		// Package manager apps (flatpak, brew, apt) are checked at startup
		appVersion := ""
		if appType == "app" {
			appVersion = getAppVersionForMethod(methodID, inst, p)
		}

		app := config.InstalledApp{
			AppName:       appName,
			AppType:       appType,
			InstallMethod: methodID,
			InstallPath:   installPath,
			InstalledDate: time.Now().Format("2006-01-02 15:04:05"),
			AppVersion:    appVersion,
			UIType:        uiType, // Save UI type for MPV apps
			Managed:       true,   // Installed and managed by MPV Manager
		}

		if err := config.AddInstalledApp(app); err != nil {
			log.Error(constants.LogPrefixAPI + "Failed to save installed app: " + err.Error())
			s.jobManager.AddOutput(jobID, "Warning: Failed to save app to configuration")
		} else {
			log.Info(constants.LogPrefixAPI + "Successfully saved installed app: " + appName)
			s.jobManager.AddOutput(jobID, "App saved to configuration successfully")

			// Update version cache for package manager apps
			if isPackageManagerApp(methodID) {
				versionInfo := GetPackageVersion(methodID, appName)
				typeTag := getTypeTag(methodID)

				s.versionCacheMux.Lock()
				if s.versionCache.PackageVersionMap == nil {
					s.versionCache.PackageVersionMap = make(map[string]AppVersionInfo)
				}
				s.versionCache.PackageVersionMap[methodID] = AppVersionInfo{
					CurrentVersion:   versionInfo,
					AvailableVersion: "Check via " + typeTag,
				}
				s.versionCacheMux.Unlock()

				log.Info(constants.LogPrefixAPI + "Updated version cache for " + methodID + ": " + versionInfo)
			}
		}
	}

	// Mark job as complete
	s.jobManager.UpdateProgress(jobID, 100, "Installation completed successfully")
	s.jobManager.CompleteJob(jobID)
	log.Info(constants.LogPrefixAPI + "Installation job completed: " + jobID)
}

// ============================================================================
// UI Selection API Endpoints
// ============================================================================

// UIChangeRequest for the change UI API
type UIChangeRequest struct {
	AppName string `json:"app_name"`
	UIType  string `json:"ui_type"`
}

// handleUIOptionsAPI returns UI options for the selection modal
// GET /api/ui/options?method_id=mpv-binary&app_name=MPV
func (s *Server) handleUIOptionsAPI(w http.ResponseWriter, r *http.Request) {
	methodID := r.URL.Query().Get(constants.QueryParamMethodID)
	appName := r.URL.Query().Get(constants.QueryParamAppName)

	// Check if this method needs UI selection (MPV-based apps only)
	needsUI := methodNeedsUISelection(methodID)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"needs_ui_selection": needsUI,
		"method_id":          methodID,
		"app_name":           appName,
		"options": []map[string]interface{}{
			{
				"id":          constants.UITypeUOSC,
				"name":        "UOSC",
				"description": "Feature-rich modern UI with touch support",
				"recommended": true,
			},
			{
				"id":          constants.UITypeModernZ,
				"name":        "ModernZ",
				"description": "Classic OSC look with modern features",
				"recommended": false,
			},
			{
				"id":          constants.UITypeNone,
				"name":        "No UI Overlay",
				"description": "Use MPV's default interface",
				"recommended": false,
			},
		},
	})
}

// methodNeedsUISelection returns true if the installation method needs UI selection
// Only MPV-based methods need UI selection; IINA, MPC-QT, Celluloid, Infuse have their own UIs
func methodNeedsUISelection(methodID string) bool {
	mpvMethods := []string{
		constants.MethodMPVBinary,
		constants.MethodMPVBinaryV3,
		constants.MethodMPVFlatpak,
		constants.MethodMPVPackage,
		constants.MethodMPVBrew,
		constants.MethodMPVApp,
	}
	for _, m := range mpvMethods {
		if methodID == m {
			return true
		}
	}
	return false
}

// handleUIChangeAPI handles requests to change the UI for an installed app
// POST /api/ui/change
func (s *Server) handleUIChangeAPI(w http.ResponseWriter, r *http.Request) {
	var req UIChangeRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Validate app name
	if req.AppName == "" {
		sendJSONError(w, http.StatusBadRequest, "Missing app_name")
		return
	}

	// Validate UI type
	if req.UIType != constants.UITypeUOSC &&
		req.UIType != constants.UITypeModernZ &&
		req.UIType != constants.UITypeNone {
		sendJSONError(w, http.StatusBadRequest, "Invalid UI type. Must be 'uosc', 'modernz', or 'none'")
		return
	}

	// Find the installed app
	apps := config.GetInstalledApps()
	var foundApp *config.InstalledApp
	for i := range apps {
		if apps[i].AppName == req.AppName {
			foundApp = &apps[i]
			break
		}
	}

	if foundApp == nil {
		sendJSONError(w, http.StatusNotFound, "App not found: "+req.AppName)
		return
	}

	// Check if this method supports UI selection
	if !methodNeedsUISelection(foundApp.InstallMethod) {
		sendJSONError(w, http.StatusBadRequest, "This app does not support UI customization")
		return
	}

	log.Info(constants.LogPrefixAPI + "UI change request received: app_name=" + req.AppName + ", ui_type=" + req.UIType)

	// Check if there's already an active job for this app
	if s.jobManager.HasActiveJob(foundApp.InstallMethod) {
		log.Warn(constants.LogPrefixAPI + "Active job already exists for method: " + foundApp.InstallMethod)
		sendJSONError(w, http.StatusConflict, "An operation is already in progress for this app")
		return
	}

	// Create a new job for UI change
	job := s.jobManager.CreateJob("ui-change", foundApp.InstallMethod, req.AppName)

	// Run the UI change in a goroutine
	go s.runUIChangeJob(job.ID, foundApp, req.UIType)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"id":     job.ID,
		"status": "started",
	})
}

// runUIChangeJob executes a UI change operation using the job manager system
func (s *Server) runUIChangeJob(jobID string, app *config.InstalledApp, newUIType string) {
	// Get the job to access its context
	job, exists := s.jobManager.GetJob(jobID)
	if !exists {
		log.Error(constants.LogPrefixAPI + "Job not found: " + jobID)
		return
	}

	ctx := job.Context()

	// Update progress to indicate starting
	s.jobManager.UpdateProgress(jobID, 5, "Starting UI change...")

	// Create output channels
	outputChan := make(chan string, 100)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	// Goroutine to process output lines
	go func() {
		for output := range outputChan {
			s.jobManager.AddOutput(jobID, output)
		}
	}()

	// Goroutine to process errors
	go func() {
		for err := range errorChan {
			s.jobManager.AddOutput(jobID, "ERROR: "+err.Error())
		}
	}()

	// Check for cancellation before proceeding
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("UI change cancelled"), "Job was cancelled before it could start")
		return
	}

	// Determine old UI type (default to UOSC if not set)
	oldUIType := app.UIType
	if oldUIType == "" {
		oldUIType = constants.UITypeUOSC
	}

	// Get config directory for this app
	configDir := getConfigDirForApp(app)

	s.jobManager.UpdateProgress(jobID, 10, fmt.Sprintf("Changing UI from %s to %s...", oldUIType, newUIType))
	s.jobManager.AddOutput(jobID, fmt.Sprintf("Current UI: %s", oldUIType))
	s.jobManager.AddOutput(jobID, fmt.Sprintf("Target UI: %s", newUIType))

	// Check for cancellation
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("UI change cancelled"), "Job was cancelled during execution")
		return
	}

	// Perform UI change if different from current type
	if oldUIType != newUIType {
		s.jobManager.UpdateProgress(jobID, 20, "Removing old UI files...")

		// Check for cancellation
		if job.IsCancelled() {
			s.jobManager.SetError(jobID, fmt.Errorf("UI change cancelled"), "Job was cancelled during execution")
			return
		}

		// Install new UI (InstallUISafely handles removing old files first)
		s.jobManager.UpdateProgress(jobID, 40, "Installing new UI...")
		installer.InstallUISafely(s.installer, cr, configDir, newUIType)
		s.jobManager.AddOutput(jobID, "New UI installed")
	} else {
		s.jobManager.AddOutput(jobID, "UI type unchanged, nothing to do")
	}

	// Check for errors that may have occurred during installation
	select {
	case err := <-errorChan:
		s.jobManager.SetError(jobID, err, "Failed to install UI")
		log.Error(constants.LogPrefixAPI + "UI change job failed: " + err.Error())
		return
	default:
		// No errors
	}

	// Update app's UI type in config
	uiTypeToSave := newUIType
	if newUIType == constants.UITypeNone {
		uiTypeToSave = ""
	}

	if err := config.UpdateInstalledAppUIType(app.AppName, uiTypeToSave); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to update UI type in configuration: "+err.Error())
		log.Error(constants.LogPrefixAPI + "Failed to update app UI type: " + err.Error())
	} else {
		s.jobManager.AddOutput(jobID, "Configuration updated successfully")
	}

	// Mark job as complete
	s.jobManager.UpdateProgress(jobID, 100, "UI change completed successfully")
	s.jobManager.CompleteJob(jobID)
	log.Info(constants.LogPrefixAPI + "UI change job completed: " + jobID)
}

// getConfigDirForApp returns the config directory path for an installed app
func getConfigDirForApp(app *config.InstalledApp) string {
	homeDir, _ := os.UserHomeDir()

	switch app.InstallMethod {
	case constants.MethodMPVBinary, constants.MethodMPVBinaryV3:
		// Windows portable installs use portable_config inside the install directory
		return filepath.Join(app.InstallPath, constants.PortableConfigDir)
	default:
		// macOS MPV.app, Flatpak, Package Manager, Brew all use ~/.config/mpv
		return filepath.Join(homeDir, ".config", "mpv")
	}
}

// ============================================================================
// Keyring API Endpoints (for sudo password storage)
// ============================================================================

// handleKeyringAuthRoutes routes /api/keyring/auth based on HTTP method
// POST -> store password, DELETE -> clear password
func (s *Server) handleKeyringAuthRoutes(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodPost:
		s.handleKeyringAuthStore(w, r)
	case http.MethodDelete:
		s.handleKeyringAuthClear(w, r)
	default:
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
	}
}

// handleKeyringStatusAPI returns keyring status information
// GET /api/keyring/status
func (s *Server) handleKeyringStatusAPI(w http.ResponseWriter, r *http.Request) {
	status := keyring.DetectStatus()

	// Open keyring to check if password is stored
	kr, err := keyring.Open()
	hasPassword := false
	if err == nil {
		hasPassword = kr.HasPassword()
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"availableBackends": status.AvailableBackends,
		"activeBackend":     status.ActiveBackend,
		"daemonRunning":     status.DaemonRunning,
		"hasPassword":       hasPassword,
		"installHint":       status.InstallHint,
		"desktopEnv":        status.DesktopEnv,
		"distro":            status.Distro,
		"needsSetup":        keyring.NeedsInstallPrompt(),
		"platform":          status.Platform,
	})
}

// handleKeyringAuthStore stores sudo password after validating it
// POST /api/keyring/auth
func (s *Server) handleKeyringAuthStore(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Password string `json:"password"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	if req.Password == "" {
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"success": false,
			"error":   "Password is required",
		})
		return
	}

	// On Linux, validate password with sudo before storing
	if runtime.GOOS == "linux" {
		cmd := exec.Command("sudo", "-S", "-v")
		cmd.Stdin = bytes.NewBufferString(req.Password + "\n")
		var stdout, stderr bytes.Buffer
		cmd.Stdout = &stdout
		cmd.Stderr = &stderr

		err := cmd.Run()
		stdoutStr := stdout.String()
		stderrStr := stderr.String()

		if err != nil {
			log.Debug(fmt.Sprintf("Keyring auth validation failed: err=%v, stdout=%q, stderr=%q", err, stdoutStr, stderrStr))
			log.Warn(constants.LogPrefixAPI + "Keyring auth: invalid password provided")
			w.Header().Set("Content-Type", constants.ContentTypeJSON)
			json.NewEncoder(w).Encode(map[string]interface{}{
				"success": false,
				"error":   "Invalid password",
			})
			return
		}

		log.Debug(fmt.Sprintf("Keyring auth validation succeeded: stdout=%q, stderr=%q", stdoutStr, stderrStr))
	}

	// Open keyring
	kr, err := keyring.Open()
	if err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to open keyring: " + err.Error())
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"success": false,
			"error":   "Failed to open keyring: " + err.Error(),
		})
		return
	}

	// Store in keyring
	if err := kr.StorePassword(req.Password); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to store password: " + err.Error())
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"success": false,
			"error":   "Failed to store password: " + err.Error(),
		})
		return
	}

	log.Info(constants.LogPrefixAPI + "Password stored in keyring successfully")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": true,
	})
}

// handleKeyringAuthClear clears stored password
// DELETE /api/keyring/auth
func (s *Server) handleKeyringAuthClear(w http.ResponseWriter, r *http.Request) {
	// Open keyring
	kr, err := keyring.Open()
	if err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to open keyring: " + err.Error())
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"success": false,
			"error":   "Failed to open keyring: " + err.Error(),
		})
		return
	}

	if err := kr.DeletePassword(); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to delete password: " + err.Error())
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
		return
	}

	log.Info(constants.LogPrefixAPI + "Password deleted from keyring successfully")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": true,
	})
}

// ============================================================================
// Settings API Endpoints
// ============================================================================

// handleSettingsAPI returns current settings for the settings page
// GET /api/settings
func (s *Server) handleSettingsAPI(w http.ResponseWriter, r *http.Request) {
	configPath := getConfigFilePath()

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"desktop_shortcut_enabled": config.GetCreateManagerShortcut(),
		"config_file_path":         configPath,
	})
}

// handleSettingsShortcutAPI toggles desktop shortcut preference
// POST /api/settings/shortcut
func (s *Server) handleSettingsShortcutAPI(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Enabled bool `json:"enabled"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Update the preference
	if err := config.SetCreateManagerShortcut(req.Enabled); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to set shortcut preference: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, "Failed to save preference")
		return
	}

	log.Info(fmt.Sprintf(constants.LogPrefixAPI+"Desktop shortcut preference set to: %v", req.Enabled))

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "success",
		"enabled": req.Enabled,
	})
}

// handleSettingsResetAPI resets the mpv-manager.json file to defaults
// POST /api/settings/reset
func (s *Server) handleSettingsResetAPI(w http.ResponseWriter, r *http.Request) {
	// Use config package to reset
	if err := config.ResetToDefaults(); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to reset config: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, "Failed to reset config file")
		return
	}

	log.Info(constants.LogPrefixAPI + "MPV Manager data reset successfully")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "success",
		"message": "MPV Manager data has been reset to defaults",
	})
}

// ============================================================================
// Adopt App API Endpoints
// ============================================================================

// AdoptAppRequest represents a request to adopt an externally installed app
type AdoptAppRequest struct {
	AppName       string `json:"app_name"`
	InstallMethod string `json:"install_method"`
	UIType        string `json:"ui_type"`
}

// handleAdoptAppAPI handles requests to adopt an externally installed app
// POST /api/apps/adopt
// This configures mpv.conf, optionally installs UI overlays, and marks the app as managed
func (s *Server) handleAdoptAppAPI(w http.ResponseWriter, r *http.Request) {
	var req AdoptAppRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Validate app name
	if req.AppName == "" {
		sendJSONError(w, http.StatusBadRequest, "Missing app_name")
		return
	}

	// Validate install method
	if req.InstallMethod == "" {
		sendJSONError(w, http.StatusBadRequest, "Missing install_method")
		return
	}

	// Validate UI type (default to none if not provided)
	if req.UIType == "" {
		req.UIType = constants.UITypeNone
	}

	if req.UIType != constants.UITypeUOSC &&
		req.UIType != constants.UITypeModernZ &&
		req.UIType != constants.UITypeNone {
		sendJSONError(w, http.StatusBadRequest, "Invalid UI type. Must be 'uosc', 'modernz', or 'none'")
		return
	}

	// Find the installed app
	apps := config.GetInstalledApps()
	var foundApp *config.InstalledApp
	for i := range apps {
		if apps[i].AppName == req.AppName && apps[i].InstallMethod == req.InstallMethod {
			foundApp = &apps[i]
			break
		}
	}

	if foundApp == nil {
		sendJSONError(w, http.StatusNotFound, "App not found: "+req.AppName)
		return
	}

	// Check if already managed
	if foundApp.Managed {
		sendJSONError(w, http.StatusBadRequest, "App is already managed by MPV Manager")
		return
	}

	log.Info(constants.LogPrefixAPI + "Adopt app request received: app_name=" + req.AppName + ", method=" + req.InstallMethod + ", ui_type=" + req.UIType)

	// Check if there's already an active job for this app
	if s.jobManager.HasActiveJob(foundApp.InstallMethod) {
		log.Warn(constants.LogPrefixAPI + "Active job already exists for method: " + foundApp.InstallMethod)
		sendJSONError(w, http.StatusConflict, "An operation is already in progress for this app")
		return
	}

	// Create a new job for adoption
	job := s.jobManager.CreateJob("adopt", foundApp.InstallMethod, req.AppName)

	// Run the adoption in a goroutine
	go s.runAdoptAppJob(job.ID, foundApp, req.UIType)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"id":     job.ID,
		"status": "started",
	})
}

// runAdoptAppJob executes an app adoption operation using the job manager system
func (s *Server) runAdoptAppJob(jobID string, app *config.InstalledApp, uiType string) {
	// Get the job to access its context
	job, exists := s.jobManager.GetJob(jobID)
	if !exists {
		log.Error(constants.LogPrefixAPI + "Job not found: " + jobID)
		return
	}

	ctx := job.Context()

	// Update progress to indicate starting
	s.jobManager.UpdateProgress(jobID, 5, "Starting app adoption...")
	s.jobManager.AddOutput(jobID, fmt.Sprintf("Adopting %s (%s)", app.AppName, app.InstallMethod))

	// Create output channels
	outputChan := make(chan string, 100)
	errorChan := make(chan error, 10)
	cr := installer.NewCommandRunnerWithContext(ctx, outputChan, errorChan)

	// Goroutine to process output lines
	go func() {
		for output := range outputChan {
			s.jobManager.AddOutput(jobID, output)
		}
	}()

	// Goroutine to process errors
	go func() {
		for err := range errorChan {
			s.jobManager.AddOutput(jobID, "ERROR: "+err.Error())
		}
	}()

	// Check for cancellation before proceeding
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("adoption cancelled"), "Job was cancelled before it could start")
		return
	}

	// Get home directory
	homeDir, err := os.UserHomeDir()
	if err != nil {
		s.jobManager.SetError(jobID, err, "Failed to get home directory")
		return
	}
	configDir := installer.GetMPVConfigDirFromHome(homeDir)

	// Step 1: Configure mpv.conf
	s.jobManager.UpdateProgress(jobID, 20, "Configuring mpv.conf...")
	installer.InstallMPVConfigSafely(s.installer, cr)
	s.jobManager.AddOutput(jobID, "mpv.conf configured")

	// Check for cancellation
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("adoption cancelled"), "Job was cancelled during execution")
		return
	}

	// Step 2: Configure Flatpak-specific settings if applicable
	if app.InstallMethod == constants.MethodMPVFlatpak {
		s.jobManager.UpdateProgress(jobID, 40, "Configuring Flatpak permissions...")
		s.jobManager.AddOutput(jobID, "Granting filesystem access to mpv config...")
		if err := cr.RunCommand("flatpak", "override", "--user", "--filesystem=xdg-config/mpv:ro", constants.FlatpakMPV); err != nil {
			s.jobManager.AddOutput(jobID, "Warning: Failed to configure Flatpak: "+err.Error())
		} else {
			s.jobManager.AddOutput(jobID, "Flatpak permissions configured")
		}
	} else if app.InstallMethod == constants.MethodCelluloidFlatpak {
		s.jobManager.UpdateProgress(jobID, 40, "Configuring Celluloid Flatpak...")
		if err := s.configureCelluloidFlatpakJob(jobID, cr, homeDir); err != nil {
			s.jobManager.AddOutput(jobID, "Warning: Failed to configure Celluloid: "+err.Error())
		} else {
			s.jobManager.AddOutput(jobID, "Celluloid configured")
		}
	} else if app.InstallMethod == constants.MethodCelluloidPackage {
		s.jobManager.UpdateProgress(jobID, 40, "Configuring Celluloid...")
		if err := s.configureCelluloidPackageJob(jobID, cr, homeDir); err != nil {
			s.jobManager.AddOutput(jobID, "Warning: Failed to configure Celluloid: "+err.Error())
		} else {
			s.jobManager.AddOutput(jobID, "Celluloid configured")
		}
	}

	// Check for cancellation
	if job.IsCancelled() {
		s.jobManager.SetError(jobID, fmt.Errorf("adoption cancelled"), "Job was cancelled during execution")
		return
	}

	// Step 3: Install UI overlay if selected (only for MPV-based apps)
	if methodNeedsUISelection(app.InstallMethod) && uiType != constants.UITypeNone {
		s.jobManager.UpdateProgress(jobID, 60, "Installing UI overlay...")
		installer.InstallUISafely(s.installer, cr, configDir, uiType)
		s.jobManager.AddOutput(jobID, fmt.Sprintf("UI overlay '%s' installed", uiType))
	}

	// Check for errors that may have occurred
	select {
	case err := <-errorChan:
		s.jobManager.SetError(jobID, err, "Failed during adoption")
		log.Error(constants.LogPrefixAPI + "Adopt job failed: " + err.Error())
		return
	default:
		// No errors
	}

	// Step 4: Update app in config to mark as managed
	s.jobManager.UpdateProgress(jobID, 90, "Updating configuration...")
	uiTypeToSave := uiType
	if uiType == constants.UITypeNone {
		uiTypeToSave = ""
	}

	if err := config.UpdateInstalledAppManaged(app.AppName, app.InstallMethod, true, uiTypeToSave); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to update app in configuration: "+err.Error())
		log.Error(constants.LogPrefixAPI + "Failed to update app managed status: " + err.Error())
	} else {
		s.jobManager.AddOutput(jobID, "App marked as managed")
	}

	// Mark job as complete
	s.jobManager.CompleteJob(jobID)
	s.jobManager.AddOutput(jobID, "App adopted successfully!")
	log.Info(constants.LogPrefixAPI + "Adopt job completed successfully: " + app.AppName)
}

// configureCelluloidFlatpakJob configures Celluloid Flatpak with mpv config path (for job system)
func (s *Server) configureCelluloidFlatpakJob(jobID string, cr *installer.CommandRunner, homeDir string) error {
	s.jobManager.AddOutput(jobID, "Configuring Celluloid Flatpak...")

	// Grant filesystem access
	s.jobManager.AddOutput(jobID, "Granting filesystem access to mpv config...")
	if err := cr.RunCommand("flatpak", "override", "--user", "--filesystem=xdg-config/mpv:ro", constants.FlatpakCelluloid); err != nil {
		return err
	}

	// Set mpv-config-enable
	s.jobManager.AddOutput(jobID, "Setting mpv-config-enable to true...")
	if err := cr.RunCommand("flatpak", "run", "--command=gsettings", constants.FlatpakCelluloid, "set", constants.GSettingsCelluloidSchema, "mpv-config-enable", "true"); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to set gsettings (Celluloid may not have been run yet)")
	}

	// Set mpv-config-file
	configPath := installer.GetMPVConfigDirFromHome(homeDir) + "/mpv.conf"
	s.jobManager.AddOutput(jobID, fmt.Sprintf("Setting mpv-config-file to: %s", configPath))
	if err := cr.RunCommand("flatpak", "run", "--command=gsettings", constants.FlatpakCelluloid, "set", constants.GSettingsCelluloidSchema, "mpv-config-file", configPath); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to set gsettings mpv-config-file")
	}

	return nil
}

// configureCelluloidPackageJob configures Celluloid (package manager version) with mpv config path (for job system)
func (s *Server) configureCelluloidPackageJob(jobID string, cr *installer.CommandRunner, homeDir string) error {
	s.jobManager.AddOutput(jobID, "Configuring Celluloid settings...")

	// Check if gsettings is available
	if _, err := exec.LookPath("gsettings"); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: gsettings not found, skipping Celluloid configuration")
		return nil
	}

	// Set mpv-config-enable
	s.jobManager.AddOutput(jobID, "Setting mpv-config-enable to true...")
	if err := cr.RunCommand("gsettings", "set", "io.github.celluloid-player.Celluloid", "mpv-config-enable", "true"); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to set gsettings (Celluloid may not have been run yet)")
	}

	// Set mpv-config-file
	configPath := installer.GetMPVConfigDirFromHome(homeDir) + "/mpv.conf"
	s.jobManager.AddOutput(jobID, fmt.Sprintf("Setting mpv-config-file to: %s", configPath))
	if err := cr.RunCommand("gsettings", "set", "io.github.celluloid-player.Celluloid", "mpv-config-file", configPath); err != nil {
		s.jobManager.AddOutput(jobID, "Warning: Failed to set gsettings mpv-config-file")
	}

	return nil
}

// ============================================================================
// Hotkey Preset API Endpoints
// ============================================================================

// handleHotkeyPresetsAPI returns the list of available hotkey preset profiles.
// GET /api/hotkeys/presets
func (s *Server) handleHotkeyPresetsAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	presets := hotkeys.GetPresetList()

	// Determine the active preset by comparing the current input.conf against presets.
	activePreset := detectActivePreset()

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"presets":       presets,
		"active_preset": activePreset,
	})
}

// handleHotkeyApplyPresetAPI applies a hotkey preset profile.
// POST /api/hotkeys/apply
func (s *Server) handleHotkeyApplyPresetAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	var req struct {
		Preset string `json:"preset"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		sendJSONError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Validate preset name
	if err := ValidateHotkeyPreset(req.Preset); err != nil {
		sendJSONError(w, http.StatusBadRequest, err.Error())
		return
	}

	configPath := hotkeys.GetInputConfPath()
	if configPath == "" {
		sendJSONError(w, http.StatusInternalServerError, "Failed to determine input.conf path")
		return
	}

	if err := hotkeys.ApplyPreset(req.Preset, configPath); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to apply hotkey preset: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, "Failed to apply preset: "+err.Error())
		return
	}

	// Find the display name for the preset
	presetName := req.Preset
	for _, p := range hotkeys.GetPresetList() {
		if p.ID == req.Preset {
			presetName = p.Name
			break
		}
	}

	log.Info(constants.LogPrefixAPI + "Applied hotkey preset: " + req.Preset)

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": true,
		"message": presetName + " keybindings applied successfully",
	})
}

// handleHotkeyResetAPI resets keybindings to mpv defaults.
// POST /api/hotkeys/reset
func (s *Server) handleHotkeyResetAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	configPath := hotkeys.GetInputConfPath()
	if configPath == "" {
		sendJSONError(w, http.StatusInternalServerError, "Failed to determine input.conf path")
		return
	}

	if err := hotkeys.ApplyPreset("default", configPath); err != nil {
		log.Error(constants.LogPrefixAPI + "Failed to reset hotkeys: " + err.Error())
		sendJSONError(w, http.StatusInternalServerError, "Failed to reset keybindings: "+err.Error())
		return
	}

	log.Info(constants.LogPrefixAPI + "Reset keybindings to mpv defaults")

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"success": true,
		"message": "Keybindings reset to mpv defaults",
	})
}

// handleHotkeyCurrentAPI returns the user's current keybindings.
// GET /api/hotkeys/current
func (s *Server) handleHotkeyCurrentAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
		return
	}

	bindings, err := hotkeys.GetActiveBindings()
	if err != nil {
		sendJSONError(w, http.StatusInternalServerError, "Failed to read keybindings: "+err.Error())
		return
	}

	// Detect if user has a custom (non-preset) config
	hasCustom := detectActivePreset() == ""

	// Convert bindings to a serializable format
	type bindingEntry struct {
		Key     string `json:"key"`
		Command string `json:"command"`
		Comment string `json:"comment"`
	}

	entries := make([]bindingEntry, 0, len(bindings))
	for _, b := range bindings {
		entries = append(entries, bindingEntry{
			Key:     b.Key,
			Command: b.Command,
			Comment: b.Comment,
		})
	}

	w.Header().Set("Content-Type", constants.ContentTypeJSON)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"bindings":      entries,
		"has_custom":    hasCustom,
		"binding_count": len(entries),
	})
}

// detectActivePreset compares the current input.conf to each known preset
// and returns the preset ID if it matches, or "" for a custom config.
// It compares raw file bytes against raw preset bytes to avoid false
// negatives caused by parse→serialize whitespace normalization.
func detectActivePreset() string {
	configPath := hotkeys.GetInputConfPath()
	if configPath == "" {
		return ""
	}

	rawFile, err := os.ReadFile(configPath)
	if err != nil || len(rawFile) == 0 {
		// No file or empty file means mpv uses built-in defaults
		return "default"
	}

	fileContent := strings.TrimSpace(string(rawFile))

	for _, preset := range hotkeys.GetPresetList() {
		presetContent, err := hotkeys.LoadPreset(preset.ID)
		if err != nil {
			continue
		}
		if strings.TrimSpace(presetContent) == fileContent {
			return preset.ID
		}
	}

	// No preset matched — it's a custom config
	return ""
}

// handleNoticeAPI returns and clears any pending notice for the Web UI
// GET returns the notice, DELETE clears it
func (s *Server) handleNoticeAPI(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		notice := s.GetPendingNotice()
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"notice": notice,
		})
	case http.MethodDelete:
		s.GetAndClearPendingNotice()
		w.Header().Set("Content-Type", constants.ContentTypeJSON)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"status": "cleared",
		})
	default:
		sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed")
	}
}
