package utils

import (
	"encoding/json"
	"epochsilicon/pkg/log"
	"errors"
	"fmt"
	"github.com/Masterminds/semver/v3"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/dialog"
)

// min returns the smaller of two integers
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// PathExists checks if a path exists.
func PathExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

// DirExists checks if a path exists and is a directory.
func DirExists(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}
	return info.IsDir()
}

// CopyFile copies a single file from src to dst.
func CopyFile(src, dst string) error {
	sourceFileStat, err := os.Stat(src)
	if err != nil {
		return err
	}

	if !sourceFileStat.Mode().IsRegular() {
		return fmt.Errorf("%s is not a regular file", src)
	}

	source, err := os.Open(src)
	if err != nil {
		return err
	}
	defer source.Close()

	destination, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destination.Close()
	_, err = io.Copy(destination, source)
	return err
}

// CopyDir copies a directory recursively from src to dst.
func CopyDir(src string, dst string) error {
	srcInfo, err := os.Stat(src)
	if err != nil {
		return err
	}

	err = os.MkdirAll(dst, srcInfo.Mode())
	if err != nil {
		return err
	}

	dir, err := os.ReadDir(src)
	if err != nil {
		return err
	}

	for _, entry := range dir {
		srcPath := filepath.Join(src, entry.Name())
		dstPath := filepath.Join(dst, entry.Name())

		if entry.IsDir() {
			err = CopyDir(srcPath, dstPath)
			if err != nil {
				return err
			}
		} else {
			err = CopyFile(srcPath, dstPath)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// RunOsascript runs an AppleScript command using osascript.
func RunOsascript(scriptString string, myWindow fyne.Window) bool {
	log.Debugf("Executing AppleScript: %s", scriptString)
	cmd := exec.Command("osascript", "-e", scriptString)
	output, err := cmd.CombinedOutput()
	if err != nil {
		errMsg := fmt.Sprintf("AppleScript failed: %v\nOutput: %s", err, string(output))
		dialog.ShowError(errors.New(errMsg), myWindow)
		log.Debug(errMsg)
		return false
	}
	log.Debugf("osascript output: %s", string(output))
	return true
}

// EscapeStringForAppleScript escapes a string for AppleScript.
func EscapeStringForAppleScript(s string) string {
	s = strings.ReplaceAll(s, "\\", "\\\\")
	s = strings.ReplaceAll(s, "\"", "\\\"")
	return s
}

// QuotePathForShell quotes paths for shell commands.
func QuotePathForShell(path string) string {
	return fmt.Sprintf(`"%s"`, path)
}

// UpdateInfo contains information about the latest release
type UpdateInfo struct {
	TagName string  `json:"tag_name"`
	Body    string  `json:"body"`
	Assets  []Asset `json:"assets"`
}

type Asset struct {
	Name               string `json:"name"`
	BrowserDownloadURL string `json:"browser_download_url"`
	Size               int64  `json:"size"`
}

// CheckForUpdateWithAssets returns update information including download assets
func CheckForUpdateWithAssets(currentVersion *semver.Version) (*UpdateInfo, bool, error) {
	resp, err := http.Get("https://git.burkey.co/api/v1/repos/eburk/epochsilicon/releases/latest")
	if err != nil {
		return nil, false, err
	}
	defer resp.Body.Close()

	// Check for HTTP errors
	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return nil, false, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
	}

	// Read the response body first to check content
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, false, fmt.Errorf("failed to read response body: %v", err)
	}

	// Check if response looks like HTML (rate limiting or other errors)
	bodyStr := string(body)
	if strings.Contains(bodyStr, "<!DOCTYPE html>") || strings.Contains(bodyStr, "<html") {
		return nil, false, fmt.Errorf("GitHub API returned HTML instead of JSON (possible rate limiting): %s", bodyStr[:min(200, len(bodyStr))])
	}

	var updateInfo UpdateInfo
	if err := json.Unmarshal(body, &updateInfo); err != nil {
		return nil, false, fmt.Errorf("failed to parse JSON response: %v. Response: %s", err, bodyStr[:min(200, len(bodyStr))])
	}

	latest, err := semver.NewVersion(updateInfo.TagName)
	if err != nil {
		return nil, false, fmt.Errorf("failed to parse semver: %v", err)
	}
	updateAvailable := latest.GreaterThan(currentVersion)

	return &updateInfo, updateAvailable, nil
}

// DownloadUpdate downloads the latest release and returns the path to the downloaded file
func DownloadUpdate(downloadURL string, progressCallback func(downloaded, total int64)) (string, error) {
	// Create temporary file
	tempFile, err := os.CreateTemp("", "EpochSilicon-update-*.dmg")
	if err != nil {
		return "", fmt.Errorf("failed to create temp file: %v", err)
	}
	defer tempFile.Close()

	// Download the file
	resp, err := http.Get(downloadURL)
	if err != nil {
		os.Remove(tempFile.Name())
		return "", fmt.Errorf("failed to download update: %v", err)
	}
	defer resp.Body.Close()

	totalSize := resp.ContentLength
	var downloaded int64

	// Copy with progress tracking
	buffer := make([]byte, 32*1024) // 32KB buffer
	for {
		n, err := resp.Body.Read(buffer)
		if n > 0 {
			_, writeErr := tempFile.Write(buffer[:n])
			if writeErr != nil {
				os.Remove(tempFile.Name())
				return "", fmt.Errorf("failed to write update file: %v", writeErr)
			}
			downloaded += int64(n)
			if progressCallback != nil {
				progressCallback(downloaded, totalSize)
			}
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			os.Remove(tempFile.Name())
			return "", fmt.Errorf("failed to read update data: %v", err)
		}
	}

	return tempFile.Name(), nil
}

// InstallUpdate installs the downloaded update by mounting the DMG and replacing the current app
func InstallUpdate(dmgPath string) error {
	// Get current app path
	execPath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("failed to get executable path: %v", err)
	}

	// Navigate up to find the .app bundle
	currentAppPath := execPath
	for !strings.HasSuffix(currentAppPath, ".app") && currentAppPath != "/" {
		currentAppPath = filepath.Dir(currentAppPath)
	}

	if !strings.HasSuffix(currentAppPath, ".app") {
		return fmt.Errorf("could not find app bundle path")
	}

	// Mount the DMG and parse the mount point from plist output
	log.Debugf("Mounting DMG: %s", dmgPath)
	mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
	mountOutput, err := mountCmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
	}

	log.Debugf("Mount output: %s", string(mountOutput))

	mountPoint := ""

	// Parse the plist XML output to find mount points
	outputStr := string(mountOutput)

	// Look for mount-point entries in the XML
	// The plist contains mount-point entries that show where volumes are mounted
	lines := strings.Split(outputStr, "\n")
	for i, line := range lines {
		line = strings.TrimSpace(line)
		if strings.Contains(line, "<key>mount-point</key>") && i+1 < len(lines) {
			// The next line should contain the mount point path
			nextLine := strings.TrimSpace(lines[i+1])
			if strings.HasPrefix(nextLine, "<string>/Volumes/") {
				// Extract the path from <string>/Volumes/...</string>
				start := strings.Index(nextLine, "<string>") + 8
				end := strings.Index(nextLine, "</string>")
				if start >= 8 && end > start {
					mountPoint = nextLine[start:end]
					log.Debugf("Found mount point in plist: %s", mountPoint)
					break
				}
			}
		}
	}

	// Fallback: try without -plist flag for simpler output
	if mountPoint == "" {
		log.Debugf("Plist parsing failed, trying simple mount")
		// Unmount first if something was mounted
		exec.Command("hdiutil", "detach", dmgPath, "-force").Run()

		// Try mounting without plist
		simpleMountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse")
		simpleOutput, simpleErr := simpleMountCmd.CombinedOutput()
		if simpleErr != nil {
			return fmt.Errorf("failed to mount DMG (simple): %v, output: %s", simpleErr, string(simpleOutput))
		}

		// Parse simple output
		simpleLines := strings.Split(string(simpleOutput), "\n")
		for _, line := range simpleLines {
			line = strings.TrimSpace(line)
			if strings.Contains(line, "/Volumes/") {
				parts := strings.Fields(line)
				for i := len(parts) - 1; i >= 0; i-- {
					if strings.HasPrefix(parts[i], "/Volumes/") {
						mountPoint = parts[i]
						log.Debugf("Found mount point in simple output: %s", mountPoint)
						break
					}
				}
				if mountPoint != "" {
					break
				}
			}
		}
	}

	if mountPoint == "" {
		return fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
	}

	log.Debugf("Using mount point: %s", mountPoint)

	defer func() {
		// Unmount the DMG
		log.Debugf("Unmounting DMG from: %s", mountPoint)
		unmountCmd := exec.Command("hdiutil", "detach", mountPoint, "-force")
		unmountCmd.Run()
	}()

	// Find the app in the mounted DMG - search for any .app bundle
	var newAppPath string

	// First, try the exact name
	exactPath := filepath.Join(mountPoint, "EpochSilicon.app")
	if PathExists(exactPath) {
		newAppPath = exactPath
	} else {
		// Search for any .app bundle in the mount point
		log.Debugf("EpochSilicon.app not found at exact path, searching for .app bundles")
		entries, err := os.ReadDir(mountPoint)
		if err != nil {
			return fmt.Errorf("failed to read DMG contents: %v", err)
		}

		for _, entry := range entries {
			if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
				candidatePath := filepath.Join(mountPoint, entry.Name())
				log.Debugf("Found .app bundle: %s", candidatePath)
				newAppPath = candidatePath
				break
			}
		}
	}

	if newAppPath == "" {
		return fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
	}

	log.Debugf("Found app to install: %s", newAppPath)

	// Create backup of current app
	backupPath := currentAppPath + ".backup"
	log.Debugf("Creating backup: %s -> %s", currentAppPath, backupPath)

	// Remove old backup if it exists
	if PathExists(backupPath) {
		os.RemoveAll(backupPath)
	}

	if err := CopyDir(currentAppPath, backupPath); err != nil {
		return fmt.Errorf("failed to create backup: %v", err)
	}

	// Remove current app
	log.Debugf("Removing current app: %s", currentAppPath)
	if err := os.RemoveAll(currentAppPath); err != nil {
		return fmt.Errorf("failed to remove current app: %v", err)
	}

	// Copy new app
	log.Debugf("Installing new app: %s -> %s", newAppPath, currentAppPath)
	if err := CopyDir(newAppPath, currentAppPath); err != nil {
		// Try to restore backup on failure
		log.Debugf("Installation failed, restoring backup")
		os.RemoveAll(currentAppPath)
		CopyDir(backupPath, currentAppPath)
		return fmt.Errorf("failed to install new app: %v", err)
	}

	// Fix executable permissions for the main binary
	executablePath := filepath.Join(currentAppPath, "Contents", "MacOS", "epochsilicon")
	if PathExists(executablePath) {
		log.Debugf("Setting executable permissions for: %s", executablePath)
		if err := os.Chmod(executablePath, 0755); err != nil {
			log.Debugf("Warning: failed to set executable permissions: %v", err)
			// Don't fail the entire update for this, but log it
		}
	} else {
		log.Debugf("Warning: executable not found at expected path: %s", executablePath)
	}

	// Remove backup on success
	os.RemoveAll(backupPath)

	log.Debugf("Update installed successfully")
	return nil
}

