// Package client provides ZFS snapshot backup client functionality.
// It handles creating snapshots and uploading them to a remote server.
package client

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

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

// SnapshotResult contains the result of a snapshot creation and send operation.
type SnapshotResult struct {
	FullBackup bool
	Snapshot   *zfs.Dataset
}

// Client handles snapshot backup operations to a remote server.
type Client struct {
	config *Config
}

// New creates a new Client instance with the provided configuration.
func New(config *Config) *Client {
	return &Client{config: config}
}

// CreateAndSend creates a snapshot and sends it to the backup server via HTTP.
// It automatically detects if this is a full or incremental backup:
// - If no bookmark exists, does a full backup
// - If bookmark exists, does an incremental backup from the bookmark
func (c *Client) CreateAndSend() (*SnapshotResult, error) {
	// Check for existing bookmark to determine backup type
	lastBookmark, err := c.GetLastBookmark()
	if err != nil {
		return nil, fmt.Errorf("failed to check bookmarks: %v", err)
	}

	// Create new snapshot
	snapshot, err := c.CreateSnapshot()
	if err != nil {
		return nil, fmt.Errorf("failed to create snapshot: %v", err)
	}

	isFullBackup := lastBookmark == ""
	if isFullBackup {
		fmt.Println("→ No previous backup found, doing FULL backup...")
		// Send as full (no base)
		if err := c.SendIncrementalHTTP(snapshot, ""); err != nil {
			return nil, fmt.Errorf("failed to send snapshot: %v", err)
		}
	} else {
		fmt.Printf("→ Found previous backup, doing INCREMENTAL from %s...\n", lastBookmark)
		// Send as incremental from bookmark
		if err := c.SendIncrementalHTTP(snapshot, lastBookmark); err != nil {
			return nil, fmt.Errorf("failed to send incremental: %v", err)
		}
	}

	// Create bookmark for future incremental backups
	if err := c.CreateBookmark(snapshot); err != nil {
		fmt.Printf("Warning: failed to create bookmark: %v\n", err)
	}

	return &SnapshotResult{
		FullBackup: isFullBackup,
		Snapshot:   snapshot,
	}, nil
}

// CreateSnapshot creates a local ZFS snapshot of the configured dataset.
func (c *Client) CreateSnapshot() (*zfs.Dataset, error) {
	ds, err := zfs.GetDataset(c.config.LocalDataset)
	if err != nil {
		return nil, fmt.Errorf("failed to get dataset: %v", err)
	}

	// Generate snapshot name with timestamp
	timestamp := time.Now().Format("2006-01-02_15:04:05")
	snapshotName := fmt.Sprintf("backup_%s", timestamp)

	// Create the snapshot
	snapshot, err := ds.Snapshot(snapshotName, false)
	if err != nil {
		return nil, fmt.Errorf("failed to create snapshot: %v", err)
	}

	fmt.Printf("✓ Created local snapshot: %s@%s\n", c.config.LocalDataset, snapshotName)
	return snapshot, nil
}

// GetSnapshotSize returns the used size of a snapshot in bytes.
func (c *Client) GetSnapshotSize(snapshot *zfs.Dataset) int64 {
	return int64(snapshot.Used)
}

// SendIncrementalHTTP sends a snapshot to the server via HTTP.
// The server then handles storage (S3 or local ZFS).
func (c *Client) SendIncrementalHTTP(snapshot *zfs.Dataset, base string) error {
	estimatedSize := c.GetSnapshotSize(snapshot)

	// Determine if this is incremental or full
	isIncremental := base != ""

	// Request upload authorization from server
	uploadReq := map[string]interface{}{
		"client_id":      c.config.ClientID,
		"api_key":        c.config.APIKey,
		"dataset_name":   c.config.LocalDataset,
		"timestamp":      time.Now().Format(time.RFC3339),
		"compressed":     c.config.Compress,
		"estimated_size": estimatedSize,
		"incremental":    isIncremental,
		"base_snapshot":  base,
	}

	reqBody, _ := json.Marshal(uploadReq)
	resp, err := http.Post(c.config.ServerURL+"/upload", "application/json", bytes.NewBuffer(reqBody))
	if err != nil {
		return fmt.Errorf("failed to request upload: %v", err)
	}
	defer resp.Body.Close()

	var uploadResp struct {
		Success      bool   `json:"success"`
		Message      string `json:"message"`
		UploadURL    string `json:"upload_url"`
		UploadMethod string `json:"upload_method"`
		StorageKey   string `json:"storage_key"`
	}

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

	if !uploadResp.Success {
		return fmt.Errorf("upload not authorized: %s", uploadResp.Message)
	}

	fmt.Printf("→ Upload authorized\n")
	fmt.Printf("  Method: %s\n", uploadResp.UploadMethod)
	fmt.Printf("  Storage key: %s\n", uploadResp.StorageKey)

	// Stream to server via HTTP
	return c.streamToServer(snapshot, base, uploadResp.UploadURL, uploadResp.StorageKey)
}

