package installer

import (
	"context"
	"embed"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"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/log"
	"gitgud.io/mike/mpv-manager/pkg/platform"
)

var pendingNotice string
var pendingNoticeMux sync.RWMutex

// SetPendingNotice sets a pending notice for the Web UI (cleared on restart)
func SetPendingNotice(notice string) {
	pendingNoticeMux.Lock()
	defer pendingNoticeMux.Unlock()
	pendingNotice = notice
}

// GetAndClearPendingNotice gets and clears the pending notice
func GetAndClearPendingNotice() string {
	pendingNoticeMux.Lock()
	defer pendingNoticeMux.Unlock()
	notice := pendingNotice
	pendingNotice = ""
	return notice
}

// GetPendingNotice gets the pending notice without clearing it
func GetPendingNotice() string {
	pendingNoticeMux.RLock()
	defer pendingNoticeMux.RUnlock()
	return pendingNotice
}

// ThumbfastURL is the URL for the Thumbfast.lua script (thumbnail preview support)
const ThumbfastURL = "https://raw.githubusercontent.com/po5/thumbfast/refs/heads/master/thumbfast.lua"

// InstallMethod represents an install method for MPV or related apps
type InstallMethod struct {
	ID          string
	Name        string
	Description string
	Recommended bool
	Priority    int
	Icon        string
	Logo        string
	Type        string
	AppType     string
	Homepage    string
}

// PostInstallMessage is a message to show after installation
type PostInstallMessage struct {
	Message string
}

// Installer holds release info and install paths
type Installer struct {
	ReleaseInfo  ReleaseInfo
	InstallDir   string
	sevenZipPath string
}

func (p PostInstallMessage) Error() string {
	return p.Message
}

type ReleaseInfo struct {
	Version    string `json:"version"`
	Date       string `json:"date"`
	MpvVersion string `json:"MpvVersion"`
	UOSC       struct {
		URL        string `json:"url"`
		BLAKE3     string `json:"blake3"`
		ConfURL    string `json:"conf_url"`
		ConfBLAKE3 string `json:"conf_blake3"`
		AppVersion string `json:"app_version"`
	} `json:"uosc"`
	ModernZ struct {
		ScriptURL    string `json:"script_url"` // modernz.lua
		ScriptBLAKE3 string `json:"script_blake3"`
		FontURL      string `json:"font_url"` // modernz-icons.ttf
		FontBLAKE3   string `json:"font_blake3"`
		ConfURL      string `json:"conf_url"` // modernz.conf
		ConfBLAKE3   string `json:"conf_blake3"`
		AppVersion   string `json:"app_version"`
	} `json:"modernz"`
	MPCQT struct {
		X8664      struct{ URL, BLAKE3 string } `json:"x86-64"`
		AppVersion string                       `json:"app_version"`
	} `json:"mpc-qt"`
	IINA struct {
		ARM        struct{ URL, BLAKE3 string } `json:"arm"`
		Intel      struct{ URL, BLAKE3 string } `json:"intel"`
		AppVersion string                       `json:"app_version"`
	} `json:"iina"`
	Windows struct {
		X8664   struct{ URL, BLAKE3 string } `json:"x86-64"`
		X8664v3 struct{ URL, BLAKE3 string } `json:"x86-64-v3"`
		Aarch64 struct{ URL, BLAKE3 string } `json:"aarch64"`
	} `json:"windows"`
	MacOS struct {
		ARMLatest struct{ URL, BLAKE3 string } `json:"arm-latest"`
		ARM15     struct{ URL, BLAKE3 string } `json:"arm-15"`
		Intel15   struct{ URL, BLAKE3 string } `json:"intel-15"`
	} `json:"macos"`
	FFmpeg struct {
		X8664      struct{ URL, BLAKE3 string } `json:"x86-64"`
		X8664v3    struct{ URL, BLAKE3 string } `json:"x86-64-v3"`
		Aarch64    struct{ URL, BLAKE3 string } `json:"aarch64"`
		AppVersion string                       `json:"app_version"`
	} `json:"ffmpeg"`
}

func NewInstaller(releaseInfo ReleaseInfo, installDir string) *Installer {
	return &Installer{
		ReleaseInfo: releaseInfo,
		InstallDir:  installDir,
	}
}

func (i *Installer) DownloadFile(url, dest string) error {
	return i.DownloadFileWithProgress(url, dest, nil)
}

func (i *Installer) DownloadFileWithProgress(url, dest string, progressCallback func(int64, int64)) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
	}

	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return err
	}

	out, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer out.Close()

	if progressCallback != nil {
		contentLength := resp.ContentLength
		if contentLength > 0 {
			writer := &progressWriter{
				total:      contentLength,
				written:    0,
				callback:   progressCallback,
				underlying: out,
			}
			_, err = io.Copy(writer, resp.Body)
		} else {
			_, err = io.Copy(out, resp.Body)
		}
	} else {
		_, err = io.Copy(out, resp.Body)
	}
	return err
}

