// Package main provides self-hosted update functionality for EMLy.
// This file contains methods for checking, downloading, and installing updates
// from a corporate network share without relying on third-party services.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unsafe"

	"github.com/wailsapp/wails/v2/pkg/runtime"
)

// =============================================================================
// Update System Types
// =============================================================================

// UpdateManifest represents the version.json file structure on the network share
type UpdateManifest struct {
	StableVersion   string            `json:"stableVersion"`
	BetaVersion     string            `json:"betaVersion"`
	StableDownload  string            `json:"stableDownload"`
	BetaDownload    string            `json:"betaDownload"`
	SHA256Checksums map[string]string `json:"sha256Checksums"`
	ReleaseNotes    map[string]string `json:"releaseNotes,omitempty"`
}

// UpdateStatus represents the current state of the update system
type UpdateStatus struct {
	CurrentVersion   string `json:"currentVersion"`
	AvailableVersion string `json:"availableVersion"`
	UpdateAvailable  bool   `json:"updateAvailable"`
	Checking         bool   `json:"checking"`
	Downloading      bool   `json:"downloading"`
	DownloadProgress int    `json:"downloadProgress"`
	Ready            bool   `json:"ready"`
	InstallerPath    string `json:"installerPath"`
	ErrorMessage     string `json:"errorMessage"`
	ReleaseNotes     string `json:"releaseNotes,omitempty"`
	LastCheckTime    string `json:"lastCheckTime"`
}

// Global update state
var updateStatus = UpdateStatus{
	CurrentVersion:   "",
	AvailableVersion: "",
	UpdateAvailable:  false,
	Checking:         false,
	Downloading:      false,
	DownloadProgress: 0,
	Ready:            false,
	InstallerPath:    "",
	ErrorMessage:     "",
}

// =============================================================================
// Update Check Methods
// =============================================================================

// CheckForUpdates checks the configured network share for available updates.
// Compares the manifest version with the current GUI version based on release channel.
//
// Returns:
//   - UpdateStatus: Current update state including available version
//   - error: Error if check fails (network, parsing, etc.)
func (a *App) CheckForUpdates() (UpdateStatus, error) {
	// Reset status
	updateStatus.Checking = true
	updateStatus.ErrorMessage = ""
	updateStatus.LastCheckTime = time.Now().Format("2006-01-02 15:04:05")
	runtime.EventsEmit(a.ctx, "update:status", updateStatus)

	// Get current version from config
	config := a.GetConfig()
	if config == nil {
		updateStatus.ErrorMessage = "Failed to load configuration"
		updateStatus.Checking = false
		return updateStatus, fmt.Errorf("failed to load config")
	}

	updateStatus.CurrentVersion = config.EMLy.GUISemver
	currentChannel := config.EMLy.GUIReleaseChannel

	// Check if updates are enabled
	if config.EMLy.UpdateCheckEnabled != "true" {
		updateStatus.ErrorMessage = "Update checking is disabled"
		updateStatus.Checking = false
		return updateStatus, fmt.Errorf("update checking is disabled in config")
	}

	// Validate update path
	updatePath := strings.TrimSpace(config.EMLy.UpdatePath)
	if updatePath == "" {
		updateStatus.ErrorMessage = "Update path not configured"
		updateStatus.Checking = false
		return updateStatus, fmt.Errorf("UPDATE_PATH is empty in config.ini")
	}

	// Load manifest from network share
	manifest, err := a.loadUpdateManifest(updatePath)
	if err != nil {
		updateStatus.ErrorMessage = fmt.Sprintf("Failed to load manifest: %v", err)
		updateStatus.Checking = false
		return updateStatus, err
	}

	// Determine target version based on release channel
	var targetVersion string
	if currentChannel == "beta" {
		targetVersion = manifest.BetaVersion
	} else {
		targetVersion = manifest.StableVersion
	}

	updateStatus.AvailableVersion = targetVersion

	// Compare versions
	comparison := compareSemanticVersions(updateStatus.CurrentVersion, targetVersion)
	if comparison < 0 {
		// New version available
		updateStatus.UpdateAvailable = true
		updateStatus.InstallerPath = "" // Reset installer path
		updateStatus.Ready = false

		// Get release notes if available
		if notes, ok := manifest.ReleaseNotes[targetVersion]; ok {
			updateStatus.ReleaseNotes = notes
		}

		log.Printf("Update available: %s -> %s (%s channel)",
			updateStatus.CurrentVersion, targetVersion, currentChannel)
	} else {
		updateStatus.UpdateAvailable = false
		updateStatus.InstallerPath = ""
		updateStatus.Ready = false
		updateStatus.ReleaseNotes = ""
		log.Printf("Already on latest version: %s (%s channel)",
			updateStatus.CurrentVersion, currentChannel)
	}

	updateStatus.Checking = false
	return updateStatus, nil
}

