package spm

import (
	"archive/tar"
	"bufio"
	"compress/gzip"
	"fmt"
	"io"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"sync"
	"time"
)

func DecompressToTemp(filePath string) (string, error) {
	UpdateProgress(0, "Decompressing package")

	// 1) Base temp dir
	baseTempDir := GetTempDir()

	// 2) Create a unique subfolder inside the base temp dir
	subfolderName := fmt.Sprintf("spm_decompress_%d", rand.Intn(1000000))
	decompressDir := filepath.Join(baseTempDir, subfolderName)
	if err := os.MkdirAll(decompressDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create decompress dir: %w", err)
	}

	// 3) Open the tar.gz file
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	gzr, err := gzip.NewReader(file)
	if err != nil {
		return "", err
	}
	defer gzr.Close()

	tarReader := tar.NewReader(gzr)

	// 4) Count total files
	var totalFiles, processedFiles int
	for {
		_, err := tarReader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return "", err
		}
		totalFiles++
	}

	// 5) Reset file position and tar reader
	if _, err := file.Seek(0, io.SeekStart); err != nil {
		return "", err
	}
	if err := gzr.Reset(file); err != nil {
		return "", err
	}
	tarReader = tar.NewReader(gzr)

	// 6) Extract into `decompressDir`
	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return "", err
		}

		targetPath := filepath.Join(decompressDir, header.Name)
		switch header.Typeflag {
		case tar.TypeDir:
			if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
				return "", err
			}
		case tar.TypeReg:
			outFile, err := os.Create(targetPath)
			if err != nil {
				return "", err
			}
			if _, err := io.Copy(outFile, tarReader); err != nil {
				outFile.Close()
				return "", err
			}
			outFile.Close()
		}

		processedFiles++
		UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
	}

	UpdateProgress(100, "Package decompressed")
	return decompressDir, nil
}

// tailLogFile continuously reads new lines from logFile until done is closed.
// It prints each new line and searches for a percentage (e.g. " 47%") to call UpdateProgress.
func tailLogFile(logFile string, done <-chan struct{}, wg *sync.WaitGroup) {
	defer wg.Done()

	var offset int64
	re := regexp.MustCompile(`\s+(\d+)%`)
	for {
		select {
		case <-done:
			return
		default:
			f, err := os.Open(logFile)
			if err != nil {
				time.Sleep(200 * time.Millisecond)
				continue
			}
			// Seek to the last known offset.
			f.Seek(offset, io.SeekStart)
			scanner := bufio.NewScanner(f)
			for scanner.Scan() {
				line := scanner.Text()
				fmt.Printf("[ROBOPROGRESS-LOG] %s\n", line)
				// Look for a percentage.
				if matches := re.FindStringSubmatch(line); len(matches) == 2 {
					var percent int
					_, err := fmt.Sscanf(matches[1], "%d", &percent)
					if err == nil {
						fmt.Printf("[ROBOPROGRESS] Parsed progress: %d%%\n", percent)
						UpdateProgress(percent, "Copying files to install directory")
					}
				}
			}
			// Update the offset.
			newOffset, _ := f.Seek(0, io.SeekCurrent)
			offset = newOffset
			f.Close()
			time.Sleep(200 * time.Millisecond)
		}
	}
}

// MoveFilesToInstallDir copies files from tempDir to installDir.
// On Windows it uses robocopy with logging so that as much output as possible is printed,
// and a separate goroutine tails the log file to update progress.
// The log file is saved (not automatically deleted) so you can inspect it.
func MoveFilesToInstallDir(tempDir, installDir, pkgType string) error {
	// Ensure tempDir exists.
	if _, err := os.Stat(tempDir); os.IsNotExist(err) {
		return fmt.Errorf("tempDir does not exist: %s", tempDir)
	}

	// If package type is "browser", adjust installDir.
	if pkgType == "browser" {
		installDir = filepath.Join(installDir, "browser")
	}

	// Ensure destination exists.
	if err := os.MkdirAll(installDir, os.ModePerm); err != nil {
		return fmt.Errorf("failed to create installDir: %w", err)
	}

	if runtime.GOOS == "windows" {
		// Create a temporary log file.
		logFile := filepath.Join(os.TempDir(), fmt.Sprintf("robocopy_%d.log", time.Now().UnixNano()))
		// Print out the log file path so you can locate it manually.
		fmt.Printf("[INFO] Robocopy log file: %s\n", logFile)

		// Build robocopy command.
		// /E: copy subdirectories (including empty ones)
		// /TEE: output to console as well as to the log file.
		// /LOG:<logFile>: write output to the log file.
		// We remove extra suppression flags so that robocopy prints as much as possible.
		cmd := exec.Command("robocopy", tempDir, installDir, "/E", "/TEE", fmt.Sprintf("/LOG:%s", logFile))

		// Start robocopy.
		if err := cmd.Start(); err != nil {
			return fmt.Errorf("failed to start robocopy: %w", err)
		}

		// Set up a goroutine to tail the log file.
		doneTail := make(chan struct{})
		var wg sync.WaitGroup
		wg.Add(1)
		go tailLogFile(logFile, doneTail, &wg)

		// Wait for robocopy to complete.
		err := cmd.Wait()
		// Signal the tail goroutine to stop.
		close(doneTail)
		wg.Wait()

		// Robocopy returns exit codes less than 8 as success.
		if err != nil {
			if exitErr, ok := err.(*exec.ExitError); ok {
				if exitCode := exitErr.ExitCode(); exitCode >= 8 {
					return fmt.Errorf("robocopy failed: exit status %d", exitCode)
				}
			} else {
				return fmt.Errorf("robocopy failed: %w", err)
			}
		}
		// Mark progress as complete.
		UpdateProgress(100, "Copying files to install directory")

		// (Optional) If you want the log file to be removed automatically, uncomment the next line.
		// os.Remove(logFile)
	} else {
		// Non-Windows fallback: copy files one-by-one.
		var totalFiles, copiedFiles int
		err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if !info.IsDir() {
				totalFiles++
			}
			return nil
		})
		if err != nil {
			return err
		}

		err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}

			relPath, err := filepath.Rel(tempDir, path)
			if err != nil {
				return err
			}

			targetPath := filepath.Join(installDir, relPath)
			if info.IsDir() {
				if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
					return err
				}
			} else {
				if err := copyFile(path, targetPath); err != nil {
					return err
				}
				copiedFiles++
				UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
			}
			return nil
		})
		if err != nil {
			return err
		}
	}

	// Clean up temporary directory.
	UpdateProgress(100, "Cleaning up temporary files")
	return os.RemoveAll(tempDir)
}

// copyFile copies the contents of the source file to the destination file.
func copyFile(src, dst string) error {
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	// Create the destination file
	destinationFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destinationFile.Close()

	// Copy the file content
	if _, err := io.Copy(destinationFile, sourceFile); err != nil {
		return err
	}

	// Preserve file permissions
	info, err := sourceFile.Stat()
	if err != nil {
		return err
	}
	return os.Chmod(dst, info.Mode())
}

// FinalizeInstall finalizes the installation by updating installed.ini.
func FinalizeInstall(packageName, release, version, arch, osName string) error {
	installDir, err := GetInstallDir()
	if err != nil {
		return err
	}
	pkgInfo := AppIndexEntry{
		Name:    packageName,
		Version: version,
		Release: release,
		Arch:    arch,
		OS:      osName,
	}
	return UpdateInstalledPackage(installDir, pkgInfo)
}