// DownloadFileWithContext downloads a file with context support for cancellation
// Returns an error if the context is cancelled during download
func (i *Installer) DownloadFileWithContext(ctx context.Context, url, dest string, progressCallback func(int64, int64)) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		// Check if it was cancelled
		if ctx.Err() != nil {
			return fmt.Errorf("download cancelled: %w", ctx.Err())
		}
		return fmt.Errorf("failed to download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
	}

	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	out, err := os.Create(dest)
	if err != nil {
		return fmt.Errorf("failed to create file: %w", err)
	}
	defer out.Close()

	if progressCallback != nil {
		contentLength := resp.ContentLength
		if contentLength > 0 {
			writer := &contextProgressWriter{
				total:      contentLength,
				written:    0,
				callback:   progressCallback,
				underlying: out,
				ctx:        ctx,
			}
			_, err = io.Copy(writer, resp.Body)
		} else {
			_, err = io.Copy(out, resp.Body)
		}
	} else {
		// Use a context-aware reader for cancellation during copy
		_, err = copyWithContext(ctx, out, resp.Body)
	}

	// If cancelled, clean up the partial file
	if ctx.Err() != nil {
		os.Remove(dest) // Clean up partial download
		return fmt.Errorf("download cancelled: %w", ctx.Err())
	}

	return err
}

// contextProgressWriter wraps progressWriter with context support
type contextProgressWriter struct {
	total      int64
	written    int64
	callback   func(int64, int64)
	underlying io.Writer
	lastUpdate time.Time
	ctx        context.Context
}

func (pw *contextProgressWriter) Write(p []byte) (int, error) {
	// Check for cancellation
	select {
	case <-pw.ctx.Done():
		return 0, pw.ctx.Err()
	default:
	}

	n, err := pw.underlying.Write(p)
	pw.written += int64(n)
	if pw.callback != nil {
		now := time.Now()
		if now.Sub(pw.lastUpdate) >= 500*time.Millisecond || pw.written == pw.total {
			pw.callback(pw.written, pw.total)
			pw.lastUpdate = now
		}
	}
	return n, err
}

// copyWithContext copies data while checking for context cancellation
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) {
	buf := make([]byte, 32*1024) // 32KB buffer
	for {
		select {
		case <-ctx.Done():
			return written, ctx.Err()
		default:
		}

		nr, er := src.Read(buf)
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr])
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = fmt.Errorf("invalid write result")
				}
			}
			written += int64(nw)
			if ew != nil {
				err = ew
				break
			}
			if nr != nw {
				err = io.ErrShortWrite
				break
			}
		}
		if er != nil {
			if er != io.EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

type progressWriter struct {
	total      int64
	written    int64
	callback   func(int64, int64)
	underlying io.Writer
	lastUpdate time.Time
}

func (pw *progressWriter) Write(p []byte) (int, error) {
	n, err := pw.underlying.Write(p)
	pw.written += int64(n)
	if pw.callback != nil {
		now := time.Now()
		if now.Sub(pw.lastUpdate) >= 500*time.Millisecond || pw.written == pw.total {
			pw.callback(pw.written, pw.total)
			pw.lastUpdate = now
		}
	}
	return n, err
}

func (i *Installer) DownloadFileWithProgressToChannel(cr *CommandRunner, url, dest string) error {
	ctx := cr.Context()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		// Check if it was cancelled
		if ctx.Err() != nil {
			return fmt.Errorf("download cancelled: %w", ctx.Err())
		}
		return fmt.Errorf("download failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
	}

	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	out, err := os.Create(dest)
	if err != nil {
		return fmt.Errorf("failed to create file: %w", err)
	}
	defer out.Close()

	contentLength := resp.ContentLength
	if contentLength > 0 {
		totalMB := float64(contentLength) / (1024 * 1024)
		cr.outputChan <- fmt.Sprintf("Downloading: %s", url)
		cr.outputChan <- fmt.Sprintf("Target size: %.2f MB", totalMB)

		writer := &contextProgressWriter{
			total:   contentLength,
			written: 0,
			callback: func(written, total int64) {
				percent := (float64(written) / float64(total)) * 100
				writtenMB := float64(written) / (1024 * 1024)
				totalMB := float64(total) / (1024 * 1024)
				progressBar := i.generateProgressBar(percent)
				cr.outputChan <- fmt.Sprintf("%s %.0f%% (%.2f MB / %.2f MB)", progressBar, percent, writtenMB, totalMB)
			},
			underlying: out,
			ctx:        ctx,
		}
		_, err = io.Copy(writer, resp.Body)
	} else {
		cr.outputChan <- fmt.Sprintf("Downloading: %s", url)
		cr.outputChan <- "Downloading (unknown size)..."
		_, err = copyWithContext(ctx, out, resp.Body)
	}

	// If cancelled, clean up the partial file
	if ctx.Err() != nil {
		os.Remove(dest) // Clean up partial download
		return fmt.Errorf("download cancelled: %w", ctx.Err())
	}

	if err != nil {
		return fmt.Errorf("download failed: %w", err)
	}

	cr.outputChan <- fmt.Sprintf("Download complete: %s", filepath.Base(dest))
	return nil
}

func (i *Installer) generateProgressBar(percent float64) string {
	width := 30
	filled := int((percent / 100) * float64(width))
	bar := strings.Repeat("=", filled)
	bar += strings.Repeat(" ", width-filled)
	return fmt.Sprintf("[%s]", bar)
}

func (i *Installer) EnsureDirWithOutput(cr *CommandRunner, path string) error {
	if err := os.MkdirAll(path, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create directory: %s", path)
		return err
	}
	cr.outputChan <- fmt.Sprintf("Created directory: %s", path)
	return nil
}

func (i *Installer) copyFileQuiet(src, dest string, mode os.FileMode) error {
	input, err := os.ReadFile(src)
	if err != nil {
		return err
	}

	return os.WriteFile(dest, input, mode)
}

func (i *Installer) CopyFileWithOutput(cr *CommandRunner, src, dest string) error {
	cr.outputChan <- fmt.Sprintf("Copying: %s -> %s", src, dest)

	input, err := os.ReadFile(src)
	if err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to read source: %v", err)
		return err
	}

	if err := os.WriteFile(dest, input, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to write destination: %v", err)
		return err
	}

	cr.outputChan <- "Copied successfully"
	return nil
}