// loadUpdateManifest reads and parses version.json from the network share
func (a *App) loadUpdateManifest(updatePath string) (*UpdateManifest, error) {
	// Resolve path (handle UNC paths, file:// URLs, local paths)
	manifestPath, err := resolveUpdatePath(updatePath, "version.json")
	if err != nil {
		return nil, fmt.Errorf("failed to resolve manifest path: %w", err)
	}

	log.Printf("Loading update manifest from: %s", manifestPath)

	// Read manifest file
	data, err := os.ReadFile(manifestPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read manifest file: %w", err)
	}

	// Parse JSON
	var manifest UpdateManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("failed to parse manifest JSON: %w", err)
	}

	// Validate manifest
	if manifest.StableVersion == "" || manifest.StableDownload == "" {
		return nil, fmt.Errorf("invalid manifest: missing stable version or download")
	}

	return &manifest, nil
}

// =============================================================================
// Download Methods
// =============================================================================

// DownloadUpdate downloads the installer from the network share to a temporary location.
// Verifies SHA256 checksum if provided in the manifest.
//
// Returns:
//   - string: Path to the downloaded installer
//   - error: Error if download or verification fails
func (a *App) DownloadUpdate() (string, error) {
	if !updateStatus.UpdateAvailable {
		return "", fmt.Errorf("no update available")
	}

	updateStatus.Downloading = true
	updateStatus.DownloadProgress = 0
	updateStatus.ErrorMessage = ""
	runtime.EventsEmit(a.ctx, "update:status", updateStatus)

	defer func() {
		updateStatus.Downloading = false
		runtime.EventsEmit(a.ctx, "update:status", updateStatus)
	}()

	// Get config
	config := a.GetConfig()
	if config == nil {
		updateStatus.ErrorMessage = "Failed to load configuration"
		return "", fmt.Errorf("failed to load config")
	}

	updatePath := strings.TrimSpace(config.EMLy.UpdatePath)
	currentChannel := config.EMLy.GUIReleaseChannel

	// Reload manifest to get download filename
	manifest, err := a.loadUpdateManifest(updatePath)
	if err != nil {
		updateStatus.ErrorMessage = "Failed to load manifest"
		return "", err
	}

	// Determine download filename
	var downloadFilename string
	if currentChannel == "beta" {
		downloadFilename = manifest.BetaDownload
	} else {
		downloadFilename = manifest.StableDownload
	}

	// Resolve source path
	sourcePath, err := resolveUpdatePath(updatePath, downloadFilename)
	if err != nil {
		updateStatus.ErrorMessage = "Failed to resolve installer path"
		return "", fmt.Errorf("failed to resolve installer path: %w", err)
	}

	log.Printf("Downloading installer from: %s", sourcePath)

	// Create temp directory for download
	tempDir := filepath.Join(os.TempDir(), "emly_update")
	if err := os.MkdirAll(tempDir, 0755); err != nil {
		updateStatus.ErrorMessage = "Failed to create temp directory"
		return "", fmt.Errorf("failed to create temp directory: %w", err)
	}

	// Destination path
	destPath := filepath.Join(tempDir, downloadFilename)

	// Copy file with progress
	if err := a.copyFileWithProgress(sourcePath, destPath); err != nil {
		updateStatus.ErrorMessage = "Download failed"
		return "", fmt.Errorf("failed to copy installer: %w", err)
	}

	// Verify checksum if available
	if checksum, ok := manifest.SHA256Checksums[downloadFilename]; ok {
		log.Printf("Verifying checksum for %s", downloadFilename)
		if err := verifyChecksum(destPath, checksum); err != nil {
			updateStatus.ErrorMessage = "Checksum verification failed"
			// Delete corrupted file
			os.Remove(destPath)
			return "", fmt.Errorf("checksum verification failed: %w", err)
		}
		log.Printf("Checksum verified successfully")
	} else {
		log.Printf("Warning: No checksum available for %s", downloadFilename)
	}

	updateStatus.InstallerPath = destPath
	updateStatus.Ready = true
	updateStatus.DownloadProgress = 100
	log.Printf("Update downloaded successfully to: %s", destPath)

	return destPath, nil
}