// TestDMGMount tests DMG mounting and returns mount point and app path for debugging
func TestDMGMount(dmgPath string) (string, string, error) {
	log.Debugf("Testing DMG mount: %s", dmgPath)

	// Mount the DMG with verbose output to better parse mount point
	mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
	mountOutput, err := mountCmd.CombinedOutput()
	if err != nil {
		return "", "", fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
	}

	log.Debugf("Mount output: %s", string(mountOutput))

	// Parse mount output to get mount point
	mountPoint := ""

	// First try: look for /Volumes/ in the output lines
	lines := strings.Split(string(mountOutput), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.Contains(line, "/Volumes/") {
			parts := strings.Fields(line)
			for i := len(parts) - 1; i >= 0; i-- {
				if strings.HasPrefix(parts[i], "/Volumes/") {
					mountPoint = parts[i]
					break
				}
			}
			if mountPoint != "" {
				break
			}
		}
	}

	// Second try: use hdiutil info to get mount points if first method failed
	if mountPoint == "" {
		log.Debugf("First mount point detection failed, trying hdiutil info")
		infoCmd := exec.Command("hdiutil", "info", "-plist")
		infoOutput, infoErr := infoCmd.CombinedOutput()
		if infoErr == nil {
			infoLines := strings.Split(string(infoOutput), "\n")
			for _, line := range infoLines {
				if strings.Contains(line, "/Volumes/") && strings.Contains(line, "EpochSilicon") {
					line = strings.TrimSpace(line)
					if strings.HasPrefix(line, "/Volumes/") {
						mountPoint = line
						break
					}
				}
			}
		}
	}

	if mountPoint == "" {
		return "", "", fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
	}

	log.Debugf("Using mount point: %s", mountPoint)

	// Find the app in the mounted DMG
	var newAppPath string

	// First, try the exact name
	exactPath := filepath.Join(mountPoint, "EpochSilicon.app")
	if PathExists(exactPath) {
		newAppPath = exactPath
	} else {
		// Search for any .app bundle in the mount point
		log.Debugf("EpochSilicon.app not found at exact path, searching for .app bundles")
		entries, err := os.ReadDir(mountPoint)
		if err != nil {
			// Unmount before returning error
			exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
			return "", "", fmt.Errorf("failed to read DMG contents: %v", err)
		}

		for _, entry := range entries {
			if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
				candidatePath := filepath.Join(mountPoint, entry.Name())
				log.Debugf("Found .app bundle: %s", candidatePath)
				newAppPath = candidatePath
				break
			}
		}
	}

	// Unmount after testing
	log.Debugf("Unmounting test DMG from: %s", mountPoint)
	exec.Command("hdiutil", "detach", mountPoint, "-force").Run()

	if newAppPath == "" {
		return mountPoint, "", fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
	}

	return mountPoint, newAppPath, nil
}

