package spm

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"time"
)

// DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
	// Parse the APPINDEX
	entries, err := ParseAppIndex(appIndexPath)
	if err != nil {
		return fmt.Errorf("failed to parse APPINDEX: %w", err)
	}

	// Find the right entry
	var selected *AppIndexEntry
	for _, e := range entries {
		if e.Name == packageName &&
			e.Release == release &&
			e.Type == pkgType &&
			e.OS == runtime.GOOS &&
			e.Arch == runtime.GOARCH {
			selected = &e
			break
		}
	}

	// Handle no matching entry
	if selected == nil {
		return fmt.Errorf("package not found in APPINDEX: %s (release: %s, type: %s, os: %s, arch: %s)", packageName, release, pkgType, runtime.GOOS, runtime.GOARCH)
	}

	// Check if the package is already installed and up-to-date
	installDir, err := GetInstallDir()
	if err != nil {
		return fmt.Errorf("failed to get install directory: %w", err)
	}
	needsUpdate, err := IsUpdateNeeded(installDir, packageName, release, selected.Version, selected.Arch, selected.OS)
	if err != nil {
		return fmt.Errorf("failed to check update status: %w", err)
	}
	if !needsUpdate {
		UpdateProgress(0, "Already up-to-date, skipping download.")
		return nil // Skip download
	}

	// Download the package
	UpdateProgress(0, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type))
	resp, err := http.Get(selected.DownloadURL)
	if err != nil {
		return fmt.Errorf("failed to download package: %w", err)
	}
	defer resp.Body.Close()

	// Save the downloaded file
	downloadedFileName := filepath.Base(selected.DownloadURL)
	downloadedFilePath := filepath.Join(destDir, downloadedFileName)

	out, err := os.OpenFile(downloadedFilePath, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return fmt.Errorf("failed to create output file: %w", err)
	}
	defer out.Close()

	totalSize := resp.ContentLength
	var downloaded int64
	buf := make([]byte, 32*1024) // Use a larger buffer for efficiency

	for {
		n, errRead := resp.Body.Read(buf)
		if n > 0 {
			downloaded += int64(n)
			percentage := int(float64(downloaded) / float64(totalSize) * 100)
			UpdateProgress(percentage, fmt.Sprintf("Downloading %s %s (%s)...", packageName, selected.Version, selected.Type))
			if _, errWrite := out.Write(buf[:n]); errWrite != nil {
				return fmt.Errorf("failed to write to output file: %w", errWrite)
			}
		}
		if errRead == io.EOF {
			break
		}
		if errRead != nil {
			return fmt.Errorf("error while reading response: %w", errRead)
		}
	}

	// Ensure the file handle is closed before renaming
	out.Close()

	// Construct the expected filename
	expectedFileName := fmt.Sprintf("%s@%s@%s@%s@%s@%s.tar.gz",
		packageName, selected.Arch, selected.OS, selected.Type, selected.Release, selected.Version)

	expectedFilePath := filepath.Join(destDir, expectedFileName)

	// I dont know why is this happening, I dont want to know but sometimes some process is helding up the donwloaded files so thats why it retries here
	maxRetries := 5
	for i := 0; i < maxRetries; i++ {
		err = os.Rename(downloadedFilePath, expectedFilePath)
		if err == nil {
			break
		}

		// Check if file is in use
		f, checkErr := os.Open(downloadedFilePath)
		if checkErr != nil {
			return fmt.Errorf("file is locked by another process: %w", checkErr)
		}
		f.Close()

		if i < maxRetries-1 {
			time.Sleep(500 * time.Millisecond) // Wait before retrying
		}
	}

	if err != nil {
		return fmt.Errorf("failed to rename downloaded file after retries: %w", err)
	}

	UpdateProgress(100, fmt.Sprintf("Downloaded %s %s (%s).", packageName, selected.Version, selected.Type))
	return nil
}