// copyFileWithProgress copies a file and emits progress events
func (a *App) copyFileWithProgress(src, dst string) error {
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	// Get file size
	stat, err := sourceFile.Stat()
	if err != nil {
		return err
	}
	totalSize := stat.Size()

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

	// Copy with progress tracking
	buffer := make([]byte, 1024*1024) // 1MB buffer
	var copiedSize int64 = 0

	for {
		n, err := sourceFile.Read(buffer)
		if n > 0 {
			if _, writeErr := destFile.Write(buffer[:n]); writeErr != nil {
				return writeErr
			}
			copiedSize += int64(n)

			// Update progress (avoid too many events)
			progress := int((copiedSize * 100) / totalSize)
			if progress != updateStatus.DownloadProgress {
				updateStatus.DownloadProgress = progress
				runtime.EventsEmit(a.ctx, "update:status", updateStatus)
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
	}

	return nil
}

// =============================================================================
// Install Methods
// =============================================================================

// InstallUpdate launches the downloaded installer with elevated privileges
// and optionally quits the application.
//
// Parameters:
//   - quitAfterLaunch: If true, exits EMLy after launching the installer
//
// Returns:
//   - error: Error if installer launch fails
func (a *App) InstallUpdate(quitAfterLaunch bool) error {
	if !updateStatus.Ready || updateStatus.InstallerPath == "" {
		return fmt.Errorf("no installer ready to install")
	}

	installerPath := updateStatus.InstallerPath

	// Verify installer exists
	if _, err := os.Stat(installerPath); os.IsNotExist(err) {
		updateStatus.ErrorMessage = "Installer file not found"
		updateStatus.Ready = false
		return fmt.Errorf("installer not found: %s", installerPath)
	}

	log.Printf("Launching installer: %s", installerPath)

	// Launch installer with UAC elevation using ShellExecute
	if err := shellExecuteAsAdmin(installerPath); err != nil {
		updateStatus.ErrorMessage = fmt.Sprintf("Failed to launch installer: %v", err)
		return fmt.Errorf("failed to launch installer: %w", err)
	}

	log.Printf("Installer launched successfully")

	// Quit application if requested
	if quitAfterLaunch {
		time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts
		runtime.Quit(a.ctx)
	}

	return nil
}

// shellExecuteAsAdmin launches an executable with UAC elevation on Windows
func shellExecuteAsAdmin(exePath string) error {
	verb := "runas" // Triggers UAC elevation
	exe := syscall.StringToUTF16Ptr(exePath)
	verbPtr := syscall.StringToUTF16Ptr(verb)

	var hwnd uintptr = 0
	var operation = verbPtr
	var file = exe
	var parameters uintptr = 0
	var directory uintptr = 0
	var showCmd int32 = 1 // SW_SHOWNORMAL

	// Load shell32.dll
	shell32 := syscall.NewLazyDLL("shell32.dll")
	shellExecute := shell32.NewProc("ShellExecuteW")

	ret, _, err := shellExecute.Call(
		hwnd,
		uintptr(unsafe.Pointer(operation)),
		uintptr(unsafe.Pointer(file)),
		parameters,
		directory,
		uintptr(showCmd),
	)

	// ShellExecuteW returns a value > 32 on success
	if ret <= 32 {
		return fmt.Errorf("ShellExecuteW failed with code %d: %v", ret, err)
	}

	return nil
}

// launchDetachedInstaller launches the installer as a completely detached process
// using CreateProcess with DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP flags.
// This allows the installer to continue running and close EMLy without errors.
//
// Parameters:
//   - exePath: Full path to the installer executable
//   - args: Array of command-line arguments to pass to the installer
//
// Returns:
//   - error: Error if process creation fails
func launchDetachedInstaller(exePath string, args []string) error {
	// Build command line: executable path + arguments
	cmdLine := fmt.Sprintf(`"%s"`, exePath)
	if len(args) > 0 {
		cmdLine += " " + strings.Join(args, " ")
	}

	log.Printf("Launching detached installer: %s", cmdLine)

	// Convert to UTF16 for Windows API
	cmdLinePtr := syscall.StringToUTF16Ptr(cmdLine)

	// Setup process startup info
	var si syscall.StartupInfo
	var pi syscall.ProcessInformation

	si.Cb = uint32(unsafe.Sizeof(si))
	si.Flags = syscall.STARTF_USESHOWWINDOW
	si.ShowWindow = syscall.SW_HIDE // Hide installer window (silent mode)

	// Process creation flags:
	// CREATE_NEW_PROCESS_GROUP: Creates process in new process group
	// DETACHED_PROCESS: Process has no console, completely detached from parent
	const (
		CREATE_NEW_PROCESS_GROUP = 0x00000200
		DETACHED_PROCESS         = 0x00000008
	)
	flags := uint32(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)

	// Create the detached process
	err := syscall.CreateProcess(
		nil,        // Application name (nil = use command line)
		cmdLinePtr, // Command line
		nil,        // Process security attributes
		nil,        // Thread security attributes
		false,      // Inherit handles
		flags,      // Creation flags
		nil,        // Environment (nil = inherit)
		nil,        // Current directory (nil = inherit)
		&si,        // Startup info
		&pi,        // Process information (output)
	)

	if err != nil {
		log.Printf("CreateProcess failed: %v", err)
		return fmt.Errorf("failed to create detached process: %w", err)
	}

	// Close process and thread handles immediately
	// We don't need to wait for the process - it's fully detached
	syscall.CloseHandle(pi.Process)
	syscall.CloseHandle(pi.Thread)

	log.Printf("Detached installer process launched successfully (PID: %d)", pi.ProcessId)

	return nil
}

// InstallUpdateSilent downloads the update (if needed) and launches the installer
// in completely silent mode with a detached process. The installer will run with
// these arguments: /VERYSILENT /ALLUSERS /SUPPRESSMSGBOXES /NORESTART /FORCEUPGRADE
//
// This method automatically quits EMLy after launching the installer, allowing the
// installer to close the application and complete the upgrade without user interaction.
//
// Returns:
//   - error: Error if download or launch fails
func (a *App) InstallUpdateSilent() error {
	log.Println("Starting silent update installation...")

	// If installer not ready, attempt to download first
	if !updateStatus.Ready || updateStatus.InstallerPath == "" {
		log.Println("Installer not ready, downloading update first...")

		_, err := a.DownloadUpdate()
		if err != nil {
			errMsg := fmt.Sprintf("Failed to download update: %v", err)
			log.Println(errMsg)
			updateStatus.ErrorMessage = errMsg
			return fmt.Errorf("download failed: %w", err)
		}

		// Wait briefly for download to complete
		log.Println("Download initiated, waiting for completion...")
		for i := 0; i < 60; i++ { // Wait up to 60 seconds
			time.Sleep(1 * time.Second)
			if updateStatus.Ready {
				break
			}
			if updateStatus.ErrorMessage != "" {
				return fmt.Errorf("download error: %s", updateStatus.ErrorMessage)
			}
		}

		if !updateStatus.Ready {
			return fmt.Errorf("download timeout - update not ready after 60 seconds")
		}
	}

	installerPath := updateStatus.InstallerPath

	// Verify installer exists
	if _, err := os.Stat(installerPath); os.IsNotExist(err) {
		updateStatus.ErrorMessage = "Installer file not found"
		updateStatus.Ready = false
		log.Printf("Installer not found: %s", installerPath)
		return fmt.Errorf("installer not found: %s", installerPath)
	}

	log.Printf("Installer ready at: %s", installerPath)

	// Prepare silent installation arguments
	args := []string{
		"/VERYSILENT",           // No UI, completely silent
		"/ALLUSERS",             // Install for all users (requires admin)
		"/SUPPRESSMSGBOXES",     // Suppress all message boxes
		"/NORESTART",            // Don't restart system
		"/FORCEUPGRADE",         // Skip upgrade confirmation dialog
		`/LOG="C:\install.log"`, // Create installation log
	}

	log.Printf("Launching installer with args: %v", args)

	// Launch detached installer
	if err := launchDetachedInstaller(installerPath, args); err != nil {
		errMsg := fmt.Sprintf("Failed to launch installer: %v", err)
		log.Println(errMsg)
		updateStatus.ErrorMessage = errMsg
		return fmt.Errorf("failed to launch installer: %w", err)
	}

	log.Println("Detached installer launched successfully, quitting EMLy...")

	// Quit application to allow installer to replace files
	time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts
	runtime.Quit(a.ctx)

	return nil
}

// InstallUpdateSilentFromPath downloads an installer from a custom SMB/network path
// and launches it in silent mode with a detached process. Use this when you know the
// exact installer path (e.g., \\server\updates\EMLy_Installer.exe) without needing
// to check the version.json manifest.
//
// Parameters:
//   - smbPath: Full UNC path or local path to the installer (e.g., \\server\share\EMLy.exe)
//
// Returns:
//   - error: Error if download or launch fails
func (a *App) InstallUpdateSilentFromPath(smbPath string) error {
	log.Printf("Starting silent installation from custom path: %s", smbPath)

	// Verify source installer exists and is accessible
	if _, err := os.Stat(smbPath); os.IsNotExist(err) {
		errMsg := fmt.Sprintf("Installer not found at: %s", smbPath)
		log.Println(errMsg)
		return fmt.Errorf("%s", errMsg)
	}

	// Create temporary directory for installer
	tempDir := os.TempDir()
	installerFilename := filepath.Base(smbPath)
	tempInstallerPath := filepath.Join(tempDir, installerFilename)

	log.Printf("Copying installer to temp location: %s", tempInstallerPath)

	// Copy installer from SMB path to local temp
	sourceFile, err := os.Open(smbPath)
	if err != nil {
		errMsg := fmt.Sprintf("Failed to open source installer: %v", err)
		log.Println(errMsg)
		return fmt.Errorf("failed to open installer: %w", err)
	}
	defer sourceFile.Close()

	destFile, err := os.Create(tempInstallerPath)
	if err != nil {
		errMsg := fmt.Sprintf("Failed to create temp installer: %v", err)
		log.Println(errMsg)
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	defer destFile.Close()

	// Copy file
	bytesWritten, err := io.Copy(destFile, sourceFile)
	if err != nil {
		errMsg := fmt.Sprintf("Failed to copy installer: %v", err)
		log.Println(errMsg)
		return fmt.Errorf("failed to copy installer: %w", err)
	}

	log.Printf("Installer copied successfully (%d bytes)", bytesWritten)

	// Prepare silent installation arguments
	args := []string{
		"/VERYSILENT",           // No UI, completely silent
		"/ALLUSERS",             // Install for all users (requires admin)
		"/SUPPRESSMSGBOXES",     // Suppress all message boxes
		"/NORESTART",            // Don't restart system
		"/FORCEUPGRADE",         // Skip upgrade confirmation dialog
		`/LOG="C:\install.log"`, // Create installation log
	}

	log.Printf("Launching installer with args: %v", args)

	// Launch detached installer
	if err := launchDetachedInstaller(tempInstallerPath, args); err != nil {
		errMsg := fmt.Sprintf("Failed to launch installer: %v", err)
		log.Println(errMsg)
		return fmt.Errorf("failed to launch installer: %w", err)
	}

	log.Println("Detached installer launched successfully, quitting EMLy...")

	// Quit application to allow installer to replace files
	time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts
	runtime.Quit(a.ctx)

	return nil
}

// =============================================================================
// Status Methods
// =============================================================================

// GetUpdateStatus returns the current update system status.
// This can be polled by the frontend to update UI state.
//
// Returns:
//   - UpdateStatus: Current state of the update system
func (a *App) GetUpdateStatus() UpdateStatus {
	return updateStatus
}

// =============================================================================
// Utility Functions
// =============================================================================

// resolveUpdatePath resolves a network share path or file:// URL to a local path.
// Handles UNC paths (\\server\share), file:// URLs, and local paths.
func resolveUpdatePath(basePath, filename string) (string, error) {
	basePath = strings.TrimSpace(basePath)

	// Handle file:// URL
	if strings.HasPrefix(strings.ToLower(basePath), "file://") {
		u, err := url.Parse(basePath)
		if err != nil {
			return "", fmt.Errorf("invalid file URL: %w", err)
		}
		// Convert file URL to local path
		basePath = filepath.FromSlash(u.Path)
		// Handle Windows drive letters (file:///C:/path -> C:/path)
		if len(basePath) > 0 && basePath[0] == '/' && len(basePath) > 2 && basePath[2] == ':' {
			basePath = basePath[1:]
		}
	}

	// Join with filename
	fullPath := filepath.Join(basePath, filename)

	// Verify path is accessible
	if _, err := os.Stat(fullPath); err != nil {
		return "", fmt.Errorf("path not accessible: %w", err)
	}

	return fullPath, nil
}

// compareSemanticVersions compares two semantic version strings.
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
func compareSemanticVersions(v1, v2 string) int {
	// Strip beta/alpha suffixes for comparison
	v1Clean := strings.Split(v1, "-")[0]
	v2Clean := strings.Split(v2, "-")[0]

	parts1 := strings.Split(v1Clean, ".")
	parts2 := strings.Split(v2Clean, ".")

	// Compare each version component
	maxLen := len(parts1)
	if len(parts2) > maxLen {
		maxLen = len(parts2)
	}

	for i := 0; i < maxLen; i++ {
		var num1, num2 int

		if i < len(parts1) {
			num1, _ = strconv.Atoi(parts1[i])
		}
		if i < len(parts2) {
			num2, _ = strconv.Atoi(parts2[i])
		}

		if num1 < num2 {
			return -1
		}
		if num1 > num2 {
			return 1
		}
	}

	// If base versions are equal, check beta/stable
	if v1 != v2 {
		// Version with beta suffix is considered "older" than without
		if strings.Contains(v1, "-beta") && !strings.Contains(v2, "-beta") {
			return -1
		}
		if !strings.Contains(v1, "-beta") && strings.Contains(v2, "-beta") {
			return 1
		}
	}

	return 0
}

// verifyChecksum verifies the SHA256 checksum of a file
func verifyChecksum(filePath, expectedChecksum string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	hash := sha256.New()
	if _, err := io.Copy(hash, file); err != nil {
		return err
	}

	actualChecksum := hex.EncodeToString(hash.Sum(nil))

	if !strings.EqualFold(actualChecksum, expectedChecksum) {
		return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
	}

	return nil
}
