package version

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"gitgud.io/mike/mpv-manager/pkg/config"
	"gitgud.io/mike/mpv-manager/pkg/log"
	"lukechampine.com/blake3"
)

const (
	CurrentVersion     = "1.0.0"
	ReleasesURL        = "https://mpv.rocks/api/releases.json"
	UpdateCheckTimeout = 10 * time.Second
)

var (
	BuildTime string
	GitCommit string
)

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"` // material-design-icons.ttf
		FontBLAKE3       string `json:"font_blake3"`
		FluentFontURL    string `json:"fluent_font_url"` // fluent-system-icons.ttf
		FluentFontBLAKE3 string `json:"fluent_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"`
	FFmpeg struct {
		I686       struct{ URL, BLAKE3 string } `json:"i686"`
		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"`
	Manager struct {
		LinuxAMD64 struct{ URL, BLAKE3 string } `json:"linux-amd64"`
		LinuxARM64 struct{ URL, BLAKE3 string } `json:"linux-arm64"`
		WinX86_64  struct{ URL, BLAKE3 string } `json:"win-x86_64"`
		WinARM64   struct{ URL, BLAKE3 string } `json:"win-arm64"`
		MacosIntel struct{ URL, BLAKE3 string } `json:"macos-intel"`
		MacosARM   struct{ URL, BLAKE3 string } `json:"macos-arm"`
	} `json:"manager"`
}

type VersionCheckResult struct {
	CurrentVersion       string
	LatestVersion        string
	MpvVersion           string
	UOSCLatestVersion    string
	ModernZLatestVersion string
	FFmpegLatestVersion  string
	MPQTLLatestVersion   string
	INALatestVersion     string
	UpdateAvailable      bool
	URL                  string
	BLAKE3               string
	Error                error
}

func GetCurrentVersion() string {
	return CurrentVersion
}

func CheckForUpdate() *VersionCheckResult {
	result := &VersionCheckResult{
		CurrentVersion: CurrentVersion,
	}

	client := &http.Client{
		Timeout: UpdateCheckTimeout,
	}

	log.Info("Checking for manager updates...")
	resp, err := client.Get(ReleasesURL)
	if err != nil {
		log.Error("Failed to check for updates: " + err.Error())
		result.Error = fmt.Errorf("failed to check for updates: %w", err)
		return result
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Error(fmt.Sprintf("Failed to check for updates: HTTP %d", resp.StatusCode))
		result.Error = fmt.Errorf("failed to check for updates: HTTP %d", resp.StatusCode)
		return result
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Error("Failed to read update response: " + err.Error())
		result.Error = fmt.Errorf("failed to read response: %w", err)
		return result
	}

	var release ReleaseInfo
	if err := json.Unmarshal(body, &release); err != nil {
		log.Error("Failed to parse release info: " + err.Error())
		result.Error = fmt.Errorf("failed to parse release info: %w", err)
		return result
	}

	log.Info(fmt.Sprintf("Current version: %s, Latest version: %s", CurrentVersion, release.Version))

	result.MpvVersion = release.MpvVersion
	result.UOSCLatestVersion = release.UOSC.AppVersion
	result.ModernZLatestVersion = release.ModernZ.AppVersion
	result.FFmpegLatestVersion = release.FFmpeg.AppVersion
	result.MPQTLLatestVersion = release.MPCQT.AppVersion
	result.INALatestVersion = release.IINA.AppVersion

	var managerURL, managerBLAKE3 string

	osName := runtime.GOOS
	archName := runtime.GOARCH

	switch {
	case osName == "linux" && archName == "amd64":
		managerURL = release.Manager.LinuxAMD64.URL
		managerBLAKE3 = release.Manager.LinuxAMD64.BLAKE3
	case osName == "linux" && archName == "arm64":
		managerURL = release.Manager.LinuxARM64.URL
		managerBLAKE3 = release.Manager.LinuxARM64.BLAKE3
	case osName == "windows" && archName == "amd64":
		managerURL = release.Manager.WinX86_64.URL
		managerBLAKE3 = release.Manager.WinX86_64.BLAKE3
	case osName == "windows" && archName == "arm64":
		managerURL = release.Manager.WinARM64.URL
		managerBLAKE3 = release.Manager.WinARM64.BLAKE3
	case osName == "darwin" && archName == "amd64":
		managerURL = release.Manager.MacosIntel.URL
		managerBLAKE3 = release.Manager.MacosIntel.BLAKE3
	case osName == "darwin" && archName == "arm64":
		managerURL = release.Manager.MacosARM.URL
		managerBLAKE3 = release.Manager.MacosARM.BLAKE3
	default:
		managerURL = ""
		managerBLAKE3 = ""
	}

	result.LatestVersion = release.Version
	result.URL = managerURL
	result.BLAKE3 = managerBLAKE3

	result.UpdateAvailable = CompareVersions(CurrentVersion, release.Version) < 0

	return result
}