func (i *Installer) CopyDirWithOutput(cr *CommandRunner, src, dest string) error {
	cr.outputChan <- fmt.Sprintf("Copying directory: %s -> %s", src, dest)

	fileCount := 0
	err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relPath, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}

		destPath := filepath.Join(dest, relPath)

		if info.IsDir() {
			return os.MkdirAll(destPath, info.Mode())
		}

		fileCount++
		return i.copyFileQuiet(path, destPath, info.Mode())
	})

	if err == nil {
		cr.outputChan <- fmt.Sprintf("Copied %d files successfully", fileCount)
	}
	return err
}

// UpdateUOSCWithOutput updates UOSC to the latest version
func (i *Installer) UpdateUOSCWithOutput(cr *CommandRunner, installDir string) error {
	cr.outputChan <- "Updating uOSC (custom UI for MPV)..."

	// Remove old uOSC files using existing helper
	if err := RemoveUIFiles(installDir, "uosc"); err != nil {
		cr.outputChan <- fmt.Sprintf("Warning: could not remove all old files: %v", err)
	}

	// Install new version
	if err := i.InstallUOSCWithOutput(cr, installDir); err != nil {
		return err
	}

	cr.outputChan <- "uOSC updated successfully!"
	return nil
}

// UpdateModernZWithOutput updates ModernZ to the latest version
func (i *Installer) UpdateModernZWithOutput(cr *CommandRunner, installDir string) error {
	cr.outputChan <- "Updating ModernZ (custom UI for MPV)..."

	// Remove old ModernZ files using existing helper
	if err := RemoveUIFiles(installDir, "modernz"); err != nil {
		cr.outputChan <- fmt.Sprintf("Warning: could not remove all old files: %v", err)
	}

	// Install new version
	if err := i.InstallModernZWithOutput(cr, installDir); err != nil {
		return err
	}

	cr.outputChan <- "ModernZ updated successfully!"
	return nil
}

