// Package installer provides cross-platform installation functionality
package installer

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"sync"
	"time"

	"gitgud.io/mike/mpv-manager/pkg/config"
	"gitgud.io/mike/mpv-manager/pkg/constants"
	"gitgud.io/mike/mpv-manager/pkg/log"
)

// DetectedApp represents a detected MPV application installation
type DetectedApp struct {
	AppName     string // "MPV", "Celluloid"
	MethodID    string // "mpv-flatpak", "celluloid-package", etc.
	Version     string // Installed version
	InstallPath string // Path to binary if known
	InstallType string // "flatpak", "package"
}

// DetectInstalledMPVApps concurrently detects all installed MPV applications
// Checks Flatpak and system package managers for mpv and celluloid
func DetectInstalledMPVApps() []DetectedApp {
	if runtime.GOOS != "linux" {
		return nil
	}

	log.Info("[Detection] Starting concurrent MPV app detection...")

	var wg sync.WaitGroup
	resultsChan := make(chan []DetectedApp, 2) // 2 detection types

	// Detect Flatpak apps
	wg.Add(1)
	go func() {
		defer wg.Done()
		apps := detectFlatpakApps()
		if len(apps) > 0 {
			log.Info(fmt.Sprintf("[Detection] Found %d Flatpak apps", len(apps)))
		}
		resultsChan <- apps
	}()

	// Detect Package Manager apps
	wg.Add(1)
	go func() {
		defer wg.Done()
		apps := detectPackageApps()
		if len(apps) > 0 {
			log.Info(fmt.Sprintf("[Detection] Found %d Package Manager apps", len(apps)))
		}
		resultsChan <- apps
	}()

	// Wait for all goroutines and collect results
	go func() {
		wg.Wait()
		close(resultsChan)
	}()

	// Collect all detected apps
	var allApps []DetectedApp
	for apps := range resultsChan {
		allApps = append(allApps, apps...)
	}

	log.Info(fmt.Sprintf("[Detection] Total detected apps: %d", len(allApps)))
	return allApps
}

// detectFlatpakApps checks for MPV and Celluloid installed via Flatpak
func detectFlatpakApps() []DetectedApp {
	var apps []DetectedApp

	// Check if flatpak is available
	if _, err := exec.LookPath("flatpak"); err != nil {
		log.Debug("[Detection] Flatpak not available")
		return apps
	}

	// Check for MPV Flatpak
	if version := getFlatpakAppVersion(constants.FlatpakMPV); version != "" {
		apps = append(apps, DetectedApp{
			AppName:     "MPV",
			MethodID:    constants.MethodMPVFlatpak,
			Version:     version,
			InstallPath: "flatpak run " + constants.FlatpakMPV,
			InstallType: "flatpak",
		})
		log.Debug(fmt.Sprintf("[Detection] Found MPV Flatpak: %s", version))
	}

	// Check for Celluloid Flatpak
	if version := getFlatpakAppVersion(constants.FlatpakCelluloid); version != "" {
		apps = append(apps, DetectedApp{
			AppName:     "Celluloid",
			MethodID:    constants.MethodCelluloidFlatpak,
			Version:     version,
			InstallPath: "flatpak run " + constants.FlatpakCelluloid,
			InstallType: "flatpak",
		})
		log.Debug(fmt.Sprintf("[Detection] Found Celluloid Flatpak: %s", version))
	}

	return apps
}

// detectPackageApps checks for MPV and Celluloid installed via system package manager
func detectPackageApps() []DetectedApp {
	var apps []DetectedApp

	// Detect the package manager family
	family := detectPackageManagerFamily()
	if family == "" {
		log.Debug("[Detection] No supported package manager found")
		return apps
	}

	log.Debug(fmt.Sprintf("[Detection] Detected package manager family: %s", family))

	// Check for MPV package
	if version, path := getPackageAppVersion(family, "mpv"); version != "" {
		apps = append(apps, DetectedApp{
			AppName:     "MPV",
			MethodID:    constants.MethodMPVPackage,
			Version:     version,
			InstallPath: path,
			InstallType: "package",
		})
		log.Debug(fmt.Sprintf("[Detection] Found MPV Package: %s", version))
	}

	// Check for Celluloid package (with alternative names)
	celluloidNames := []string{"celluloid", "gnome-mpv"}
	for _, name := range celluloidNames {
		if version, path := getPackageAppVersion(family, name); version != "" {
			apps = append(apps, DetectedApp{
				AppName:     "Celluloid",
				MethodID:    constants.MethodCelluloidPackage,
				Version:     version,
				InstallPath: path,
				InstallType: "package",
			})
			log.Debug(fmt.Sprintf("[Detection] Found Celluloid Package (%s): %s", name, version))
			break // Found one, no need to check others
		}
	}

	return apps
}

