package version

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

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

type UpdateCheckResult struct {
	AppsToUpdate      []UpdateItem
	NoEligibleApps    bool
	ConfigBackupItems []ConfigBackupItem
}

type UpdateItem struct {
	AppName          string
	CurrentVersion   string
	AvailableVersion string
	UpdateType       string
	PackageManager   string
	MethodID         string
}

type ConfigBackupItem struct {
	BackupName string
	BackupPath string
	BackupDate string
}

func CheckForAppUpdates(releaseInfo *installer.ReleaseInfo, installedApps []config.InstalledApp) *UpdateCheckResult {
	result := &UpdateCheckResult{
		AppsToUpdate:      []UpdateItem{},
		ConfigBackupItems: []ConfigBackupItem{},
	}

	for _, app := range installedApps {
		updateItem := checkAppForUpdate(app, releaseInfo)
		if updateItem != nil {
			result.AppsToUpdate = append(result.AppsToUpdate, *updateItem)
		}
	}

	checkUOSCUpdate(result, releaseInfo)
	checkModernZUpdate(result, releaseInfo)
	checkFFmpegUpdate(result, releaseInfo)

	hasManagedApp := false
	for _, app := range installedApps {
		if isManagedApp(app.InstallMethod) {
			hasManagedApp = true
			break
		}
	}

	result.NoEligibleApps = !hasManagedApp && len(result.AppsToUpdate) == 0

	checkConfigBackups(result)

	return result
}

func isManagedApp(methodID string) bool {
	managedMethods := []string{
		constants.MethodMPVBinary, constants.MethodMPVBinaryV3, constants.MethodMPVApp,
		constants.MethodMPCQT, constants.MethodIINA,
	}
	for _, m := range managedMethods {
		if methodID == m {
			return true
		}
	}
	return false
}

func checkAppForUpdate(app config.InstalledApp, releaseInfo *installer.ReleaseInfo) *UpdateItem {
	switch app.InstallMethod {
	case constants.MethodMPVBinary, constants.MethodMPVBinaryV3, constants.MethodMPVApp:
		currentVersion := app.AppVersion
		availableVersion := releaseInfo.MpvVersion
		updateAvailable := IsVersionUpdateAvailable(currentVersion, availableVersion)
		// Determine UpdateType based on whether update is available
		updateType := "current-version"
		if updateAvailable {
			updateType = "update"
		}
		return &UpdateItem{
			AppName:          app.AppName,
			CurrentVersion:   currentVersion,
			AvailableVersion: availableVersion,
			UpdateType:       updateType,
			MethodID:         app.InstallMethod,
		}
	case constants.MethodMPCQT:
		currentVersion := app.AppVersion
		availableVersion := releaseInfo.MPCQT.AppVersion
		updateAvailable := IsVersionUpdateAvailable(currentVersion, availableVersion)
		// Determine UpdateType based on whether update is available
		updateType := "current-version"
		if updateAvailable {
			updateType = "update"
		}
		return &UpdateItem{
			AppName:          app.AppName,
			CurrentVersion:   currentVersion,
			AvailableVersion: availableVersion,
			UpdateType:       updateType,
			MethodID:         app.InstallMethod,
		}
	case constants.MethodIINA:
		currentVersion := app.AppVersion
		availableVersion := releaseInfo.IINA.AppVersion
		updateAvailable := IsVersionUpdateAvailable(currentVersion, availableVersion)
		// Determine UpdateType based on whether update is available
		updateType := "current-version"
		if updateAvailable {
			updateType = "update"
		}
		return &UpdateItem{
			AppName:          app.AppName,
			CurrentVersion:   currentVersion,
			AvailableVersion: availableVersion,
			UpdateType:       updateType,
			MethodID:         app.InstallMethod,
		}
	case constants.MethodMPVFlatpak, constants.MethodMPVSnap, constants.MethodMPVPackage,
		constants.MethodCelluloidFlatpak, constants.MethodCelluloidSnap,
		constants.MethodCelluloidPackage, constants.MethodMPVBrew:
		pkgManager := getPackageManagerName(app.InstallMethod)
		currentVersion := app.AppVersion

		// If no version is stored, try to query package manager
		if currentVersion == "" {
			currentVersion = queryPackageManagerVersion(app.InstallMethod, app.AppName)
		}

		// Always return update item for version display, even if version query failed
		// For package managers, show current version without update button
		availableVersion := "Check via " + pkgManager
		if currentVersion == "" {
			availableVersion = "Unknown"
		}

		return &UpdateItem{
			AppName:          app.AppName,
			CurrentVersion:   currentVersion,
			AvailableVersion: availableVersion,
			UpdateType:       "package-info",
			PackageManager:   pkgManager,
			MethodID:         app.InstallMethod,
		}

		return nil
	}

	return nil
}