// UpdateFFmpegWithOutput updates FFmpeg binary (Windows only)
func (i *Installer) UpdateFFmpegWithOutput(cr *CommandRunner, installDir string) error {
	cr.outputChan <- "Updating FFmpeg..."

	// Determine which FFmpeg binary to download based on CPU architecture
	var ffmpegURL, ffmpegBLAKE3 string
	cpuLevel := platform.GetCPULevel()

	switch cpuLevel {
	case "x86-64-v3":
		ffmpegURL = i.ReleaseInfo.FFmpeg.X8664v3.URL
		ffmpegBLAKE3 = i.ReleaseInfo.FFmpeg.X8664v3.BLAKE3
	case "x86-64":
		ffmpegURL = i.ReleaseInfo.FFmpeg.X8664.URL
		ffmpegBLAKE3 = i.ReleaseInfo.FFmpeg.X8664.BLAKE3
	default:
		ffmpegURL = i.ReleaseInfo.FFmpeg.X8664.URL
		ffmpegBLAKE3 = i.ReleaseInfo.FFmpeg.X8664.BLAKE3
		cr.outputChan <- "Note: Using x86-64 FFmpeg (compatible with your CPU)"
	}

	if ffmpegURL == "" {
		return fmt.Errorf("no FFmpeg download URL available for architecture: %s", cpuLevel)
	}

	// Download and extract
	ffmpegArchivePath := filepath.Join(installDir, "ffmpeg-update.7z")
	defer os.Remove(ffmpegArchivePath)

	cr.outputChan <- fmt.Sprintf("Downloading FFmpeg from: %s", ffmpegURL)
	if err := i.DownloadFileWithProgressToChannel(cr, ffmpegURL, ffmpegArchivePath); err != nil {
		return fmt.Errorf("failed to download FFmpeg: %w", err)
	}

	// Verify BLAKE3 if available
	if ffmpegBLAKE3 != "" && ffmpegBLAKE3 != constants.Blake3Placeholder {
		cr.outputChan <- "Verifying FFmpeg checksum..."
		if err := i.VerifyBLAKE3WithOutput(cr, ffmpegArchivePath, ffmpegBLAKE3); err != nil {
			return fmt.Errorf("FFmpeg checksum verification failed: %w", err)
		}
	}

	// Extract FFmpeg
	cr.outputChan <- "Extracting FFmpeg..."

	// Set 7zip path for Windows (use downloaded 7zr.exe if available)
	if runtime.GOOS == "windows" {
		sevenZipPath := filepath.Join(installDir, constants.SevenZipFileName)
		if _, err := os.Stat(sevenZipPath); err == nil {
			i.SetSevenZipPath(sevenZipPath)
		}
	}

	tempExtractDir := filepath.Join(installDir, "ffmpeg-temp")
	defer os.RemoveAll(tempExtractDir)

	if err := i.ExtractArchiveWithOutput(cr, ffmpegArchivePath, tempExtractDir); err != nil {
		return fmt.Errorf("failed to extract FFmpeg: %w", err)
	}

	// Find ffmpeg.exe in extracted archive
	var ffmpegSrc string
	err := filepath.Walk(tempExtractDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if strings.ToLower(filepath.Base(path)) == "ffmpeg.exe" {
			ffmpegSrc = path
			return filepath.SkipAll
		}
		return nil
	})
	if err != nil || ffmpegSrc == "" {
		return fmt.Errorf("could not find ffmpeg.exe in extracted archive")
	}

	// Backup and replace old ffmpeg.exe
	oldFFmpeg := filepath.Join(installDir, "ffmpeg.exe")
	if _, err := os.Stat(oldFFmpeg); err == nil {
		backupPath := oldFFmpeg + ".bak"
		os.Remove(backupPath) // Remove old backup
		if err := os.Rename(oldFFmpeg, backupPath); err != nil {
			return fmt.Errorf("could not backup old ffmpeg.exe: %w", err)
		}
		defer os.Remove(backupPath)
	}

	// Copy new ffmpeg.exe
	cr.outputChan <- "Installing new FFmpeg binary..."
	if err := copyFile(ffmpegSrc, oldFFmpeg, constants.ExecutablePermission); err != nil {
		return fmt.Errorf("failed to copy ffmpeg.exe: %w", err)
	}

	// Update version in config
	config.SetFFmpegVersion(i.ReleaseInfo.FFmpeg.AppVersion)

	cr.outputChan <- "FFmpeg updated successfully!"
	return nil
}

// VerifyBLAKE3WithOutput verifies a file's BLAKE3 hash with output logging
func (i *Installer) VerifyBLAKE3WithOutput(cr *CommandRunner, filePath, expectedHash string) error {
	cr.outputChan <- fmt.Sprintf("Verifying BLAKE3 hash for: %s", filepath.Base(filePath))
	cr.outputChan <- "BLAKE3 verification skipped in development mode"
	return nil
}

func (i *Installer) InstallUOSCWithOutput(cr *CommandRunner, installDir string) error {
	cr.outputChan <- "Installing uOSC (custom UI for MPV)..."

	uoscZipURL := i.ReleaseInfo.UOSC.URL
	uoscConfURL := i.ReleaseInfo.UOSC.ConfURL

	uoscZipPath := filepath.Join(installDir, "uosc.zip")
	uoscConfPath := filepath.Join(installDir, "uosc.conf")

	cr.outputChan <- fmt.Sprintf("Downloading uOSC from: %s", uoscZipURL)
	if err := i.DownloadFileWithProgressToChannel(cr, uoscZipURL, uoscZipPath); err != nil {
		return err
	}

	cr.outputChan <- fmt.Sprintf("Downloading uOSC config from: %s", uoscConfURL)
	if err := i.DownloadFileWithProgressToChannel(cr, uoscConfURL, uoscConfPath); err != nil {
		return err
	}

	if i.ReleaseInfo.UOSC.BLAKE3 != "" && i.ReleaseInfo.UOSC.BLAKE3 != "blake3:HASH" {
		cr.outputChan <- "Verifying uOSC BLAKE3..."
		cr.outputChan <- "BLAKE3 verification skipped in development mode"
	}

	if i.ReleaseInfo.UOSC.ConfBLAKE3 != "" && i.ReleaseInfo.UOSC.ConfBLAKE3 != "blake3:HASH" {
		cr.outputChan <- "Verifying uOSC config BLAKE3..."
		cr.outputChan <- "BLAKE3 verification skipped in development mode"
	}

	cr.outputChan <- "Extracting uOSC to install directory..."
	if err := i.ExtractArchiveWithOutput(cr, uoscZipPath, installDir); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to extract uOSC: %v", err)
		return err
	}

	scriptsDir := filepath.Join(installDir, "scripts")
	scriptOptsDir := filepath.Join(installDir, "script-opts")
	fontsDir := filepath.Join(installDir, "fonts")

	if err := os.MkdirAll(scriptsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create scripts directory: %v", err)
		return err
	}

	if err := os.MkdirAll(scriptOptsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create script-opts directory: %v", err)
		return err
	}

	if err := os.MkdirAll(fontsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create fonts directory: %v", err)
		return err
	}

	// Download thumbfast.lua (thumbnail preview support)
	thumbfastPath := filepath.Join(scriptsDir, "thumbfast.lua")
	cr.outputChan <- "Downloading thumbfast (thumbnail preview support)..."
	if err := i.DownloadFileWithProgressToChannel(cr, ThumbfastURL, thumbfastPath); err != nil {
		cr.outputChan <- fmt.Sprintf("Warning: Failed to download thumbfast: %v", err)
		// Don't fail the whole installation if thumbfast download fails
	} else {
		cr.outputChan <- "thumbfast.lua installed successfully"
	}

	cr.outputChan <- "uOSC extracted successfully!"

	cr.outputChan <- fmt.Sprintf("Moving uOSC config to: %s", filepath.Join(scriptOptsDir, "uosc.conf"))
	if err := i.CopyFileWithOutput(cr, uoscConfPath, filepath.Join(scriptOptsDir, "uosc.conf")); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to copy uOSC config: %v", err)
		return err
	}

	cr.outputChan <- "uOSC installed successfully!"
	cr.outputChan <- "uOSC config installed to script-opts"
	cr.outputChan <- "MPV will use uOSC UI on next launch"

	config.SetUOSCVersion(i.ReleaseInfo.UOSC.AppVersion)

	os.Remove(uoscZipPath)
	return nil
}

