// Package restore provides functionality for restoring ZFS snapshots from a backup server.
// It supports restoring to ZFS datasets, saving to files, and mounting restored datasets.
package restore

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"sort"
	"strings"
	"time"

	"github.com/mistifyio/go-zfs"
	"github.com/pierrec/lz4/v4"
)

// SnapshotMetadata represents snapshot information from the server.
type SnapshotMetadata struct {
	ClientID     string    `json:"client_id"`
	SnapshotID   string    `json:"snapshot_id"`
	Timestamp    time.Time `json:"timestamp"`
	SizeBytes    int64     `json:"size_bytes"`
	DatasetName  string    `json:"dataset_name"`
	StorageKey   string    `json:"storage_key"`
	StorageType  string    `json:"storage_type"`
	Compressed   bool      `json:"compressed"`
	Incremental  bool      `json:"incremental"`
	BaseSnapshot string    `json:"base_snapshot,omitempty"`
}

// Client handles restore operations from the backup server.
type Client struct {
	ClientID  string
	APIKey    string
	ServerURL string
}

// New creates a new restore client instance.
func New(clientID, apiKey, serverURL string) *Client {
	return &Client{
		ClientID:  clientID,
		APIKey:    apiKey,
		ServerURL: serverURL,
	}
}

// ListSnapshots retrieves all available snapshots from the server.
func (c *Client) ListSnapshots() ([]*SnapshotMetadata, error) {
	url := fmt.Sprintf("%s/status?client_id=%s&api_key=%s",
		c.ServerURL, c.ClientID, c.APIKey)

	resp, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("failed to get snapshots: %v", err)
	}
	defer resp.Body.Close()

	var status struct {
		Success   bool                `json:"success"`
		Snapshots []*SnapshotMetadata `json:"snapshots"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
		return nil, fmt.Errorf("failed to decode response: %v", err)
	}

	if !status.Success {
		return nil, fmt.Errorf("failed to list snapshots")
	}

	return status.Snapshots, nil
}

// DisplaySnapshots prints a formatted list of available snapshots.
// Snapshots are sorted by timestamp (newest first).
func (c *Client) DisplaySnapshots(snapshots []*SnapshotMetadata) {
	if len(snapshots) == 0 {
		fmt.Println("No snapshots available")
		return
	}

	// Sort by timestamp (newest first)
	sort.Slice(snapshots, func(i, j int) bool {
		return snapshots[i].Timestamp.After(snapshots[j].Timestamp)
	})

	fmt.Printf("\n=== Available Snapshots ===\n\n")
	fmt.Printf("%-4s %-25s %-20s %-12s %-10s %s\n",
		"#", "Timestamp", "Dataset", "Size", "Storage", "ID")
	fmt.Println(strings.Repeat("-", 100))

	for i, snap := range snapshots {
		age := time.Since(snap.Timestamp)
		sizeGB := float64(snap.SizeBytes) / (1024 * 1024 * 1024)
		compressed := ""
		if snap.Compressed {
			compressed = " (lz4)"
		}

		fmt.Printf("%-4d %-25s %-20s %7.2f GB   %-10s %s%s\n",
			i+1,
			snap.Timestamp.Format("2006-01-02 15:04:05"),
			truncate(snap.DatasetName, 20),
			sizeGB,
			snap.StorageType,
			truncate(snap.SnapshotID, 30),
			compressed)

		if age < 24*time.Hour {
			fmt.Printf("     (created %s ago)\n", formatDuration(age))
		}
	}

	fmt.Println()
}

// RestoreSnapshot downloads and restores a snapshot to a local ZFS dataset.
// If force is true, existing datasets will be overwritten.
func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset string, force bool) error {
	fmt.Printf("\n=== Restoring Snapshot ===\n")
	fmt.Printf("Source: %s\n", snapshot.SnapshotID)
	fmt.Printf("Target: %s\n", targetDataset)
	fmt.Printf("Size: %.2f GB\n", float64(snapshot.SizeBytes)/(1024*1024*1024))
	fmt.Printf("Storage: %s\n", snapshot.StorageType)
	fmt.Printf("Compressed: %v\n", snapshot.Compressed)
	fmt.Printf("Incremental: %v\n\n", snapshot.Incremental)

	// For incremental snapshots, we need special handling
	if snapshot.Incremental && force {
		// Check if target dataset exists
		if _, err := zfs.GetDataset(targetDataset); err == nil {
			fmt.Printf("→ Destroying existing dataset for incremental restore...\n")
			// Destroy the existing dataset to allow clean restore
			cmd := exec.Command("zfs", "destroy", "-r", targetDataset)
			if err := cmd.Run(); err != nil {
				fmt.Printf("  Warning: could not destroy dataset (may not exist): %v\n", err)
			}
		}
	}

	// Check if target dataset exists
	if !force {
		if _, err := zfs.GetDataset(targetDataset); err == nil {
			return fmt.Errorf("target dataset %s already exists. Use --force to overwrite", targetDataset)
		}
	}

	// If force is used, destroy the dataset first (including all snapshots)
	if force {
		if _, err := zfs.GetDataset(targetDataset); err == nil {
			fmt.Printf("→ Destroying existing dataset %s (including all snapshots)...\n", targetDataset)
			cmd := exec.Command("zfs", "destroy", "-r", targetDataset)
			if err := cmd.Run(); err != nil {
				fmt.Printf("  Warning: could not destroy dataset: %v\n", err)
			}
		}
	}

	// Request download from server
	downloadURL := fmt.Sprintf("%s/download?client_id=%s&api_key=%s&snapshot_id=%s",
		c.ServerURL, c.ClientID, c.APIKey, snapshot.SnapshotID)

	fmt.Printf("→ Downloading from server...\n")

	resp, err := http.Get(downloadURL)
	if err != nil {
		return fmt.Errorf("failed to download: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("download failed: %s", body)
	}

	// Create decompression reader if needed
	var reader io.Reader = resp.Body
	if snapshot.Compressed {
		fmt.Printf("  Decompressing with LZ4...\n")
		lz4Reader := lz4.NewReader(resp.Body)
		reader = lz4Reader
	}

	// Receive into ZFS
	fmt.Printf("→ Receiving into ZFS dataset...\n")

	var cmd *exec.Cmd
	if force {
		cmd = exec.Command("zfs", "recv", "-F", targetDataset)
	} else {
		cmd = exec.Command("zfs", "recv", targetDataset)
	}

	cmd.Stdin = reader
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("zfs recv failed: %v", err)
	}

	fmt.Printf("\n✓ Snapshot restored successfully!\n")
	fmt.Printf("  Dataset: %s\n", targetDataset)

	return nil
}

// RestoreToFile downloads a snapshot and saves it to a file.
// This is useful for archiving or manual inspection.
func (c *Client) RestoreToFile(snapshot *SnapshotMetadata, outputFile string) error {
	fmt.Printf("\n=== Saving Snapshot to File ===\n")
	fmt.Printf("Source: %s\n", snapshot.SnapshotID)
	fmt.Printf("Output: %s\n", outputFile)
	fmt.Printf("Size: %.2f GB\n\n", float64(snapshot.SizeBytes)/(1024*1024*1024))

	// Request download
	downloadURL := fmt.Sprintf("%s/download?client_id=%s&api_key=%s&snapshot_id=%s",
		c.ServerURL, c.ClientID, c.APIKey, snapshot.SnapshotID)

	fmt.Printf("→ Downloading...\n")

	resp, err := http.Get(downloadURL)
	if err != nil {
		return fmt.Errorf("failed to download: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("download failed: %s", body)
	}

	// Create output file
	outFile, err := os.Create(outputFile)
	if err != nil {
		return fmt.Errorf("failed to create file: %v", err)
	}
	defer outFile.Close()

	// Copy with progress
	written, err := io.Copy(outFile, resp.Body)
	if err != nil {
		return fmt.Errorf("failed to save: %v", err)
	}

	fmt.Printf("\n✓ Snapshot saved successfully!\n")
	fmt.Printf("  Size: %.2f MB\n", float64(written)/(1024*1024))
	fmt.Printf("  File: %s\n", outputFile)

	return nil
}

// MountDataset mounts a restored dataset to a specified mountpoint for file recovery.
func (c *Client) MountDataset(dataset, mountpoint string) error {
	fmt.Printf("\n=== Mounting Dataset ===\n")
	fmt.Printf("Dataset: %s\n", dataset)
	fmt.Printf("Mountpoint: %s\n\n", mountpoint)

	ds, err := zfs.GetDataset(dataset)
	if err != nil {
		return fmt.Errorf("dataset not found: %v", err)
	}

	// Check current mountpoint
	currentMP, _ := ds.GetProperty("mountpoint")
	fmt.Printf("Current mountpoint: %s\n", currentMP)

	// Create mountpoint directory if it doesn't exist
	if err := os.MkdirAll(mountpoint, 0755); err != nil {
		return fmt.Errorf("failed to create mountpoint: %v", err)
	}

	// Set mountpoint property
	if err := ds.SetProperty("mountpoint", mountpoint); err != nil {
		return fmt.Errorf("failed to set mountpoint: %v", err)
	}

	// Mount the dataset if not already mounted
	cmd := exec.Command("zfs", "mount", dataset)
	if err := cmd.Run(); err != nil {
		// Might already be mounted, that's OK
		fmt.Printf("  (dataset may already be mounted)\n")
	}

	fmt.Printf("\n✓ Mounted successfully!\n")
	fmt.Printf("  Access files at: %s\n", mountpoint)
	fmt.Printf("  When done, run: umount %s\n", mountpoint)

	return nil
}

// Helper functions

// truncate shortens a string to maxLen characters with ellipsis.
func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen-3] + "..."
}

// formatDuration converts a duration to a human-readable string.
func formatDuration(d time.Duration) string {
	if d < time.Minute {
		return fmt.Sprintf("%.0f seconds", d.Seconds())
	} else if d < time.Hour {
		return fmt.Sprintf("%.0f minutes", d.Minutes())
	} else if d < 24*time.Hour {
		return fmt.Sprintf("%.1f hours", d.Hours())
	} else {
		return fmt.Sprintf("%.1f days", d.Hours()/24)
	}
}
