// SPDX-License-Identifier: GPL-3.0-or-later

// Package cli provides the backup command.
package cli

import (
	"context"
	"fmt"
	"time"

	"github.com/privatebackup/client-frontend-unix/internal/client"
	"github.com/privatebackup/client-frontend-unix/internal/output"
	"github.com/spf13/cobra"
)

// BackupOptions contains options for the backup command.
type BackupOptions struct {
	Exclude       []string
	ExcludeFile   string
	ExcludeCaches bool
	OneFileSystem bool
	Tags          []string
	DryRun        bool
	Stdin         bool
	StdinFilename string
}

// newBackupCommand creates the backup command.
func (c *CLI) newBackupCommand() *cobra.Command {
	opts := &BackupOptions{}

	cmd := &cobra.Command{
		Use:   "backup [paths...]",
		Short: "Create a new backup snapshot",
		Long: `Back up the specified files and directories to the Private Archive server.

Data is encrypted locally before upload - the server never sees your data.
Backups are incremental: only new or changed files are uploaded, making
subsequent backups fast and efficient.

Uses maximum compression by default to minimize storage costs and upload time.`,
		Example: `  # Backup home directory
  privatebackup backup ~

  # Backup specific directories
  privatebackup backup ~/documents ~/photos

  # Backup with exclusions
  privatebackup backup ~/projects --exclude "node_modules" --exclude "*.log"

  # Backup with tags
  privatebackup backup ~/documents --tag work --tag important

  # Dry run (see what would be backed up)
  privatebackup backup ~/documents --dry-run

  # Backup from file list
  find ~/important -name "*.pdf" | privatebackup backup --stdin`,
		Args: cobra.MinimumNArgs(0),
		RunE: func(cmd *cobra.Command, args []string) error {
			return c.runBackup(opts, args)
		},
	}

	flags := cmd.Flags()
	flags.StringSliceVarP(&opts.Exclude, "exclude", "e", nil, "Exclude files matching pattern (can repeat)")
	flags.StringVar(&opts.ExcludeFile, "exclude-file", "", "Read exclude patterns from file")
	flags.BoolVar(&opts.ExcludeCaches, "exclude-caches", true, "Exclude directories with CACHEDIR.TAG")
	flags.BoolVar(&opts.OneFileSystem, "one-file-system", false, "Don't cross filesystem boundaries")
	flags.StringSliceVarP(&opts.Tags, "tag", "t", nil, "Add tag to snapshot (can repeat)")
	flags.BoolVarP(&opts.DryRun, "dry-run", "n", false, "Don't actually upload, just show what would be done")
	flags.BoolVar(&opts.Stdin, "stdin", false, "Read file list from stdin")
	flags.StringVar(&opts.StdinFilename, "stdin-filename", "", "Name for stdin data (use with --stdin)")

	return cmd
}

// runBackup executes the backup command.
func (c *CLI) runBackup(opts *BackupOptions, paths []string) error {
	// Validate arguments
	if len(paths) == 0 && !opts.Stdin {
		return fmt.Errorf("no paths specified. Use 'privatebackup backup <path>' or '--stdin'")
	}

	// Check backend connection
	if err := c.checkBackendConnection(); err != nil {
		if IsBackendNotRunning(err) {
			c.renderer.Error("Cannot connect to backend daemon")
			c.renderer.Newline()
			c.renderer.Info("The Private Archive backend is not running.")
			c.renderer.Newline()
			c.renderer.Info("To start it:")
			c.renderer.Printf("  $ privatebackup daemon start\n")
			c.renderer.Newline()
			c.renderer.Info("Or run with --foreground for debugging:")
			c.renderer.Printf("  $ privatebackup daemon start --foreground\n")
			return err
		}
		return fmt.Errorf("backend error: %w", err)
	}

	// Expand paths
	expandedPaths := make([]string, len(paths))
	for i, p := range paths {
		expandedPaths[i] = expandPath(p)
	}

	// Build tags map
	tags := make(map[string]string)
	for _, tag := range opts.Tags {
		tags[tag] = "true"
	}

	// Build backup request
	req := &client.BackupRequest{
		Paths:   expandedPaths,
		Exclude: opts.Exclude,
		Tags:    tags,
	}

	if opts.DryRun {
		return c.runBackupDryRun(req)
	}

	// Start backup
	ctx, cancel := context.WithTimeout(context.Background(), 24*time.Hour) // Long timeout for backups
	defer cancel()

	c.renderer.Info("Starting backup...")
	c.renderer.Newline()

	status, err := c.client.StartBackup(ctx, req)
	if err != nil {
		// Check for rate limit
		if apiErr, ok := err.(*client.Error); ok {
			if apiErr.Code == "RATE_LIMITED" || apiErr.Code == "HTTP_429" {
				c.renderer.Error("Backup rate limit exceeded")
				c.renderer.Newline()
				c.renderer.Info("You can only run one backup every 23 hours.")
				c.renderer.Info("Use 'privatebackup status' to check when you can back up again.")
				return err
			}
		}
		return fmt.Errorf("failed to start backup: %w", err)
	}

	// Stream progress using SSE
	return c.streamBackupProgress(ctx, status.JobID)
}