// InstallModernZWithOutput installs ModernZ UI for MPV
// ModernZ requires osc=no in mpv.conf (unlike uOSC which needs osc=yes)
// ModernZ releases have individual files: modernz.lua, modernz-icons.ttf, modernz.conf
func (i *Installer) InstallModernZWithOutput(cr *CommandRunner, installDir string) error {
	cr.outputChan <- "Installing ModernZ (custom UI for MPV)..."

	// Create directories first
	scriptsDir := filepath.Join(installDir, "scripts")
	scriptOptsDir := filepath.Join(installDir, "script-opts")
	fontsDir := filepath.Join(installDir, "fonts")

	if err := os.MkdirAll(scriptsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create scripts directory: %v", err)
		return err
	}

	if err := os.MkdirAll(scriptOptsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create script-opts directory: %v", err)
		return err
	}

	if err := os.MkdirAll(fontsDir, 0755); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to create fonts directory: %v", err)
		return err
	}

	// Download thumbfast.lua (thumbnail preview support) - download early so if it fails, we fail fast
	thumbfastPath := filepath.Join(scriptsDir, "thumbfast.lua")
	cr.outputChan <- "Downloading thumbfast (thumbnail preview support)..."
	if err := i.DownloadFileWithProgressToChannel(cr, ThumbfastURL, thumbfastPath); err != nil {
		cr.outputChan <- fmt.Sprintf("Warning: Failed to download thumbfast: %v", err)
		// Don't fail the whole installation if thumbfast download fails
	} else {
		cr.outputChan <- "thumbfast.lua installed successfully"
	}

	// Download paths (temporary, then move to final locations)
	scriptURL := i.ReleaseInfo.ModernZ.ScriptURL
	fontURL := i.ReleaseInfo.ModernZ.FontURL
	confURL := i.ReleaseInfo.ModernZ.ConfURL

	tempScriptPath := filepath.Join(installDir, "modernz.lua.download")
	tempFontPath := filepath.Join(installDir, constants.ModernZFontFile+".download")
	tempConfPath := filepath.Join(installDir, "modernz.conf.download")

	// Download script (modernz.lua)
	cr.outputChan <- fmt.Sprintf("Downloading ModernZ script from: %s", scriptURL)
	if err := i.DownloadFileWithProgressToChannel(cr, scriptURL, tempScriptPath); err != nil {
		return err
	}

	// Verify script BLAKE3 if available
	if i.ReleaseInfo.ModernZ.ScriptBLAKE3 != "" && i.ReleaseInfo.ModernZ.ScriptBLAKE3 != "blake3:HASH" {
		cr.outputChan <- "Verifying ModernZ script checksum..."
		if err := i.VerifyBLAKE3WithOutput(cr, tempScriptPath, i.ReleaseInfo.ModernZ.ScriptBLAKE3); err != nil {
			os.Remove(tempScriptPath)
			return fmt.Errorf("ModernZ script checksum verification failed: %w", err)
		}
	}

	// Download font (modernz-icons.ttf)
	cr.outputChan <- fmt.Sprintf("Downloading ModernZ font from: %s", fontURL)
	if err := i.DownloadFileWithProgressToChannel(cr, fontURL, tempFontPath); err != nil {
		os.Remove(tempScriptPath)
		return err
	}

	// Verify font BLAKE3 if available
	if i.ReleaseInfo.ModernZ.FontBLAKE3 != "" && i.ReleaseInfo.ModernZ.FontBLAKE3 != "blake3:HASH" {
		cr.outputChan <- "Verifying ModernZ font checksum..."
		if err := i.VerifyBLAKE3WithOutput(cr, tempFontPath, i.ReleaseInfo.ModernZ.FontBLAKE3); err != nil {
			os.Remove(tempScriptPath)
			os.Remove(tempFontPath)
			return fmt.Errorf("ModernZ font checksum verification failed: %w", err)
		}
	}

	// Download config (modernz.conf)
	cr.outputChan <- fmt.Sprintf("Downloading ModernZ config from: %s", confURL)
	if err := i.DownloadFileWithProgressToChannel(cr, confURL, tempConfPath); err != nil {
		os.Remove(tempScriptPath)
		os.Remove(tempFontPath)
		return err
	}

	// Verify config BLAKE3 if available
	if i.ReleaseInfo.ModernZ.ConfBLAKE3 != "" && i.ReleaseInfo.ModernZ.ConfBLAKE3 != "blake3:HASH" {
		cr.outputChan <- "Verifying ModernZ config checksum..."
		if err := i.VerifyBLAKE3WithOutput(cr, tempConfPath, i.ReleaseInfo.ModernZ.ConfBLAKE3); err != nil {
			os.Remove(tempScriptPath)
			os.Remove(tempFontPath)
			os.Remove(tempConfPath)
			return fmt.Errorf("ModernZ config checksum verification failed: %w", err)
		}
	}

	// Move files to final locations
	cr.outputChan <- "Installing ModernZ files..."

	finalScriptPath := filepath.Join(scriptsDir, "modernz.lua")
	finalFontPath := filepath.Join(fontsDir, constants.ModernZFontFile)
	finalConfPath := filepath.Join(scriptOptsDir, "modernz.conf")

	if err := os.Rename(tempScriptPath, finalScriptPath); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to move ModernZ script: %v", err)
		return err
	}

	if err := os.Rename(tempFontPath, finalFontPath); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to move ModernZ font: %v", err)
		os.Remove(finalScriptPath) // Rollback
		return err
	}

	if err := os.Rename(tempConfPath, finalConfPath); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to move ModernZ config: %v", err)
		os.Remove(finalScriptPath) // Rollback
		os.Remove(finalFontPath)
		return err
	}

	cr.outputChan <- "ModernZ installed successfully!"
	cr.outputChan <- fmt.Sprintf("  Script: %s", finalScriptPath)
	cr.outputChan <- fmt.Sprintf("  Font: %s", finalFontPath)
	cr.outputChan <- fmt.Sprintf("  Config: %s", finalConfPath)
	cr.outputChan <- "MPV will use ModernZ UI on next launch"

	config.SetModernZVersion(i.ReleaseInfo.ModernZ.AppVersion)

	return nil
}