func CompareVersions(v1, v2 string) int {
	v1Parts := strings.Split(strings.TrimPrefix(v1, "v"), ".")
	v2Parts := strings.Split(strings.TrimPrefix(v2, "v"), ".")

	maxLen := len(v1Parts)
	if len(v2Parts) > maxLen {
		maxLen = len(v2Parts)
	}

	for i := 0; i < maxLen; i++ {
		var v1Num, v2Num int
		if i < len(v1Parts) {
			fmt.Sscanf(v1Parts[i], "%d", &v1Num)
		}
		if i < len(v2Parts) {
			fmt.Sscanf(v2Parts[i], "%d", &v2Num)
		}

		if v1Num < v2Num {
			return -1
		} else if v1Num > v2Num {
			return 1
		}
	}

	return 0
}

func IsVersionUpdateAvailable(current, latest string) bool {
	return CompareVersions(current, latest) < 0
}

func UpdateSelf(executablePath string) error {
	log.Info("Starting manager self-update...")
	check := CheckForUpdate()
	if check.Error != nil {
		log.Error("Failed to check for update: " + check.Error.Error())
		return check.Error
	}

	if !check.UpdateAvailable {
		log.Info("No update available, manager is up to date")
		return fmt.Errorf("no update available")
	}

	log.Info("Update available: " + check.LatestVersion)
	tempPath := executablePath + ".new"

	if err := downloadFile(check.URL, tempPath); err != nil {
		log.Error("Failed to download update: " + err.Error())
		return err
	}

	if check.BLAKE3 != "" {
		if err := VerifyBLAKE3(tempPath, check.BLAKE3); err != nil {
			log.Error("BLAKE3 verification failed: " + err.Error())
			os.Remove(tempPath)
			return err
		}
		log.Info("BLAKE3 verification passed")
	}

	backupPath := executablePath + ".backup"
	if err := os.Rename(executablePath, backupPath); err != nil {
		log.Error("Failed to create backup: " + err.Error())
		os.Remove(tempPath)
		return err
	}
	log.Info("Backup created: " + backupPath)

	if err := os.Rename(tempPath, executablePath); err != nil {
		log.Error("Failed to replace executable: " + err.Error())
		os.Rename(backupPath, executablePath)
		return err
	}

	// Set executable permissions on the new binary
	if err := os.Chmod(executablePath, 0755); err != nil {
		log.Error("Failed to set executable permissions: " + err.Error())
		return fmt.Errorf("failed to set executable permissions: %w", err)
	}

	os.Remove(backupPath)
	log.Info("Self-update completed successfully")

	// Update secondary installation if configured (Windows PATH or Linux self-install)
	if err := updateSecondaryInstallation(check.URL, check.BLAKE3, executablePath); err != nil {
		log.Error("Failed to update secondary installation: " + err.Error())
		// Don't return error - the main update succeeded
	}

	return nil
}