func checkUOSCUpdate(result *UpdateCheckResult, releaseInfo *installer.ReleaseInfo) {
	installedUOSCVersion := config.GetUOSCVersion()
	if installedUOSCVersion == "" {
		return
	}

	availableUOSCVersion := releaseInfo.UOSC.AppVersion
	if IsVersionUpdateAvailable(installedUOSCVersion, availableUOSCVersion) {
		result.AppsToUpdate = append(result.AppsToUpdate, UpdateItem{
			AppName:          "uOSC - custom MPV UI",
			CurrentVersion:   installedUOSCVersion,
			AvailableVersion: availableUOSCVersion,
			UpdateType:       "update",
			MethodID:         constants.MethodUOSC,
		})
	}
}

func checkModernZUpdate(result *UpdateCheckResult, releaseInfo *installer.ReleaseInfo) {
	installedModernZVersion := config.GetModernZVersion()
	if installedModernZVersion == "" {
		return
	}

	availableModernZVersion := releaseInfo.ModernZ.AppVersion
	if IsVersionUpdateAvailable(installedModernZVersion, availableModernZVersion) {
		result.AppsToUpdate = append(result.AppsToUpdate, UpdateItem{
			AppName:          "ModernZ - custom MPV UI",
			CurrentVersion:   installedModernZVersion,
			AvailableVersion: availableModernZVersion,
			UpdateType:       "update",
			MethodID:         constants.MethodModernZ,
		})
	}
}

func checkFFmpegUpdate(result *UpdateCheckResult, releaseInfo *installer.ReleaseInfo) {
	installedFFmpegVersion := config.GetFFmpegVersion()
	if installedFFmpegVersion == "" {
		return
	}

	availableFFmpegVersion := releaseInfo.FFmpeg.AppVersion
	if IsVersionUpdateAvailable(installedFFmpegVersion, availableFFmpegVersion) {
		result.AppsToUpdate = append(result.AppsToUpdate, UpdateItem{
			AppName:          "FFmpeg - media utilities",
			CurrentVersion:   installedFFmpegVersion,
			AvailableVersion: availableFFmpegVersion,
			UpdateType:       "update",
			MethodID:         constants.MethodFFmpeg,
		})
	}
}

func getPackageManagerName(methodID string) string {
	switch methodID {
	case constants.MethodMPVFlatpak, constants.MethodCelluloidFlatpak:
		return "Flatpak"
	case constants.MethodMPVSnap, constants.MethodCelluloidSnap:
		return "Snap"
	case constants.MethodMPVPackage, constants.MethodCelluloidPackage:
		return "Package Manager"
	case constants.MethodMPVBrew:
		return "Homebrew"
	}
	return "Unknown"
}

func checkConfigBackups(result *UpdateCheckResult) {
	configDir := config.GetMPVConfigPath()
	if configDir == "" {
		return
	}

	backupDir := configDir + "/conf_backups"
	backupDir = strings.ReplaceAll(backupDir, "~", "")

	if err := checkAndCollectBackups(backupDir, &result.ConfigBackupItems); err != nil {
		return
	}
}

func checkAndCollectBackups(backupDir string, items *[]ConfigBackupItem) error {
	files, err := os.ReadDir(backupDir)
	if err != nil {
		return err
	}

	for _, file := range files {
		fileName := file.Name()
		if strings.HasSuffix(fileName, ".conf") {
			backupName := strings.TrimSuffix(fileName, ".conf")
			backupPath := filepath.Join(backupDir, fileName)
			info, err := os.Stat(backupPath)
			if err != nil {
				continue
			}

			*items = append(*items, ConfigBackupItem{
				BackupName: backupName,
				BackupPath: backupPath,
				BackupDate: info.ModTime().Format("2006-01-02 15:04"),
			})
		}
	}

	return nil
}

func queryPackageManagerVersion(methodID, appName string) string {
	switch {
	case strings.Contains(methodID, "flatpak"):
		return queryFlatpakVersion(appName)
	case strings.Contains(methodID, "snap"):
		return querySnapVersion(appName)
	case strings.Contains(methodID, "brew"):
		return queryBrewVersion(appName)
	case strings.Contains(methodID, "package"):
		// Detect distro and use appropriate package manager
		distroFamily := detectDistroFamily()
		switch distroFamily {
		case "arch":
			return queryPacmanVersion(appName)
		case "rhel":
			return queryDnfVersion(appName)
		case "suse":
			return queryZypperVersion(appName)
		default:
			return queryAptVersion(appName)
		}
	default:
		return ""
	}
}