// CompareFileWithBundledResource compares the size of a file on disk with a bundled resource
func CompareFileWithBundledResource(filePath, resourceName string) bool {
	// Check if the file exists first
	if !PathExists(filePath) {
		return false
	}

	// Get file info for the existing file
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		log.Debugf("Failed to stat file %s: %v", filePath, err)
		return false
	}

	// Load the bundled resource
	resource, err := fyne.LoadResourceFromPath(resourceName)
	if err != nil {
		log.Debugf("Failed to load bundled resource %s: %v", resourceName, err)
		return false
	}

	// Compare sizes
	fileSize := fileInfo.Size()
	resourceSize := int64(len(resource.Content()))

	log.Debugf("Comparing file sizes - %s: %d bytes vs bundled %s: %d bytes",
		filePath, fileSize, resourceName, resourceSize)

	return fileSize == resourceSize
}

// RestartApp restarts the application
func RestartApp() error {
	execPath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("failed to get executable path: %v", err)
	}

	// Find the .app bundle
	appPath := execPath
	for !strings.HasSuffix(appPath, ".app") && appPath != "/" {
		appPath = filepath.Dir(appPath)
	}

	if strings.HasSuffix(appPath, ".app") {
		// Launch the app bundle
		cmd := exec.Command("open", appPath)
		return cmd.Start()
	} else {
		// Launch the executable directly
		cmd := exec.Command(execPath)
		return cmd.Start()
	}
}