// streamToServer streams a ZFS snapshot to the backup server via HTTP.
func (c *Client) streamToServer(snapshot *zfs.Dataset, base, uploadURL, storageKey string) error {
	fmt.Printf("→ Streaming snapshot to server...\n")

	// Create ZFS send command
	var cmd *exec.Cmd
	if base != "" {
		// Incremental send from bookmark or snapshot
		cmd = exec.Command("zfs", "send", "-i", base, snapshot.Name)
	} else {
		// Full send
		cmd = exec.Command("zfs", "send", snapshot.Name)
	}

	zfsOut, err := cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("failed to create pipe: %v", err)
	}

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("failed to start zfs send: %v", err)
	}

	var reader io.Reader = zfsOut

	// Apply LZ4 compression if enabled
	if c.config.Compress {
		fmt.Printf("  Compressing with LZ4...\n")
		pr, pw := io.Pipe()
		lz4Writer := lz4.NewWriter(pw)
		lz4Writer.Apply(lz4.BlockSizeOption(lz4.BlockSize(4 * 1024 * 1024))) // 4MB blocks

		go func() {
			io.Copy(lz4Writer, zfsOut)
			lz4Writer.Close()
			pw.Close()
		}()

		reader = pr
	}

	// Create HTTP request to server
	req, err := http.NewRequest("POST", c.config.ServerURL+uploadURL, reader)
	if err != nil {
		return fmt.Errorf("failed to create request: %v", err)
	}

	// Set headers
	req.Header.Set("X-API-Key", c.config.APIKey)
	req.Header.Set("X-Storage-Key", storageKey)
	req.Header.Set("X-Dataset-Name", c.config.LocalDataset)
	req.Header.Set("X-Compressed", fmt.Sprintf("%v", c.config.Compress))
	req.Header.Set("X-Incremental", fmt.Sprintf("%v", base != ""))
	if base != "" {
		req.Header.Set("X-Base-Snapshot", base)
	}
	req.Header.Set("Content-Type", "application/octet-stream")

	// Send request with no timeout for large uploads
	httpClient := &http.Client{
		Timeout: 0,
	}

	httpResp, err := httpClient.Do(req)
	if err != nil {
		cmd.Process.Kill()
		return fmt.Errorf("failed to upload: %v", err)
	}
	defer httpResp.Body.Close()

	if httpResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(httpResp.Body)
		return fmt.Errorf("upload failed with status %d: %s", httpResp.StatusCode, body)
	}

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

	// Parse response
	var result struct {
		Success bool   `json:"success"`
		Message string `json:"message"`
		Size    int64  `json:"size"`
	}

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

	if !result.Success {
		return fmt.Errorf("upload failed: %s", result.Message)
	}

	fmt.Printf("✓ Snapshot uploaded successfully!\n")
	fmt.Printf("  Size: %.2f MB\n", float64(result.Size)/(1024*1024))

	return nil
}

// GetStatus retrieves and displays the client's backup status from the server.
func (c *Client) GetStatus() error {
	url := fmt.Sprintf("%s/status?client_id=%s&api_key=%s",
		c.config.ServerURL, c.config.ClientID, c.config.APIKey)

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

	var status struct {
		Success        bool    `json:"success"`
		TotalSnapshots int     `json:"total_snapshots"`
		UsedBytes      int64   `json:"used_bytes"`
		MaxBytes       int64   `json:"max_bytes"`
		PercentUsed    float64 `json:"percent_used"`
		StorageType    string  `json:"storage_type"`
	}

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

	if !status.Success {
		return fmt.Errorf("status check failed")
	}

	fmt.Printf("\n=== Server Status ===\n")
	fmt.Printf("Storage Type: %s\n", status.StorageType)
	fmt.Printf("Total Snapshots: %d\n", status.TotalSnapshots)
	fmt.Printf("Used: %.2f GB / %.2f GB (%.1f%%)\n",
		float64(status.UsedBytes)/(1024*1024*1024),
		float64(status.MaxBytes)/(1024*1024*1024),
		status.PercentUsed)

	return nil
}