// detectDistroFamily reads /etc/os-release and returns the distro family using platform.GetDistroFamily
func detectDistroFamily() string {
	if _, err := os.Stat("/etc/os-release"); err != nil {
		return "debian" // default
	}

	file, err := os.Open("/etc/os-release")
	if err != nil {
		return "debian"
	}
	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="), `"`)
		}
	}

	return platform.GetDistroFamily(id, idLike)
}

func queryFlatpakVersion(appName string) string {
	packageName := getFlatpakPackageName(appName)
	log.Debug(fmt.Sprintf("Querying Flatpak version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("flatpak", "info", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Debug(fmt.Sprintf("Flatpak info command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "Version:") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				version := strings.TrimSpace(parts[1])
				log.Debug(fmt.Sprintf("Found Flatpak version for %s: %s", appName, version))
				return version
			}
		}
	}
	log.Debug("No Flatpak version found for: " + appName)
	return ""
}

func querySnapVersion(appName string) string {
	packageName := getSnapPackageName(appName)
	log.Debug(fmt.Sprintf("Querying Snap version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("snap", "info", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Debug(fmt.Sprintf("Snap info command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.Contains(line, "installed:") && !strings.Contains(line, "summary:") {
			parts := strings.SplitN(line, " ", 2)
			if len(parts) == 2 {
				installedInfo := strings.TrimSpace(parts[1])
				versionParts := strings.Fields(installedInfo)
				if len(versionParts) > 1 {
					version := versionParts[1]
					log.Debug(fmt.Sprintf("Found Snap version for %s: %s", appName, version))
					return version
				}
			}
		}
	}
	log.Debug("No Snap version found for: " + appName)
	return ""
}

func queryBrewVersion(appName string) string {
	packageName := getBrewPackageName(appName)
	log.Debug(fmt.Sprintf("Querying Brew version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("brew", "list", "--versions", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Debug(fmt.Sprintf("Brew list command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, packageName) {
			parts := strings.Fields(line)
			if len(parts) >= 2 {
				version := parts[1]
				log.Debug(fmt.Sprintf("Found Brew version for %s: %s", appName, version))
				return version
			}
		}
	}
	log.Debug("No Brew version found for: " + appName)
	return ""
}

func queryAptVersion(appName string) string {
	packageName := getAptPackageName(appName)
	log.Debug(fmt.Sprintf("Querying APT version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("apt-cache", "policy", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Debug(fmt.Sprintf("APT cache command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "Installed:") {
			parts := strings.SplitN(line, ":", 2)
			if len(parts) == 2 {
				version := strings.TrimSpace(parts[1])
				if version != "(none)" {
					log.Debug(fmt.Sprintf("Found APT version for %s: %s", appName, version))
					return version
				}
			}
		}
	}
	log.Debug("No APT version found for: " + appName)
	return ""
}

func getFlatpakPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return constants.FlatpakMPV
	case "celluloid":
		return constants.FlatpakCelluloid
	default:
		return appName
	}
}

func getSnapPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	case "celluloid":
		return "celluloid"
	default:
		return appName
	}
}

func getBrewPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	default:
		return appName
	}
}

func getAptPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	case "celluloid":
		return "celluloid"
	default:
		return appName
	}
}

func queryPacmanVersion(appName string) string {
	packageName := getPacmanPackageName(appName)
	log.Debug(fmt.Sprintf("Querying pacman version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("pacman", "-Qi", packageName)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Debug(fmt.Sprintf("pacman command failed for %s: %s", packageName, err.Error()))
		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 {
				version := strings.TrimSpace(parts[1])
				// Handle version format like "1.2.3-1"
				if idx := strings.Index(version, " "); idx > 0 {
					version = version[:idx]
				}
				log.Debug(fmt.Sprintf("Found pacman version for %s: %s", appName, version))
				return version
			}
		}
	}
	log.Debug("No pacman version found for: " + appName)
	return ""
}

func queryDnfVersion(appName string) string {
	packageName := getDnfPackageName(appName)
	log.Debug(fmt.Sprintf("Querying DNF version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("rpm", "-q", "--queryformat", "%{VERSION}", packageName)
	output, err := cmd.Output()
	if err != nil {
		log.Debug(fmt.Sprintf("DNF/RPM command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	version := strings.TrimSpace(string(output))
	if version != "" {
		log.Debug(fmt.Sprintf("Found DNF version for %s: %s", appName, version))
		return version
	}
	log.Debug("No DNF version found for: " + appName)
	return ""
}

func getPacmanPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	case "celluloid":
		return "celluloid"
	default:
		return appName
	}
}

func getDnfPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	case "celluloid":
		return "celluloid"
	default:
		return appName
	}
}

func queryZypperVersion(appName string) string {
	packageName := getZypperPackageName(appName)
	log.Debug(fmt.Sprintf("Querying Zypper version for: %s (package: %s)", appName, packageName))
	cmd := exec.Command("rpm", "-q", "--queryformat", "%{VERSION}", packageName)
	output, err := cmd.Output()
	if err != nil {
		log.Debug(fmt.Sprintf("Zypper/RPM command failed for %s: %s", packageName, err.Error()))
		return ""
	}

	version := strings.TrimSpace(string(output))
	if version != "" {
		log.Debug(fmt.Sprintf("Found Zypper version for %s: %s", appName, version))
		return version
	}
	log.Debug("No Zypper version found for: " + appName)
	return ""
}

func getZypperPackageName(appName string) string {
	appName = strings.ToLower(appName)
	switch appName {
	case "mpv":
		return "mpv"
	case "celluloid":
		return "celluloid"
	default:
		return appName
	}
}