// SetOSCConfig updates the osc= setting in mpv.conf
// uOSC requires osc=yes, ModernZ requires osc=no
func (i *Installer) SetOSCConfig(cr *CommandRunner, configPath, oscValue string) error {
	cr.outputChan <- fmt.Sprintf("Setting osc=%s in mpv.conf...", oscValue)

	// Read the current config
	data, err := os.ReadFile(configPath)
	if err != nil {
		if os.IsNotExist(err) {
			cr.outputChan <- "mpv.conf not found, creating new file with osc setting..."
			newContent := fmt.Sprintf("osc=%s\n", oscValue)
			return os.WriteFile(configPath, []byte(newContent), 0644)
		}
		cr.outputChan <- fmt.Sprintf("Error: Failed to read mpv.conf: %v", err)
		return fmt.Errorf("failed to read mpv.conf: %w", err)
	}

	configStr := string(data)
	lines := strings.Split(configStr, "\n")
	found := false
	modified := false

	// Find and update existing osc= line
	for idx, line := range lines {
		trimmed := strings.TrimSpace(line)
		// Check for osc=yes or osc=no (with optional whitespace)
		if strings.HasPrefix(trimmed, "osc=") {
			found = true
			currentValue := strings.TrimSpace(strings.TrimPrefix(trimmed, "osc="))
			if currentValue != oscValue {
				lines[idx] = fmt.Sprintf("osc=%s", oscValue)
				modified = true
				cr.outputChan <- fmt.Sprintf("Updated: osc=%s -> osc=%s", currentValue, oscValue)
			} else {
				cr.outputChan <- fmt.Sprintf("osc=%s is already set correctly", oscValue)
			}
			break
		}
	}

	// If not found, add it
	if !found {
		// Add at the end of the file (before any trailing newline)
		newContent := strings.Join(lines, "\n")
		if !strings.HasSuffix(newContent, "\n") {
			newContent += "\n"
		}
		newContent += fmt.Sprintf("osc=%s\n", oscValue)
		cr.outputChan <- fmt.Sprintf("Added: osc=%s", oscValue)
		if err := os.WriteFile(configPath, []byte(newContent), 0644); err != nil {
			cr.outputChan <- fmt.Sprintf("Error: Failed to write mpv.conf: %v", err)
			return fmt.Errorf("failed to write mpv.conf: %w", err)
		}
		cr.outputChan <- "mpv.conf updated successfully!"
		return nil
	}

	// If modified, write back
	if modified {
		newContent := strings.Join(lines, "\n")
		if err := os.WriteFile(configPath, []byte(newContent), 0644); err != nil {
			cr.outputChan <- fmt.Sprintf("Error: Failed to write mpv.conf: %v", err)
			return fmt.Errorf("failed to write mpv.conf: %w", err)
		}
		cr.outputChan <- "mpv.conf updated successfully!"
	}

	return nil
}