// updateSecondaryInstallation updates the manager binary at the configured path
// This handles both Windows PATH installations and Linux self-installations
func updateSecondaryInstallation(downloadURL, blake3Hash, executablePath string) error {
	binPath := config.GetManagerBinPath()
	if binPath == "" {
		return nil
	}

	// Resolve symlinks for comparison
	realExecPath, err := filepath.EvalSymlinks(executablePath)
	if err != nil {
		realExecPath = executablePath
	}

	realBinPath, err := filepath.EvalSymlinks(binPath)
	if err != nil {
		// If binPath doesn't exist, that's OK - nothing to update
		return nil
	}

	if realBinPath == realExecPath {
		// Same file, no need to update
		return nil
	}

	log.Info("Updating installation at: " + binPath)
	tempPath := binPath + ".new"

	if err := downloadFile(downloadURL, tempPath); err != nil {
		return fmt.Errorf("failed to download update: %w", err)
	}

	if blake3Hash != "" {
		if err := VerifyBLAKE3(tempPath, blake3Hash); err != nil {
			os.Remove(tempPath)
			return fmt.Errorf("verification failed: %w", err)
		}
	}

	input, err := os.ReadFile(tempPath)
	if err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to read downloaded file: %w", err)
	}

	if err := os.WriteFile(binPath, input, 0755); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to write to %s: %w", binPath, err)
	}

	if err := os.Chmod(binPath, 0755); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to set executable permissions: %w", err)
	}

	os.Remove(tempPath)
	log.Info("Installation updated at: " + binPath)
	return nil
}

func UpdateSelfWithProgress(executablePath string, progressCallback func(int64, int64)) error {
	log.Info("Starting manager self-update with progress...")
	check := CheckForUpdate()
	if check.Error != nil {
		log.Error("Failed to check for update: " + check.Error.Error())
		return check.Error
	}

	if !check.UpdateAvailable {
		log.Info("No update available, manager is up to date")
		return fmt.Errorf("no update available")
	}

	log.Info("Update available: " + check.LatestVersion)
	tempPath := executablePath + ".new"

	if err := downloadFileWithProgress(check.URL, tempPath, progressCallback, 3); err != nil {
		log.Error("Failed to download update: " + err.Error())
		return err
	}

	if check.BLAKE3 != "" {
		if err := VerifyBLAKE3(tempPath, check.BLAKE3); err != nil {
			log.Error("BLAKE3 verification failed: " + err.Error())
			os.Remove(tempPath)
			return err
		}
		log.Info("BLAKE3 verification passed")
	}

	backupPath := executablePath + ".backup"
	if err := os.Rename(executablePath, backupPath); err != nil {
		log.Error("Failed to create backup: " + err.Error())
		os.Remove(tempPath)
		return err
	}
	log.Info("Backup created: " + backupPath)

	if err := os.Rename(tempPath, executablePath); err != nil {
		log.Error("Failed to replace executable: " + err.Error())
		os.Rename(backupPath, executablePath)
		return err
	}

	// Set executable permissions on the new binary
	if err := os.Chmod(executablePath, 0755); err != nil {
		log.Error("Failed to set executable permissions: " + err.Error())
		return fmt.Errorf("failed to set executable permissions: %w", err)
	}

	os.Remove(backupPath)
	log.Info("Self-update completed successfully")

	// Update secondary installation if configured (Windows PATH or Linux self-install)
	if err := updateSecondaryInstallationWithProgress(check.URL, check.BLAKE3, executablePath, progressCallback); err != nil {
		log.Error("Failed to update secondary installation: " + err.Error())
		// Don't return error - the main update succeeded
	}

	return nil
}

// updateSecondaryInstallationWithProgress updates the manager binary at the configured path with progress callback
// This handles both Windows PATH installations and Linux self-installations
func updateSecondaryInstallationWithProgress(downloadURL, blake3Hash, executablePath string, progressCallback func(int64, int64)) error {
	binPath := config.GetManagerBinPath()
	if binPath == "" {
		return nil
	}

	// Resolve symlinks for comparison
	realExecPath, err := filepath.EvalSymlinks(executablePath)
	if err != nil {
		realExecPath = executablePath
	}

	realBinPath, err := filepath.EvalSymlinks(binPath)
	if err != nil {
		// If binPath doesn't exist, that's OK - nothing to update
		return nil
	}

	if realBinPath == realExecPath {
		// Same file, no need to update
		return nil
	}

	log.Info("Updating installation at: " + binPath)
	tempPath := binPath + ".new"

	if err := downloadFileWithProgress(downloadURL, tempPath, progressCallback, 3); err != nil {
		return fmt.Errorf("failed to download update: %w", err)
	}

	if blake3Hash != "" {
		if err := VerifyBLAKE3(tempPath, blake3Hash); err != nil {
			os.Remove(tempPath)
			return fmt.Errorf("verification failed: %w", err)
		}
	}

	input, err := os.ReadFile(tempPath)
	if err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to read downloaded file: %w", err)
	}

	if err := os.WriteFile(binPath, input, 0755); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to write to %s: %w", binPath, err)
	}

	if err := os.Chmod(binPath, 0755); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("failed to set executable permissions: %w", err)
	}

	os.Remove(tempPath)
	log.Info("Installation updated at: " + binPath)
	return nil
}