// getFlatpakAppVersion gets the version of a Flatpak app
func getFlatpakAppVersion(appID string) string {
	cmd := exec.Command("flatpak", "info", appID)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "Version:" || strings.HasPrefix(line, "Version: ") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				return strings.TrimSpace(parts[1])
			}
		}
	}

	return ""
}

// detectPackageManagerFamily detects the system package manager family
func detectPackageManagerFamily() string {
	// Read /etc/os-release to determine distro
	file, err := os.Open("/etc/os-release")
	if err != nil {
		// Fallback to checking which package manager exists
		if _, err := exec.LookPath("pacman"); err == nil {
			return constants.PMFamilyArch
		}
		if _, err := exec.LookPath("dnf"); err == nil {
			return constants.PMFamilyRHEL
		}
		if _, err := exec.LookPath("apt-get"); err == nil {
			return constants.PMFamilyDebian
		}
		return ""
	}
	defer file.Close()

	var id, idLike string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "ID=") {
			id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
		} else if strings.HasPrefix(line, "ID_LIKE=") {
			idLike = strings.Trim(strings.TrimPrefix(line, "ID_LIKE="), `"`)
		}
	}

	// Map to known distro families
	switch id {
	case "ubuntu", "debian", "linuxmint", "pop", "elementary":
		return constants.PMFamilyDebian
	case "fedora", "rhel", "centos", "rocky", "almalinux":
		return constants.PMFamilyRHEL
	case "arch", "manjaro", "endeavouros", "garuda", "cachyos":
		return constants.PMFamilyArch
	case "opensuse-tumbleweed", "opensuse-leap", "opensuse":
		return constants.PMFamilySUSE
	}

	// Check ID_LIKE for derivative distros
	idLikeLower := strings.ToLower(idLike)
	if strings.Contains(idLikeLower, "debian") {
		return constants.PMFamilyDebian
	}
	if strings.Contains(idLikeLower, "fedora") || strings.Contains(idLikeLower, "rhel") {
		return constants.PMFamilyRHEL
	}
	if strings.Contains(idLikeLower, "arch") {
		return constants.PMFamilyArch
	}
	if strings.Contains(idLikeLower, "suse") {
		return constants.PMFamilySUSE
	}

	// Fallback to checking which package manager exists
	if _, err := exec.LookPath("pacman"); err == nil {
		return constants.PMFamilyArch
	}
	if _, err := exec.LookPath("dnf"); err == nil {
		return constants.PMFamilyRHEL
	}
	if _, err := exec.LookPath("apt-get"); err == nil {
		return constants.PMFamilyDebian
	}
	if _, err := exec.LookPath("zypper"); err == nil {
		return constants.PMFamilySUSE
	}

	return ""
}

// getPackageAppVersion gets the version and path of a package-installed app
func getPackageAppVersion(family, packageName string) (version string, path string) {
	switch family {
	case constants.PMFamilyDebian:
		return getAptPackageVersion(packageName)
	case constants.PMFamilyRHEL:
		return getDnfPackageVersion(packageName)
	case constants.PMFamilyArch:
		return getPacmanPackageVersion(packageName)
	case constants.PMFamilySUSE:
		return getZypperPackageVersion(packageName)
	}
	return "", ""
}

// getAptPackageVersion gets version from APT
func getAptPackageVersion(packageName string) (string, string) {
	// Check if apt is available
	if _, err := exec.LookPath("apt-get"); err != nil {
		return "", ""
	}

	// Get version
	cmd := exec.Command("apt-cache", "policy", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", ""
	}

	var version string
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "Installed:") && strings.Contains(line, ":") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				v := strings.TrimSpace(parts[1])
				if v != "(none)" {
					version = v
				}
			}
		}
	}

	if version == "" {
		return "", ""
	}

	// Get path
	path, _ := exec.LookPath(packageName)
	return version, path
}

// getDnfPackageVersion gets version from DNF
func getDnfPackageVersion(packageName string) (string, string) {
	if _, err := exec.LookPath("dnf"); err != nil {
		return "", ""
	}

	cmd := exec.Command("dnf", "info", "--installed", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", ""
	}

	var version string
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "Version") && strings.Contains(line, ":") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				v := strings.TrimSpace(parts[1])
				if strings.Contains(v, " ") {
					// Take last part if contains space
					versionParts := strings.Fields(v)
					v = versionParts[len(versionParts)-1]
				}
				version = v
			}
		}
	}

	if version == "" {
		return "", ""
	}

	path, _ := exec.LookPath(packageName)
	return version, path
}