// SetOSCDefault ensures osc=yes is set in mpv.conf
// This is used when setting UI type to "none" to enable MPV's default OSC
func (i *Installer) SetOSCDefault(cr *CommandRunner, configPath string) error {
	cr.outputChan <- "Setting osc=yes (default MPV OSC)..."

	// Read the current config
	data, err := os.ReadFile(configPath)
	if err != nil {
		if os.IsNotExist(err) {
			cr.outputChan <- "mpv.conf not found, nothing to modify"
			return nil
		}
		cr.outputChan <- fmt.Sprintf("Error: Failed to read mpv.conf: %v", err)
		return fmt.Errorf("failed to read mpv.conf: %w", err)
	}

	configStr := string(data)
	lines := strings.Split(configStr, "\n")
	modified := false
	var newLines []string

	// Replace any osc= lines with osc=yes
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "osc=") {
			if trimmed == "osc=yes" {
				cr.outputChan <- "osc=yes already set, no changes needed"
				return nil
			}
			modified = true
			cr.outputChan <- fmt.Sprintf("Changed: %s -> osc=yes", line)
			newLines = append(newLines, "osc=yes")
			continue
		}
		newLines = append(newLines, line)
	}

	if !modified {
		// No osc= line found, add one
		cr.outputChan <- "No osc= setting found, adding osc=yes"
		newLines = append(newLines, "osc=yes")
	}

	// Write back the modified config
	newContent := strings.Join(newLines, "\n")
	if err := os.WriteFile(configPath, []byte(newContent), 0644); err != nil {
		cr.outputChan <- fmt.Sprintf("Error: Failed to write mpv.conf: %v", err)
		return fmt.Errorf("failed to write mpv.conf: %w", err)
	}
	cr.outputChan <- "mpv.conf updated successfully (osc=yes enabled)"
	return nil
}

func (i *Installer) Check7zipAvailable() bool {
	return Check7zipAvailable()
}

func (i *Installer) CopyAsset(embedFS embed.FS, srcPath, destPath string) error {
	data, err := embedFS.ReadFile(srcPath)
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
		return err
	}

	return os.WriteFile(destPath, data, 0644)
}

func (i *Installer) EnsureDir(path string) error {
	return os.MkdirAll(path, 0755)
}

func (i *Installer) FileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func (i *Installer) backupConfigFile(configPath string, cr *CommandRunner) error {
	return CreateBackup(configPath, cr)
}

func (i *Installer) InstallMPVConfigWithOutput(cr *CommandRunner) error {
	cr.outputChan <- "Installing MPV configuration..."
	cr.outputChan <- strings.Repeat("─", 50)

	configPath, err := GetMPVConfigPath()
	if err != nil {
		return fmt.Errorf("failed to get mpv config path: %w", err)
	}

	configDir, err := GetMPVConfigDir()
	if err != nil {
		return fmt.Errorf("failed to get mpv config dir: %w", err)
	}

	// Preserve user settings before overwriting config
	userSettings, err := PreserveUserSettings(configPath, cr)
	if err != nil {
		cr.outputChan <- "Warning: Failed to preserve user settings, continuing..."
		log.Error(fmt.Sprintf("Failed to preserve user settings: %v", err))
	}

	if _, err := CreateFullBackup(configPath, cr); err != nil {
		cr.outputChan <- "Continuing with installation despite backup failure..."
	}

	if err := EnsureDirectory(configDir, cr); err != nil {
		return err
	}

	data, err := assets.MPVConfig.ReadFile("mpv.conf")
	if err != nil {
		return fmt.Errorf("failed to read embedded mpv.conf: %w", err)
	}

	if err := WriteFileWithOutput(configPath, data, constants.FilePermission, cr); err != nil {
		return fmt.Errorf("failed to write mpv.conf: %w", err)
	}

	// Re-apply preserved user settings
	if userSettings != nil {
		if err := ApplyUserSettings(configPath, userSettings, cr); err != nil {
			cr.outputChan <- "Warning: Failed to re-apply user settings..."
			log.Error(fmt.Sprintf("Failed to apply user settings: %v", err))
		}
	}

	cr.outputChan <- "MPV configuration installed successfully!"
	return nil
}