func downloadFile(url, dest string) error {
	log.Debug("Downloading file from: " + url)
	resp, err := http.Get(url)
	if err != nil {
		log.Error("Failed to download from " + url + ": " + err.Error())
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Error(fmt.Sprintf("Download failed with HTTP %d for %s", resp.StatusCode, url))
		return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
	}

	out, err := os.Create(dest)
	if err != nil {
		log.Error("Failed to create file " + dest + ": " + err.Error())
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	if err != nil {
		log.Error("Failed to write to " + dest + ": " + err.Error())
		return err
	}
	log.Debug("Download completed successfully: " + dest)
	return nil
}

func downloadFileWithProgress(url, dest string, progressCallback func(int64, int64), maxRetries int) error {
	log.Debug(fmt.Sprintf("Downloading file from: %s (max retries: %d)", url, maxRetries))
	var lastErr error
	for attempt := 1; attempt <= maxRetries; attempt++ {
		resp, err := http.Get(url)
		if err != nil {
			lastErr = err
			log.Error(fmt.Sprintf("Download attempt %d/%d failed: %s", attempt, maxRetries, err.Error()))
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt) * time.Second)
			}
			continue
		}

		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			lastErr = fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
			log.Error(fmt.Sprintf("Download attempt %d/%d failed: HTTP %d", attempt, maxRetries, resp.StatusCode))
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt) * time.Second)
			}
			continue
		}

		if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
			resp.Body.Close()
			log.Error("Failed to create directory " + filepath.Dir(dest) + ": " + err.Error())
			return err
		}

		out, err := os.Create(dest)
		if err != nil {
			resp.Body.Close()
			log.Error("Failed to create file " + dest + ": " + err.Error())
			return err
		}

		if progressCallback != nil {
			contentLength := resp.ContentLength
			writer := &progressWriter{
				total:      contentLength,
				written:    0,
				callback:   progressCallback,
				underlying: out,
				lastUpdate: time.Now(),
			}
			_, err = io.Copy(writer, resp.Body)
			resp.Body.Close()
			out.Close()
			if err != nil {
				lastErr = err
				log.Error(fmt.Sprintf("Download attempt %d/%d failed: %s", attempt, maxRetries, err.Error()))
				os.Remove(dest)
				if attempt < maxRetries {
					time.Sleep(time.Duration(attempt) * time.Second)
				}
				continue
			}
			log.Debug("Download completed successfully: " + dest)
			return nil
		}

		_, err = io.Copy(out, resp.Body)
		resp.Body.Close()
		out.Close()
		if err != nil {
			lastErr = err
			log.Error(fmt.Sprintf("Download attempt %d/%d failed: %s", attempt, maxRetries, err.Error()))
			os.Remove(dest)
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt) * time.Second)
			}
			continue
		}
		log.Debug("Download completed successfully: " + dest)
		return nil
	}
	log.Error(fmt.Sprintf("Download failed after %d attempts: %s", maxRetries, lastErr.Error()))
	return fmt.Errorf("download failed after %d attempts: %w", maxRetries, lastErr)
}

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
}

// VerifyBLAKE3 verifies file BLAKE3 hash against expected value
func VerifyBLAKE3(filePath, expectedHash string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	hash := blake3.New(32, nil)
	if _, err := io.Copy(hash, file); err != nil {
		return fmt.Errorf("failed to compute hash: %w", err)
	}

	computedHash := "blake3:" + hex.EncodeToString(hash.Sum(nil))
	if computedHash != expectedHash {
		return fmt.Errorf("BLAKE3 hash mismatch: expected %s, got %s", expectedHash, computedHash)
	}

	return nil
}
