package ziputil

import (
	"archive/zip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
)

// DownloadFile downloads a file from URL with retry logic
func DownloadFile(url, destPath string, logger *logrus.Logger) error {
	logger.WithFields(logrus.Fields{
		"url":  url,
		"dest": destPath,
	}).Debug("Downloading file")

	// Create destination directory if needed
	destDir := filepath.Dir(destPath)
	if err := os.MkdirAll(destDir, 0755); err != nil {
		return fmt.Errorf("failed to create destination directory: %w", err)
	}

	// Retry logic: 3 attempts with exponential backoff
	var lastErr error
	for attempt := 1; attempt <= 3; attempt++ {
		if attempt > 1 {
			backoff := time.Duration(attempt*attempt) * time.Second
			logger.WithFields(logrus.Fields{
				"attempt": attempt,
				"backoff": backoff,
			}).Debug("Retrying download after backoff")
			time.Sleep(backoff)
		}

		// Download file
		resp, err := http.Get(url)
		if err != nil {
			lastErr = fmt.Errorf("download request failed: %w", err)
			continue
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			lastErr = fmt.Errorf("download failed with status: %d", resp.StatusCode)
			continue
		}

		// Create output file
		outFile, err := os.Create(destPath)
		if err != nil {
			lastErr = fmt.Errorf("failed to create file: %w", err)
			continue
		}
		defer outFile.Close()

		// Copy content
		_, err = io.Copy(outFile, resp.Body)
		if err != nil {
			lastErr = fmt.Errorf("failed to write file: %w", err)
			continue
		}

		logger.WithFields(logrus.Fields{
			"url":     url,
			"dest":    destPath,
			"attempt": attempt,
		}).Info("File downloaded successfully")
		return nil
	}

	return fmt.Errorf("download failed after 3 attempts: %w", lastErr)
}

// ExtractZip extracts a ZIP file to a destination directory with ZipSlip protection
func ExtractZip(zipPath, destDir string, logger *logrus.Logger) error {
	logger.WithFields(logrus.Fields{
		"zip":  zipPath,
		"dest": destDir,
	}).Debug("Extracting ZIP file")

	// Open ZIP file
	reader, err := zip.OpenReader(zipPath)
	if err != nil {
		return fmt.Errorf("failed to open ZIP file: %w", err)
	}
	defer reader.Close()

	// Create destination directory
	if err := os.MkdirAll(destDir, 0755); err != nil {
		return fmt.Errorf("failed to create destination directory: %w", err)
	}

	// Extract each file
	for _, file := range reader.File {
		if err := extractZipFile(file, destDir, logger); err != nil {
			return fmt.Errorf("failed to extract %s: %w", file.Name, err)
		}
	}

	logger.WithFields(logrus.Fields{
		"zip":   zipPath,
		"dest":  destDir,
		"files": len(reader.File),
	}).Info("ZIP extracted successfully")
	return nil
}

// extractZipFile extracts a single file from ZIP with ZipSlip protection
func extractZipFile(file *zip.File, destDir string, logger *logrus.Logger) error {
	// ZipSlip protection: validate path doesn't escape destDir
	filePath := filepath.Join(destDir, file.Name)
	if !strings.HasPrefix(filePath, filepath.Clean(destDir)+string(os.PathSeparator)) {
		return fmt.Errorf("illegal file path (ZipSlip protection): %s", file.Name)
	}

	// Create directory for file if needed
	if file.FileInfo().IsDir() {
		return os.MkdirAll(filePath, file.Mode())
	}

	// Create parent directories
	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
		return err
	}

	// Open source file in ZIP
	srcFile, err := file.Open()
	if err != nil {
		return err
	}
	defer srcFile.Close()

	// Create destination file
	destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
	if err != nil {
		return err
	}
	defer destFile.Close()

	// Copy contents
	if _, err := io.Copy(destFile, srcFile); err != nil {
		return err
	}

	return nil
}

// Cleanup removes files and directories
func Cleanup(logger *logrus.Logger, paths ...string) {
	for _, path := range paths {
		if path == "" {
			continue
		}

		logger.WithField("path", path).Debug("Cleaning up")
		if err := os.RemoveAll(path); err != nil {
			logger.WithError(err).WithField("path", path).Warn("Failed to cleanup")
		}
	}
}

// ValidateZipStructure checks if ZIP contains required directories
func ValidateZipStructure(zipPath string, requiredDirs []string, logger *logrus.Logger) error {
	reader, err := zip.OpenReader(zipPath)
	if err != nil {
		return fmt.Errorf("failed to open ZIP: %w", err)
	}
	defer reader.Close()

	// Build map of directories in ZIP
	foundDirs := make(map[string]bool)
	for _, file := range reader.File {
		// Extract top-level directory
		parts := strings.Split(file.Name, "/")
		if len(parts) > 0 && parts[0] != "" {
			foundDirs[parts[0]] = true
		}
	}

	// Check for required directories
	missing := []string{}
	for _, required := range requiredDirs {
		if !foundDirs[required] {
			missing = append(missing, required)
		}
	}

	if len(missing) > 0 {
		return fmt.Errorf("ZIP missing required directories: %v", missing)
	}

	logger.WithField("required_dirs", requiredDirs).Debug("ZIP structure validated")
	return nil
}