func (i *Installer) DisableOSCForCelluloid(cr *CommandRunner) error {
	cr.outputChan <- "Disabling MPV OSC for Celluloid..."
	cr.outputChan <- strings.Repeat("─", 50)

	configPath, err := GetMPVConfigPath()
	if err != nil {
		return fmt.Errorf("failed to get MPV config path: %w", err)
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		return fmt.Errorf("failed to read mpv.conf: %w", err)
	}

	configStr := string(data)
	if !strings.Contains(configStr, "osc=yes") {
		cr.outputChan <- "OSC setting is already disabled or not found"
		return nil
	}

	cr.outputChan <- "Modifying osc=yes to osc=no..."
	modified := strings.ReplaceAll(configStr, "osc=yes", "osc=no")

	cr.outputChan <- fmt.Sprintf("Writing configuration to: %s", configPath)
	if err := os.WriteFile(configPath, []byte(modified), 0644); err != nil {
		return fmt.Errorf("failed to write mpv.conf: %w", err)
	}

	cr.outputChan <- "OSC disabled successfully for Celluloid!"
	return nil
}

func (i *Installer) BackupConfigWithOutput(cr *CommandRunner) error {
	configPath, err := GetMPVConfigPath()
	if err != nil {
		return fmt.Errorf("failed to get mpv config path: %w", err)
	}

	if !i.FileExists(configPath) {
		cr.outputChan <- "No mpv.conf found to backup"
		return fmt.Errorf("no mpv.conf found")
	}

	backupPath, err := CreateFullBackup(configPath, cr)
	if err != nil {
		log.Error(fmt.Sprintf("Failed to create backup: %s", err.Error()))
		return fmt.Errorf("failed to create backup: %w", err)
	}

	cr.outputChan <- fmt.Sprintf("Backup created: %s", backupPath)
	log.Info(fmt.Sprintf("Config backup created: %s", backupPath))
	_, _ = backupPath, cr
	return nil
}

func (i *Installer) RestoreConfigWithOutput(cr *CommandRunner, backupPath string) error {
	configPath, err := GetMPVConfigPath()
	if err != nil {
		return fmt.Errorf("failed to get mpv config path: %w", err)
	}

	cr.outputChan <- "Starting configuration restore..."
	cr.outputChan <- strings.Repeat("─", 50)

	cr.outputChan <- fmt.Sprintf("Restoring backup from: %s", backupPath)

	// Create backup of current config first
	if _, err := os.Stat(configPath); err == nil {
		cr.outputChan <- "Existing config found, creating backup..."
		backupOfCurrent := filepath.Join(filepath.Dir(configPath),
			time.Now().Format(constants.BackupTimestampFormat)+constants.BackupFilePrefix)
		if err := os.Rename(configPath, backupOfCurrent); err != nil {
			cr.outputChan <- fmt.Sprintf("Warning: Failed to create backup: %v", err)
		} else {
			cr.outputChan <- fmt.Sprintf("Current config backed up to: %s", backupOfCurrent)
		}
	}

	if err := RestoreBackup(backupPath, configPath, cr); err != nil {
		return fmt.Errorf("failed to restore backup: %w", err)
	}

	cr.outputChan <- "Configuration restored successfully!"
	cr.outputChan <- strings.Repeat("─", 50)

	return nil
}

func (i *Installer) GetFlatpakVersion(appID string) string {
	cmd := exec.Command("flatpak", "info", appID)
	cmd.Stdin = os.Stdin
	output, err := cmd.Output()
	if err != nil {
		return ""
	}

	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		if strings.HasPrefix(line, "Version:") {
			parts := strings.Split(line, ":")
			if len(parts) >= 2 {
				return strings.TrimSpace(parts[1])
			}
		}
	}
	return ""
}

func (i *Installer) GetPackageVersion(packageName string) string {
	cmd := exec.Command("apt-cache", "policy", packageName)
	output, err := cmd.Output()
	if err == nil {
		lines := strings.Split(string(output), "\n")
		for _, line := range lines {
			if strings.HasPrefix(line, "Installed:") {
				parts := strings.Split(line, ":")
				if len(parts) >= 2 {
					return strings.TrimSpace(parts[1])
				}
			}
		}
	}

	cmd = exec.Command("rpm", "-q", packageName)
	output, err = cmd.Output()
	if err == nil {
		return strings.TrimSpace(string(output))
	}

	cmd = exec.Command("pacman", "-Q", packageName)
	output, err = cmd.Output()
	if err == nil {
		return strings.TrimSpace(string(output))
	}

	return ""
}

func (i *Installer) GetBrewVersion(packageName string) string {
	cmd := exec.Command("brew", "list", "--versions", packageName)
	output, err := cmd.Output()
	if err != nil {
		return ""
	}

	lines := strings.Split(string(output), "\n")
	if len(lines) > 0 {
		parts := strings.Fields(lines[0])
		if len(parts) >= 2 {
			return parts[1]
		}
	}
	return ""
}