// getPacmanPackageVersion gets version from Pacman
func getPacmanPackageVersion(packageName string) (string, string) {
	if _, err := exec.LookPath("pacman"); err != nil {
		return "", ""
	}

	// Try primary package name
	version := tryPacmanVersionQuery(packageName)
	if version == "" && packageName == "celluloid" {
		// Try alternatives for celluloid
		alternatives := []string{"celluloid-git", "gnome-mpv", "gnome-mpv-git"}
		for _, alt := range alternatives {
			version = tryPacmanVersionQuery(alt)
			if version != "" {
				break
			}
		}
	}

	if version == "" {
		return "", ""
	}

	path, _ := exec.LookPath(packageName)
	if path == "" && packageName == "celluloid" {
		path, _ = exec.LookPath("gnome-mpv") // Old binary name
	}
	return version, path
}

// tryPacmanVersionQuery attempts to get version from pacman -Qi
func tryPacmanVersionQuery(packageName string) string {
	cmd := exec.Command("pacman", "-Qi", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "Version") && strings.Contains(line, ":") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				v := strings.TrimSpace(parts[1])
				// Remove epoch if present (e.g., "1:0.41.0-2" -> "0.41.0-2")
				if strings.Contains(v, ":") {
					v = strings.SplitN(v, ":", 2)[1]
				}
				return v
			}
		}
	}

	return ""
}

// getZypperPackageVersion gets version from Zypper/RPM
func getZypperPackageVersion(packageName string) (string, string) {
	if _, err := exec.LookPath("rpm"); err != nil {
		return "", ""
	}

	cmd := exec.Command("rpm", "-q", "--queryformat", "%{VERSION}-%{RELEASE}", packageName)
	output, err := cmd.Output()
	if err != nil {
		return "", ""
	}

	version := strings.TrimSpace(string(output))
	if version == "" || strings.Contains(version, "not installed") {
		return "", ""
	}

	path, _ := exec.LookPath(packageName)
	return version, path
}

// SyncInstalledApps synchronizes detected apps with the config
// - Adds apps that are detected but not in config
// - Removes apps that are in config but no longer detected
// Returns counts of added and removed apps
func SyncInstalledApps(detected []DetectedApp) (added int, removed int) {
	// Ensure config is loaded
	_, err := config.Load()
	if err != nil {
		log.Error(fmt.Sprintf("[Detection] Failed to load config: %v", err))
		return 0, 0
	}

	// Get currently tracked apps
	trackedApps := config.GetInstalledApps()

	// Build maps for comparison
	detectedMap := make(map[string]DetectedApp)
	for _, app := range detected {
		detectedMap[app.MethodID] = app
	}

	trackedMap := make(map[string]config.InstalledApp)
	for _, app := range trackedApps {
		// Only track package manager apps (flatpak, package)
		if isPackageManagerMethod(app.InstallMethod) {
			trackedMap[app.InstallMethod] = app
		}
	}

	// Find apps to add (detected but not tracked)
	for methodID, app := range detectedMap {
		if _, exists := trackedMap[methodID]; !exists {
			log.Info(fmt.Sprintf("[Detection] Adding newly detected app: %s (%s)", app.AppName, methodID))
			newApp := config.InstalledApp{
				AppName:       app.AppName,
				AppType:       "app",
				InstallMethod: methodID,
				InstallPath:   app.InstallPath,
				InstalledDate: time.Now().Format("2006-01-02"),
				AppVersion:    app.Version,
				Managed:       false, // Externally detected, not managed by MPV Manager
			}
			if err := config.AddInstalledApp(newApp); err != nil {
				log.Warn(fmt.Sprintf("[Detection] Failed to add app %s: %v", methodID, err))
			} else {
				added++
			}
		} else {
			// Update version if it changed
			tracked := trackedMap[methodID]
			if tracked.AppVersion != app.Version {
				log.Info(fmt.Sprintf("[Detection] Updating version for %s: %s -> %s", methodID, tracked.AppVersion, app.Version))
				tracked.AppVersion = app.Version
				// Update by removing and re-adding
				config.RemoveInstalledAppByMethod(methodID)
				config.AddInstalledApp(tracked)
			}
		}
	}

	// Find apps to remove (tracked but not detected)
	for methodID := range trackedMap {
		if _, exists := detectedMap[methodID]; !exists {
			log.Info(fmt.Sprintf("[Detection] Removing stale app: %s", methodID))
			if err := config.RemoveInstalledAppByMethod(methodID); err != nil {
				log.Warn(fmt.Sprintf("[Detection] Failed to remove app %s: %v", methodID, err))
			} else {
				removed++
			}
		}
	}

	return added, removed
}

// isPackageManagerMethod checks if a method ID is a package manager type
func isPackageManagerMethod(methodID string) bool {
	return strings.Contains(methodID, "-flatpak") ||
		strings.Contains(methodID, "-package") ||
		strings.Contains(methodID, "-brew")
}

// DetectAndSyncApps is a convenience function that detects and syncs in one call
func DetectAndSyncApps() (added int, removed int) {
	detected := DetectInstalledMPVApps()
	return SyncInstalledApps(detected)
}
