package backup

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"time"

	"git.metaunix.net/bitgoblin/goblin-store/lib/config"
)

// RunRsyncIncremental executes a local hardlink-based incremental backup.
func RunRsyncIncremental(ctx context.Context, cfg *config.Config) error {
	// Verify that the rsync binary exists in PATH
	if _, err := exec.LookPath("rsync"); err != nil {
		return fmt.Errorf("rsync binary not found in PATH: %w", err)
	}

	// 1. Ensure the base destination directory exists
	if err := os.MkdirAll(cfg.Destination, 0755); err != nil {
		return fmt.Errorf("failed to create destination directory %q: %w", cfg.Destination, err)
	}

	// 2. Identify the most recent snapshot for link-dest deduplication
	latestSnapshot, err := findLatestSnapshot(cfg.Destination)
	if err != nil {
		return fmt.Errorf("failed scanning for previous snapshots: %w", err)
	}

	timestamp := time.Now().Format("2006-01-02_15-04-05")
	currentBackupDir := filepath.Join(cfg.Destination, timestamp)

	// Ensure the source ends with a trailing slash so rsync copies directory
	// contents directly rather than nesting the parent folder
	src := filepath.Clean(cfg.Source) + string(filepath.Separator)

	// 3. Assemble rsync arguments
	args := []string{
		"-a",       // Archive mode: preserves permissions, symlinks, timestamps, etc.
		"--delete", // Delete files in dest that no longer exist in source
	}

	if cfg.Verbose {
		args = append(args, "-v")
	}

	if latestSnapshot != "" {
		// Use absolute path for --link-dest to prevent path resolution issues
		absLinkDest, err := filepath.Abs(latestSnapshot)
		if err != nil {
			return fmt.Errorf("failed resolving link-dest path: %w", err)
		}
		args = append(args, fmt.Sprintf("--link-dest=%s", absLinkDest))

		if cfg.Verbose {
			fmt.Printf("Linking unchanged files against baseline: %s\n", absLinkDest)
		}
	} else if cfg.Verbose {
		fmt.Println("No previous snapshots found. Generating initial full baseline...")
	}

	args = append(args, src, currentBackupDir)

	if cfg.Verbose {
		fmt.Printf("Executing: rsync %s\n", args)
	}

	// 4. Run the rsync command with context cancellation
	cmd := exec.CommandContext(ctx, "rsync", args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("rsync execution failed: %w", err)
	}

	fmt.Printf("Successfully created snapshot at: %s\n", currentBackupDir)

	// 5. Enforce retention policy
	if cfg.KeepIncrementals > 0 {
		if err := pruneSnapshots(cfg.Destination, cfg.KeepIncrementals, cfg.Verbose); err != nil {
			return fmt.Errorf("retention pruning failed: %w", err)
		}
	}

	return nil
}

// findLatestSnapshot scans the destination directory and returns the path
// to the newest directory matching the timestamp pattern.
func findLatestSnapshot(destRoot string) (string, error) {
	entries, err := os.ReadDir(destRoot)
	if err != nil {
		return "", err
	}

	var snapshots []string
	for _, entry := range entries {
		if entry.IsDir() {
			snapshots = append(snapshots, entry.Name())
		}
	}

	if len(snapshots) == 0 {
		return "", nil
	}

	// Lexicographical sort works chronologically because of "YYYY-MM-DD_HH-MM-SS" format
	sort.Strings(snapshots)
	return filepath.Join(destRoot, snapshots[len(snapshots)-1]), nil
}

// pruneSnapshots deletes the oldest snapshots when the total count exceeds the retention limit.
func pruneSnapshots(destRoot string, keep int, verbose bool) error {
	entries, err := os.ReadDir(destRoot)
	if err != nil {
		return err
	}

	var snapshots []string
	for _, entry := range entries {
		if entry.IsDir() {
			snapshots = append(snapshots, entry.Name())
		}
	}

	sort.Strings(snapshots)

	if len(snapshots) <= keep {
		return nil
	}

	excessCount := len(snapshots) - keep
	toDelete := snapshots[:excessCount]

	if verbose {
		fmt.Printf("Pruning %d snapshot(s) exceeding retention limit (%d)...\n", excessCount, keep)
	}

	for _, dirName := range toDelete {
		targetPath := filepath.Join(destRoot, dirName)
		if verbose {
			fmt.Printf("Deleting expired snapshot: %s\n", targetPath)
		}

		// os.RemoveAll removes directory entries and unlinks hardlinked inodes safely
		if err := os.RemoveAll(targetPath); err != nil {
			return fmt.Errorf("failed to remove snapshot %s: %w", targetPath, err)
		}
	}

	return nil
}