// runBackupDryRun executes a dry run backup.
func (c *CLI) runBackupDryRun(req *client.BackupRequest) error {
	c.renderer.Info("Dry run - no files will be uploaded")
	c.renderer.Newline()

	c.renderer.Section("Backup Configuration")
	c.renderer.KeyValue("Paths", fmt.Sprintf("%v", req.Paths))
	if len(req.Exclude) > 0 {
		c.renderer.KeyValue("Exclude", fmt.Sprintf("%v", req.Exclude))
	}
	if len(req.Tags) > 0 {
		tags := make([]string, 0, len(req.Tags))
		for k := range req.Tags {
			tags = append(tags, k)
		}
		c.renderer.KeyValue("Tags", fmt.Sprintf("%v", tags))
	}

	// In a real implementation, we would call a dry-run endpoint
	// For now, just show what would be done
	c.renderer.Newline()
	c.renderer.Info("To perform the actual backup, run without --dry-run")

	return nil
}

// streamBackupProgress polls job progress until completion.
func (c *CLI) streamBackupProgress(ctx context.Context, jobID string) error {
	// Create progress bar if we have a TTY
	var progressBar *progressDisplay
	if c.terminal.CanShowProgress() && !c.options.Quiet {
		progressBar = newProgressDisplay(c.renderer, "Backing up")
	}

	handler := &backupProgressState{
		renderer:    c.renderer,
		progressBar: progressBar,
		startTime:   time.Now(),
	}

	// Poll job status until completion
	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			if progressBar != nil {
				progressBar.done()
			}
			return ctx.Err()
		case <-ticker.C:
			job, err := c.client.GetJob(ctx, jobID)
			if err != nil {
				// Ignore transient errors, keep polling
				continue
			}

			// Extract progress from metadata safely
			var bytesProcessed, filesProcessed int64
			var currentFile string
			if v, ok := job.Metadata["bytes_processed"]; ok {
				if f, ok := v.(float64); ok {
					bytesProcessed = int64(f)
				}
			}
			if v, ok := job.Metadata["files_processed"]; ok {
				if f, ok := v.(float64); ok {
					filesProcessed = int64(f)
				}
			}
			if v, ok := job.Metadata["current_file"]; ok {
				if s, ok := v.(string); ok {
					currentFile = s
				}
			}

			// Update progress
			progress := client.ProgressEvent{
				JobID:          job.ID,
				Status:         job.Status,
				Progress:       job.Progress,
				BytesProcessed: bytesProcessed,
				FilesProcessed: filesProcessed,
				CurrentFile:    currentFile,
			}

			handler.handleProgress(progress)

			// Check for completion
			switch job.Status {
			case "completed":
				handler.handleComplete(progress)
				if progressBar != nil {
					progressBar.done()
				}
				c.renderer.Newline()
				c.renderer.Success("Backup complete!")
				return nil
			case "failed":
				handler.failed = true
				handler.errorMsg = job.Error
				if progressBar != nil {
					progressBar.done()
				}
				c.renderer.Newline()
				c.renderer.Error("Backup failed")
				if job.Error != "" {
					c.renderer.ErrorWithDetails("Error", job.Error)
				}
				return fmt.Errorf("backup failed: %s", job.Error)
			case "cancelled":
				if progressBar != nil {
					progressBar.done()
				}
				c.renderer.Newline()
				c.renderer.Info("Backup cancelled")
				return fmt.Errorf("backup cancelled")
			}
		}
	}
}

// progressDisplay wraps progress bar rendering.
type progressDisplay struct {
	renderer *output.Renderer
	label    string
	current  int
}

func newProgressDisplay(r *output.Renderer, label string) *progressDisplay {
	return &progressDisplay{
		renderer: r,
		label:    label,
	}
}

func (p *progressDisplay) update(percent int, status string) {
	p.current = percent
	// In a real implementation, use the output package's progress bar
	p.renderer.Printf("\r%s... %d%% %s", p.label, percent, status)
}

func (p *progressDisplay) done() {
	p.renderer.Println()
}

// backupProgressState holds backup progress state.
type backupProgressState struct {
	renderer       *output.Renderer
	progressBar    *progressDisplay
	startTime      time.Time
	filesProcessed int64
	bytesProcessed int64
	snapshotID     string
	failed         bool
	errorMsg       string
}

func (h *backupProgressState) handleProgress(event client.ProgressEvent) {
	if h.progressBar != nil {
		status := ""
		if event.Rate > 0 {
			status = fmt.Sprintf("%.1f MB/s", event.Rate/1024/1024)
		}
		h.progressBar.update(event.Progress, status)
	}
	h.filesProcessed = event.FilesProcessed
	h.bytesProcessed = event.BytesProcessed
}

func (h *backupProgressState) handleComplete(event client.ProgressEvent) {
	h.filesProcessed = event.FilesProcessed
	h.bytesProcessed = event.BytesProcessed
	// Note: snapshot ID would be in metadata, not in ProgressEvent
}

func (h *backupProgressState) handleError(err error) {
	h.failed = true
	h.errorMsg = err.Error()
}
