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

package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/privatebackup/client-backend/internal/config"
	"github.com/privatebackup/client-backend/internal/jobs"
	"github.com/privatebackup/client-backend/internal/keyfile"
	"github.com/privatebackup/client-backend/internal/restic"
	"github.com/privatebackup/client-backend/internal/server"
	"github.com/privatebackup/client-backend/internal/tor"
	"github.com/privatebackup/client-backend/internal/updater"
)

// Handlers contains all API handler methods.
type Handlers struct {
	torClient     *tor.Client
	serverAddress string // The .onion address of the backup server
	config        *config.Config
	keyfileMgr    *keyfile.Manager
	serverClient  *server.Client
	resticEngine  *restic.Engine
	jobManager    *jobs.Manager
	updater       *updater.Updater
	version       string
	startTime     time.Time

	// Mutex to protect reinitialization of services
	mu sync.RWMutex
}

// HandlersConfig contains all dependencies for creating Handlers.
type HandlersConfig struct {
	TorClient     *tor.Client
	ServerAddress string
	Config        *config.Config
	KeyfileMgr    *keyfile.Manager
	ServerClient  *server.Client
	ResticEngine  *restic.Engine
	JobManager    *jobs.Manager
	Version       string
}

// NewHandlers creates a new Handlers instance.
func NewHandlers(cfg HandlersConfig) *Handlers {
	v := cfg.Version
	if v == "" {
		v = "0.1.0"
	}
	h := &Handlers{
		torClient:     cfg.TorClient,
		serverAddress: cfg.ServerAddress,
		config:        cfg.Config,
		keyfileMgr:    cfg.KeyfileMgr,
		serverClient:  cfg.ServerClient,
		resticEngine:  cfg.ResticEngine,
		jobManager:    cfg.JobManager,
		version:       v,
		startTime:     time.Now(),
	}

	// Register job handlers if job manager is provided
	if cfg.JobManager != nil {
		cfg.JobManager.RegisterHandler(jobs.JobTypeBackup, h.handleBackupJob)
		cfg.JobManager.RegisterHandler(jobs.JobTypeRestore, h.handleRestoreJob)
		cfg.JobManager.RegisterHandler(jobs.JobTypeCheck, h.handleCheckJob)
		cfg.JobManager.RegisterHandler(jobs.JobTypePrune, h.handlePruneJob)
	}

	return h
}

// Response types

// StatusResponse represents the overall status response.
type StatusResponse struct {
	Status    string    `json:"status"`
	Version   string    `json:"version"`
	Uptime    string    `json:"uptime"`
	Timestamp time.Time `json:"timestamp"`
}

// TorStatusResponse represents Tor connection status.
type TorStatusResponse struct {
	Connected    bool      `json:"connected"`
	CircuitCount int       `json:"circuit_count"`
	BootstrapPct int       `json:"bootstrap_pct"`
	LastActivity time.Time `json:"last_activity"`
}

// ServerStatusResponse represents server connection status.
type ServerStatusResponse struct {
	Connected        bool      `json:"connected"`
	LastContactAt    time.Time `json:"last_contact_at"`
	AccountStatus    string    `json:"account_status"`
	CreditsRemaining float64   `json:"credits_remaining"`
}

// AccountResponse represents account information.
type AccountResponse struct {
	CertHash         string    `json:"cert_hash"`
	RegisteredAt     time.Time `json:"registered_at"`
	CreditsRemaining float64   `json:"credits_remaining"`
	StorageUsedBytes int64     `json:"storage_used_bytes"`
	Status           string    `json:"status"`
}

// BackupRequest represents a backup start request.
type BackupRequest struct {
	Paths   []string          `json:"paths" binding:"required"`
	Exclude []string          `json:"exclude,omitempty"`
	Tags    map[string]string `json:"tags,omitempty"`
}

// BackupStatusResponse represents backup status.
type BackupStatusResponse struct {
	JobID           string    `json:"job_id"`
	Status          string    `json:"status"`
	Progress        int       `json:"progress"`
	BytesProcessed  int64     `json:"bytes_processed"`
	FilesProcessed  int64     `json:"files_processed"`
	StartedAt       time.Time `json:"started_at,omitempty"`
	EstimatedTimeMs int64     `json:"estimated_time_ms,omitempty"`
	Error           string    `json:"error,omitempty"`
}

// RestoreRequest represents a restore start request.
type RestoreRequest struct {
	SnapshotID string   `json:"snapshot_id" binding:"required"`
	Target     string   `json:"target" binding:"required"`
	Paths      []string `json:"paths,omitempty"`
}

// RestoreStatusResponse represents restore status.
type RestoreStatusResponse struct {
	JobID           string    `json:"job_id"`
	Status          string    `json:"status"`
	Progress        int       `json:"progress"`
	BytesRestored   int64     `json:"bytes_restored"`
	FilesRestored   int64     `json:"files_restored"`
	StartedAt       time.Time `json:"started_at,omitempty"`
	EstimatedTimeMs int64     `json:"estimated_time_ms,omitempty"`
	Error           string    `json:"error,omitempty"`
}

// ForgetRequest represents a forget/prune request.
type ForgetRequest struct {
	KeepLast    int  `json:"keep_last"`
	KeepDaily   int  `json:"keep_daily"`
	KeepWeekly  int  `json:"keep_weekly"`
	KeepMonthly int  `json:"keep_monthly"`
	KeepYearly  int  `json:"keep_yearly"`
	Prune       bool `json:"prune"`
	DryRun      bool `json:"dry_run"`
}

// CheckRequest represents a repository check request.
type CheckRequest struct {
	ReadData       bool   `json:"read_data"`
	ReadDataSubset string `json:"read_data_subset"`
}

// PaymentRequest represents a payment intent request.
type PaymentRequest struct {
	CreditsRequested float64 `json:"credits_requested" binding:"required"`
}

// PaymentResponse represents a payment intent response.
type PaymentResponse struct {
	IntentID         string    `json:"intent_id"`
	CreditsRequested float64   `json:"credits_requested"`
	AmountXMR        string    `json:"amount_xmr"`
	PaymentAddress   string    `json:"payment_address"`
	ExpiresAt        time.Time `json:"expires_at"`
	RateLocked       bool      `json:"rate_locked"`
}

// SnapshotResponse represents a snapshot.
type SnapshotResponse struct {
	ID       string            `json:"id"`
	Time     time.Time         `json:"time"`
	Hostname string            `json:"hostname"`
	Paths    []string          `json:"paths"`
	Tags     map[string]string `json:"tags,omitempty"`
	Size     int64             `json:"size"`
}

// JobResponse represents a job.
type JobResponse struct {
	ID          string                 `json:"id"`
	Type        string                 `json:"type"`
	Status      string                 `json:"status"`
	Progress    int                    `json:"progress"`
	CreatedAt   time.Time              `json:"created_at"`
	StartedAt   time.Time              `json:"started_at,omitempty"`
	CompletedAt time.Time              `json:"completed_at,omitempty"`
	Error       string                 `json:"error,omitempty"`
	Metadata    map[string]interface{} `json:"metadata,omitempty"`
}

// ScheduleRequest represents a schedule create/update request.
type ScheduleRequest struct {
	Type     string                 `json:"type" binding:"required"`
	Interval string                 `json:"interval" binding:"required"`
	Enabled  bool                   `json:"enabled"`
	Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// ScheduleResponse represents a schedule.
type ScheduleResponse struct {
	Name     string                 `json:"name"`
	Type     string                 `json:"type"`
	Interval string                 `json:"interval"`
	Enabled  bool                   `json:"enabled"`
	LastRun  time.Time              `json:"last_run,omitempty"`
	NextRun  time.Time              `json:"next_run,omitempty"`
	Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// RegisterRequest represents a registration request.
type RegisterRequest struct {
	ServerAddress string `json:"server_address"` // Optional, uses config default if not provided
}

// ConfigResponse represents the configuration.
type ConfigResponse struct {
	BackupPaths         []string `json:"backup_paths"`
	ExcludePatterns     []string `json:"exclude_patterns"`
	ServerAddress       string   `json:"server_address"`
	TorEnabled          bool     `json:"tor_enabled"`
	Mode                string   `json:"mode"`
	AutoPrune           bool     `json:"auto_prune"`
	MissingPathBehavior string   `json:"missing_path_behavior"`
}

// ConfigUpdateRequest represents a configuration update request.
type ConfigUpdateRequest struct {
	BackupPaths         []string `json:"backup_paths,omitempty"`
	ExcludePatterns     []string `json:"exclude_patterns,omitempty"`
	MissingPathBehavior string   `json:"missing_path_behavior,omitempty"`
}

// ModeResponse represents the current mode.
type ModeResponse struct {
	Mode        string   `json:"mode"`
	Description string   `json:"description"`
	Features    []string `json:"features"`
}

// ModeUpdateRequest represents a mode change request.
type ModeUpdateRequest struct {
	Mode        string `json:"mode" binding:"required"`
	Acknowledge bool   `json:"acknowledge"` // Required for switching to advanced mode
}

// PathsResponse represents the configured backup paths.
type PathsResponse struct {
	Paths      []PathInfo `json:"paths"`
	TotalPaths int        `json:"total_paths"`
}

// PathInfo represents information about a single backup path.
type PathInfo struct {
	Path        string `json:"path"`
	Exists      bool   `json:"exists"`
	IsDir       bool   `json:"is_dir"`
	SizeBytes   int64  `json:"size_bytes,omitempty"`
	FileCount   int64  `json:"file_count,omitempty"`
	AccessError string `json:"access_error,omitempty"`
}

// PathAddRequest represents a request to add a backup path.
type PathAddRequest struct {
	Path string `json:"path" binding:"required"`
}

// PathRemoveRequest represents a request to remove a backup path.
type PathRemoveRequest struct {
	Path string `json:"path" binding:"required"`
}

// RetentionResponse represents the current retention policy.
type RetentionResponse struct {
	KeepWithinDaily   string `json:"keep_within_daily"`
	KeepWithinWeekly  string `json:"keep_within_weekly"`
	KeepWithinMonthly string `json:"keep_within_monthly"`
	KeepWithinYearly  string `json:"keep_within_yearly"`
	Description       string `json:"description"`
}

// ErrorResponse represents an error response.
type ErrorResponse struct {
	Error   string `json:"error"`
	Code    string `json:"code,omitempty"`
	Details string `json:"details,omitempty"`
}

// Handler implementations

// HealthCheck handles GET /api/v1/health
func (h *Handlers) HealthCheck(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"status":    "healthy",
		"timestamp": time.Now(),
	})
}

// GetStatus handles GET /api/v1/status
func (h *Handlers) GetStatus(c *gin.Context) {
	uptime := time.Since(h.startTime).Round(time.Second)
	c.JSON(http.StatusOK, StatusResponse{
		Status:    "operational",
		Version:   h.version,
		Uptime:    uptime.String(),
		Timestamp: time.Now(),
	})
}

// GetTorStatus handles GET /api/v1/status/tor
func (h *Handlers) GetTorStatus(c *gin.Context) {
	connected := false
	bootstrapPct := 0

	if h.torClient != nil {
		connected = h.torClient.IsConnected()
		if connected {
			bootstrapPct = 100
		}
	}

	c.JSON(http.StatusOK, TorStatusResponse{
		Connected:    connected,
		CircuitCount: 0, // TODO: implement circuit counting
		BootstrapPct: bootstrapPct,
		LastActivity: time.Now(),
	})
}

// GetServerStatus handles GET /api/v1/status/server
func (h *Handlers) GetServerStatus(c *gin.Context) {
	h.mu.RLock()
	serverClient := h.serverClient
	h.mu.RUnlock()

	// If we have a server client, try to get live account status
	if serverClient != nil {
		ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
		defer cancel()

		status, err := serverClient.GetAccountStatus(ctx)
		if err == nil {
			c.JSON(http.StatusOK, ServerStatusResponse{
				Connected:        true,
				LastContactAt:    time.Now(),
				AccountStatus:    status.Status,
				CreditsRemaining: status.CreditsRemaining,
			})
			return
		}
		// If error, fall through to try TCP dial
	}

	// Fallback: Try to connect to server through Tor if available
	connected := false
	var lastContact time.Time

	if h.torClient != nil && h.torClient.IsConnected() && h.serverAddress != "" {
		ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
		defer cancel()

		// Ensure server address includes port (default to 443 for .onion)
		dialAddr := h.serverAddress
		if !strings.Contains(dialAddr, ":") {
			dialAddr = dialAddr + ":443"
		}

		// Try to dial the server
		conn, err := h.torClient.DialContext(ctx, "tcp", dialAddr)
		if err == nil {
			conn.Close()
			connected = true
			lastContact = time.Now()
		}
	}

	c.JSON(http.StatusOK, ServerStatusResponse{
		Connected:        connected,
		LastContactAt:    lastContact,
		AccountStatus:    "unknown",
		CreditsRemaining: 0,
	})
}

// GetAccount handles GET /api/v1/account
func (h *Handlers) GetAccount(c *gin.Context) {
	h.mu.RLock()
	kfMgr := h.keyfileMgr
	serverClient := h.serverClient
	h.mu.RUnlock()

	// Check if we have a keyfile (registered)
	if kfMgr == nil || !kfMgr.Exists() {
		c.JSON(http.StatusOK, AccountResponse{
			Status: "not_registered",
		})
		return
	}

	// Load keyfile to get cert hash
	kf, err := kfMgr.Load()
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to load keyfile",
			Code:    "KEYFILE_ERROR",
			Details: err.Error(),
		})
		return
	}

	resp := AccountResponse{
		CertHash:     kf.CertHash,
		RegisteredAt: kf.Certificate.NotBefore,
		Status:       "registered",
	}

	// If we have a server client, try to get live account status
	if serverClient != nil {
		ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
		defer cancel()

		status, err := serverClient.GetAccountStatus(ctx)
		if err == nil {
			resp.CreditsRemaining = status.CreditsRemaining
			resp.StorageUsedBytes = status.StorageUsedBytes()
			resp.Status = status.Status
		}
		// If error, just return what we have from the keyfile
	}

	c.JSON(http.StatusOK, resp)
}

// Register handles POST /api/v1/account/register
func (h *Handlers) Register(c *gin.Context) {
	var req RegisterRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	// Check if already registered
	if h.keyfileMgr != nil && h.keyfileMgr.Exists() {
		c.JSON(http.StatusConflict, ErrorResponse{
			Error:   "Already registered",
			Code:    "ALREADY_REGISTERED",
			Details: "A backup key file already exists. Delete it first if you want to re-register.",
		})
		return
	}

	// Validate Tor is available
	if h.torClient == nil || !h.torClient.IsConnected() {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error:   "Tor not available",
			Code:    "TOR_NOT_CONNECTED",
			Details: "Tor connection is required for registration.",
		})
		return
	}

	// Use provided server address or fall back to configured one
	serverAddr := req.ServerAddress
	if serverAddr == "" {
		serverAddr = h.serverAddress
	}
	if serverAddr == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Server address required",
			Code:    "MISSING_SERVER_ADDRESS",
			Details: "Provide server_address in the request or configure it.",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Minute)
	defer cancel()

	// Step 1: Generate key pair
	privateKey, err := keyfile.GenerateKeyPair()
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to generate key pair",
			Code:    "KEY_GENERATION_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Step 2: Generate CSR
	csrPEM, err := keyfile.GenerateCSR(privateKey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to generate CSR",
			Code:    "CSR_GENERATION_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Step 3: Register with server via Tor
	resp, err := server.Register(ctx, serverAddr, csrPEM, h.torClient)
	if err != nil {
		c.JSON(http.StatusBadGateway, ErrorResponse{
			Error:   "Failed to register with server",
			Code:    "REGISTRATION_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Step 4: Parse returned certificates
	cert, err := keyfile.ParseCertificatePEM(resp.Certificate)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to parse certificate",
			Code:    "CERT_PARSE_FAILED",
			Details: err.Error(),
		})
		return
	}

	caCert, err := keyfile.ParseCertificatePEM(resp.CACertificate)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to parse CA certificate",
			Code:    "CA_CERT_PARSE_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Step 5: Create keyfile with parsed certificates and S3 credentials
	var kf *keyfile.BackupKeyFile
	if resp.S3Credentials != nil {
		kf = keyfile.NewWithS3(privateKey, cert, caCert,
			resp.S3Credentials.AccessKeyID,
			resp.S3Credentials.SecretAccessKey,
			resp.S3Credentials.BucketName)
	} else {
		kf = keyfile.New(privateKey, cert, caCert)
	}

	// Verify cert hash matches server's response
	if kf.CertHash != resp.CertHash {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Certificate hash mismatch",
			Code:    "CERT_HASH_MISMATCH",
			Details: fmt.Sprintf("Expected %s, got %s", resp.CertHash, kf.CertHash),
		})
		return
	}

	// Step 6: Save keyfile
	if err := h.keyfileMgr.Save(kf); err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to save keyfile",
			Code:    "KEYFILE_SAVE_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Step 7: Initialize server client and restic engine
	if err := h.reinitializeServices(kf, serverAddr); err != nil {
		// Don't fail the request - keyfile is saved, services will init on restart
		c.JSON(http.StatusOK, gin.H{
			"status":        "registered",
			"cert_hash":     kf.CertHash,
			"server":        serverAddr,
			"warning":       "Service initialization failed, restart may be required",
			"init_error":    err.Error(),
			"keyfile_saved": true,
		})
		return
	}

	// Update server address in handler
	h.mu.Lock()
	h.serverAddress = serverAddr
	h.mu.Unlock()

	c.JSON(http.StatusOK, gin.H{
		"status":    "registered",
		"cert_hash": kf.CertHash,
		"server":    serverAddr,
	})
}

// isOnionHost reports whether addr is a Tor .onion hidden service. addr may
// include a port (e.g. "abc…onion:443").
func isOnionHost(addr string) bool {
	host := addr
	if i := strings.LastIndex(host, ":"); i >= 0 {
		host = host[:i]
	}
	return strings.HasSuffix(strings.ToLower(host), ".onion")
}

// reinitializeServices creates server client and restic engine after registration.
func (h *Handlers) reinitializeServices(kf *keyfile.BackupKeyFile, serverAddr string) error {
	h.mu.Lock()
	defer h.mu.Unlock()

	// Create server client (uses mTLS for API endpoints)
	serverConfig := server.DefaultConfig()
	serverConfig.ServerAddress = serverAddr
	serverConfig.KeyFile = kf
	serverConfig.Dialer = h.torClient

	if h.config != nil {
		serverConfig.Timeout = h.config.Server.Timeout
		serverConfig.MaxConnections = h.config.Performance.MaxConnections
	}

	serverClient, err := server.NewClient(serverConfig)
	if err != nil {
		return fmt.Errorf("failed to create server client: %w", err)
	}
	h.serverClient = serverClient

	// Derive repository password
	repoPassword, err := kf.GetRepositoryPassword()
	if err != nil {
		return fmt.Errorf("failed to derive repository password: %w", err)
	}

	// Create restic engine - prefer S3 backend, fall back to REST
	resticCfg := restic.DefaultConfig()
	resticCfg.Password = repoPassword
	resticCfg.Compression = restic.CompressionMax
	resticCfg.ProxyURL = h.torClient.SOCKSProxyURL() // Route CLI through Tor SOCKS proxy

	// Write CA cert so restic can verify the server's TLS certificate.
	// The CA cert is embedded in the keyfile; write it once per init.
	var caCertPath string
	if h.config != nil {
		caCertPath = filepath.Join(h.config.Paths.ConfigDir, "server-ca.pem")
	} else {
		caCertPath = filepath.Join(os.TempDir(), "privatebackup-server-ca.pem")
	}
	if err := kf.CreateCACertPEM(caCertPath); err != nil {
		return fmt.Errorf("failed to write CA certificate: %w", err)
	}
	resticCfg.CACertPath = caCertPath

	// .onion servers present a TLS certificate for the cluster-internal name
	// (backup-proxy.…svc.cluster.local), NOT the onion host — Tor already
	// provides the server's cryptographic identity via the onion address, so
	// restic's TLS hostname/CA verification will always fail against an onion.
	// Skip verification for onion repos (mirrors the API server client, which
	// sets InsecureSkipVerify for .onion). Non-onion servers keep full TLS
	// verification. Without this, backups/restores to the onion fail with
	// "certificate is valid for …svc.cluster.local, not <onion>".
	resticCfg.InsecureSkipVerify = isOnionHost(serverAddr)

	if kf.HasS3Credentials() {
		// S3 backend: AWS Sig V4 authentication (no mTLS needed for data)
		resticCfg.RepositoryURL = fmt.Sprintf("s3:https://%s/%s", serverAddr, kf.S3BucketName)
		resticCfg.S3AccessKeyID = kf.S3AccessKeyID
		resticCfg.S3SecretAccessKey = kf.S3SecretAccessKey
	} else {
		// Legacy REST backend: mTLS authentication
		resticCfg.RepositoryURL = fmt.Sprintf("rest:https://%s/%s", serverAddr, kf.CertHash)

		// Create combined PEM file for mTLS (required for restic CLI)
		var mtlsPEMPath string
		if h.config != nil {
			mtlsPEMPath = filepath.Join(h.config.Paths.ConfigDir, "mtls-client.pem")
		} else {
			mtlsPEMPath = filepath.Join(os.TempDir(), "privatebackup-mtls-client.pem")
		}
		if err := kf.CreateCombinedPEM(mtlsPEMPath); err != nil {
			return fmt.Errorf("failed to create mTLS PEM file: %w", err)
		}
		resticCfg.TLSClientCertPath = mtlsPEMPath
	}

	if h.config != nil {
		resticCfg.MaxConnections = h.config.Performance.MaxConnections
	}

	resticEngine, err := restic.NewEngine(resticCfg)
	if err != nil {
		return fmt.Errorf("failed to create restic engine: %w", err)
	}
	h.resticEngine = resticEngine

	return nil
}

// handleBackupJob is the job handler for backup operations.
func (h *Handlers) handleBackupJob(ctx context.Context, job *jobs.Job, progress func(int)) error {
	h.mu.RLock()
	engine := h.resticEngine
	cfg := h.config
	h.mu.RUnlock()

	if engine == nil {
		return fmt.Errorf("not registered - please register first")
	}

	// Get paths from job metadata
	paths, ok := job.Metadata["paths"].([]string)
	if !ok || len(paths) == 0 {
		return fmt.Errorf("no backup paths specified")
	}

	excludes, _ := job.Metadata["excludes"].([]string)
	tags, _ := job.Metadata["tags"].([]string)

	// Ensure repository is initialized or opened
	if err := engine.InitRepository(ctx); err != nil && err != restic.ErrRepositoryExists {
		return fmt.Errorf("failed to initialize repository: %w", err)
	}

	if err := engine.OpenRepository(ctx); err != nil && err != restic.ErrRepositoryNotFound {
		// Ignore ErrRepositoryNotFound here as init may have just created it
	}

	// Perform backup
	var uploadOnce sync.Once
	opts := restic.BackupOptions{
		Paths:    paths,
		Excludes: excludes,
		Tags:     tags,
		// ActivityCallback fires on every JSON line from restic — including the
		// index-loading and file-scanning phases where TotalBytes is not yet known.
		// Calling progress() here resets the hung-job watchdog so a slow index
		// fetch over Tor (which can be silent for a long time on large repos) is
		// not mistaken for a hang.
		ActivityCallback: func() {
			progress(0) // percentage unknown yet; watchdog just needs the heartbeat
		},
		ProgressCallback: func(bp restic.BackupProgress) {
			if bp.TotalBytes > 0 {
				uploadOnce.Do(func() {
					log.Printf("backup: scan complete, uploading %.2f GB (%d files)",
						float64(bp.TotalBytes)/(1<<30), bp.TotalFiles)
				})
				pct := int(float64(bp.ProcessedBytes) / float64(bp.TotalBytes) * 100)
				progress(pct)
			}
		},
	}

	result, err := engine.Backup(ctx, opts)
	if err == restic.ErrLocked {
		// Stale lock from a previously interrupted backup. Clear it and retry once.
		if unlockErr := engine.Unlock(ctx); unlockErr != nil {
			return fmt.Errorf("backup failed (locked) and unlock failed: %w", unlockErr)
		}
		result, err = engine.Backup(ctx, opts)
	}
	if err != nil {
		return fmt.Errorf("backup failed: %w", err)
	}

	// Store result in job metadata
	job.Metadata["result"] = map[string]interface{}{
		"snapshot_id":      result.SnapshotID,
		"files_new":        result.FilesNew,
		"files_changed":    result.FilesChanged,
		"files_unmodified": result.FilesUnmodified,
		"data_added":       result.DataAdded,
		"total_processed":  result.TotalBytesProcessed,
		"duration_seconds": result.Duration.Seconds(),
	}

	// Auto-prune after successful backup (in simple mode with auto-prune enabled)
	if cfg != nil && cfg.Backup.Mode == config.ModeSimple && cfg.Backup.AutoPrune {
		go func() {
			pruneCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
			defer cancel()

			pruneOpts := restic.AutoPruneOptions{
				KeepWithinDaily:   cfg.Backup.Retention.KeepWithinDaily,
				KeepWithinWeekly:  cfg.Backup.Retention.KeepWithinWeekly,
				KeepWithinMonthly: cfg.Backup.Retention.KeepWithinMonthly,
				KeepWithinYearly:  cfg.Backup.Retention.KeepWithinYearly,
			}

			// Use defaults if retention config is empty
			if pruneOpts.KeepWithinDaily == "" {
				defaults := restic.DefaultAutoPruneOptions()
				pruneOpts = defaults
			}

			if _, err := engine.AutoPrune(pruneCtx, pruneOpts); err != nil {
				// Log warning but don't fail the backup - prune will run next time
				// TODO(logging): Replace with structured logging once a logging framework is added
				// For now, use stderr to avoid polluting stdout which may be parsed
				_, _ = fmt.Fprintf(os.Stderr, "warning: auto-prune failed (will retry next backup): %v\n", err)
			}
		}()
	}

	return nil
}

// handleRestoreJob is the job handler for restore operations.
func (h *Handlers) handleRestoreJob(ctx context.Context, job *jobs.Job, progress func(int)) error {
	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		return fmt.Errorf("not registered - please register first")
	}

	snapshotID, ok := job.Metadata["snapshot_id"].(string)
	if !ok || snapshotID == "" {
		return fmt.Errorf("no snapshot ID specified")
	}

	target, ok := job.Metadata["target"].(string)
	if !ok || target == "" {
		return fmt.Errorf("no target directory specified")
	}

	paths, _ := job.Metadata["paths"].([]string)

	// Open repository
	if err := engine.OpenRepository(ctx); err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}

	// Perform restore
	opts := restic.RestoreOptions{
		SnapshotID: snapshotID,
		Target:     target,
		Paths:      paths,
		ProgressCallback: func(rp restic.RestoreProgress) {
			if rp.TotalBytes > 0 {
				pct := int(float64(rp.ProcessedBytes) / float64(rp.TotalBytes) * 100)
				progress(pct)
			}
		},
	}

	result, err := engine.Restore(ctx, opts)
	if err != nil {
		return fmt.Errorf("restore failed: %w", err)
	}

	// Store result in job metadata
	job.Metadata["result"] = map[string]interface{}{
		"files_restored":   result.FilesRestored,
		"dirs_restored":    result.DirsRestored,
		"bytes_restored":   result.BytesRestored,
		"duration_seconds": result.Duration.Seconds(),
	}

	return nil
}

// handleCheckJob is the job handler for repository check operations.
func (h *Handlers) handleCheckJob(ctx context.Context, job *jobs.Job, progress func(int)) error {
	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		return fmt.Errorf("not registered - please register first")
	}

	readData, _ := job.Metadata["read_data"].(bool)

	if err := engine.OpenRepository(ctx); err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}

	progress(10)
	if err := engine.Check(ctx, readData); err != nil {
		return fmt.Errorf("repository check failed: %w", err)
	}
	progress(100)

	return nil
}

// handlePruneJob is the job handler for forget/prune operations.
func (h *Handlers) handlePruneJob(ctx context.Context, job *jobs.Job, progress func(int)) error {
	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		return fmt.Errorf("not registered - please register first")
	}

	keepLast, _ := job.Metadata["keep_last"].(int)
	keepDaily, _ := job.Metadata["keep_daily"].(int)
	keepWeekly, _ := job.Metadata["keep_weekly"].(int)
	keepMonthly, _ := job.Metadata["keep_monthly"].(int)
	keepYearly, _ := job.Metadata["keep_yearly"].(int)
	dryRun, _ := job.Metadata["dry_run"].(bool)

	if err := engine.OpenRepository(ctx); err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}

	progress(10)
	_, err := engine.Prune(ctx, restic.PruneOptions{
		KeepLast:    keepLast,
		KeepDaily:   keepDaily,
		KeepWeekly:  keepWeekly,
		KeepMonthly: keepMonthly,
		KeepYearly:  keepYearly,
		DryRun:      dryRun,
	})
	if err != nil {
		return fmt.Errorf("prune failed: %w", err)
	}
	progress(100)

	return nil
}

// StartBackup handles POST /api/v1/backup/start
func (h *Handlers) StartBackup(c *gin.Context) {
	var req BackupRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	if len(req.Paths) == 0 {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "At least one backup path is required",
			Code:  "EMPTY_PATHS",
		})
		return
	}

	// Check if registered
	h.mu.RLock()
	hasEngine := h.resticEngine != nil
	h.mu.RUnlock()

	if !hasEngine {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first using POST /api/v1/account/register",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	// Convert tags map to slice for restic
	var tags []string
	for k, v := range req.Tags {
		tags = append(tags, fmt.Sprintf("%s=%s", k, v))
	}

	// Submit backup job
	job, err := h.jobManager.Submit(jobs.JobTypeBackup, map[string]interface{}{
		"paths":    req.Paths,
		"excludes": req.Exclude,
		"tags":     tags,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to submit backup job",
			Code:    "JOB_SUBMIT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusAccepted, BackupStatusResponse{
		JobID:     job.ID,
		Status:    string(job.Status),
		StartedAt: job.StartedAt,
	})
}

// GetBackupStatus handles GET /api/v1/backup/status
func (h *Handlers) GetBackupStatus(c *gin.Context) {
	c.JSON(http.StatusOK, BackupStatusResponse{
		Status: "idle",
	})
}

// CancelBackup handles POST /api/v1/backup/cancel
func (h *Handlers) CancelBackup(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"cancelled": true})
}

// StartRestore handles POST /api/v1/restore/start
func (h *Handlers) StartRestore(c *gin.Context) {
	var req RestoreRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	// Check if registered
	h.mu.RLock()
	hasEngine := h.resticEngine != nil
	h.mu.RUnlock()

	if !hasEngine {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first using POST /api/v1/account/register",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	// Submit restore job
	job, err := h.jobManager.Submit(jobs.JobTypeRestore, map[string]interface{}{
		"snapshot_id": req.SnapshotID,
		"target":      req.Target,
		"paths":       req.Paths,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to submit restore job",
			Code:    "JOB_SUBMIT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusAccepted, RestoreStatusResponse{
		JobID:     job.ID,
		Status:    string(job.Status),
		StartedAt: job.StartedAt,
	})
}

// GetRestoreStatus handles GET /api/v1/restore/status
func (h *Handlers) GetRestoreStatus(c *gin.Context) {
	c.JSON(http.StatusOK, RestoreStatusResponse{
		Status: "idle",
	})
}

// CancelRestore handles POST /api/v1/restore/cancel
func (h *Handlers) CancelRestore(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"cancelled": true})
}

// ListSnapshots handles GET /api/v1/snapshots
func (h *Handlers) ListSnapshots(c *gin.Context) {
	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first using POST /api/v1/account/register",
		})
		return
	}

	// Snapshot listing reads repo metadata over Tor (high per-op latency); the
	// previous 2-minute cap made this GET fail/EOF over slow Tor (measured ~240s).
	// Honor the caller's request context so the client's own timeout governs (and
	// the op cancels if the client disconnects). Paired with the API server's
	// WriteTimeout=0 so the slow response can actually be written.
	ctx := c.Request.Context()

	// Open repository
	if err := engine.OpenRepository(ctx); err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to open repository",
			Code:    "REPOSITORY_ERROR",
			Details: err.Error(),
		})
		return
	}

	// List snapshots
	snapshots, err := engine.ListSnapshots(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to list snapshots",
			Code:    "SNAPSHOT_LIST_ERROR",
			Details: err.Error(),
		})
		return
	}

	// Convert to response format
	response := make([]SnapshotResponse, len(snapshots))
	for i, snap := range snapshots {
		response[i] = SnapshotResponse{
			ID:       snap.ID,
			Time:     snap.Time,
			Hostname: snap.Hostname,
			Paths:    snap.Paths,
			Tags:     make(map[string]string),
		}
		// Parse tags into map
		for _, tag := range snap.Tags {
			// Tags may be in "key=value" format
			response[i].Tags[tag] = ""
		}
	}

	c.JSON(http.StatusOK, response)
}

// GetSnapshot handles GET /api/v1/snapshots/:id
func (h *Handlers) GetSnapshot(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Snapshot ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	// Honor the caller's timeout — a snapshot read is restic-over-Tor (slow, high
	// per-op latency); a fixed cap truncated it over real Tor (see ListSnapshots).
	ctx := c.Request.Context()

	snap, err := engine.GetSnapshot(ctx, id)
	if err != nil {
		if errors.Is(err, restic.ErrSnapshotNotFound) {
			c.JSON(http.StatusNotFound, ErrorResponse{
				Error: "Snapshot not found",
				Code:  "NOT_FOUND",
			})
			return
		}
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to get snapshot",
			Code:    "SNAPSHOT_ERROR",
			Details: err.Error(),
		})
		return
	}

	resp := SnapshotResponse{
		ID:       snap.ID,
		Time:     snap.Time,
		Hostname: snap.Hostname,
		Paths:    snap.Paths,
		Tags:     make(map[string]string),
	}
	for _, tag := range snap.Tags {
		resp.Tags[tag] = ""
	}

	c.JSON(http.StatusOK, resp)
}

// DeleteSnapshot handles DELETE /api/v1/snapshots/:id
func (h *Handlers) DeleteSnapshot(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Snapshot ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
	defer cancel()

	if err := engine.DeleteSnapshot(ctx, id); err != nil {
		if errors.Is(err, restic.ErrSnapshotNotFound) {
			c.JSON(http.StatusNotFound, ErrorResponse{
				Error: "Snapshot not found",
				Code:  "NOT_FOUND",
			})
			return
		}
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to delete snapshot",
			Code:    "SNAPSHOT_DELETE_ERROR",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{"deleted": id})
}

// GetSnapshotFiles handles GET /api/v1/snapshots/:id/files
// Returns a flat list of all files/dirs in the snapshot, optionally under a path prefix.
func (h *Handlers) GetSnapshotFiles(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Snapshot ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	h.mu.RLock()
	engine := h.resticEngine
	h.mu.RUnlock()

	if engine == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	pathPrefix := c.Query("path")

	// Honor the caller's timeout (restic-over-Tor; see ListSnapshots).
	ctx := c.Request.Context()

	entries, err := engine.ListSnapshotFiles(ctx, id, pathPrefix)
	if err != nil {
		if errors.Is(err, restic.ErrSnapshotNotFound) {
			c.JSON(http.StatusNotFound, ErrorResponse{
				Error: "Snapshot not found",
				Code:  "NOT_FOUND",
			})
			return
		}
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to list snapshot files",
			Code:    "LS_ERROR",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"snapshot_id": id,
		"files":       entries,
		"total":       len(entries),
	})
}

// ForgetSnapshots handles POST /api/v1/snapshots/forget
func (h *Handlers) ForgetSnapshots(c *gin.Context) {
	h.mu.RLock()
	mode := config.ModeSimple
	if h.config != nil {
		mode = h.config.Backup.Mode
	}
	hasEngine := h.resticEngine != nil
	h.mu.RUnlock()

	if mode != config.ModeAdvanced {
		c.JSON(http.StatusForbidden, ErrorResponse{
			Error:   "Advanced mode required",
			Code:    "ADVANCED_MODE_REQUIRED",
			Details: "Switch to advanced mode via PUT /api/v1/mode to use forget",
		})
		return
	}

	if !hasEngine {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	var req ForgetRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	job, err := h.jobManager.Submit(jobs.JobTypePrune, map[string]interface{}{
		"keep_last":    req.KeepLast,
		"keep_daily":   req.KeepDaily,
		"keep_weekly":  req.KeepWeekly,
		"keep_monthly": req.KeepMonthly,
		"keep_yearly":  req.KeepYearly,
		"dry_run":      req.DryRun,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to submit forget job",
			Code:    "JOB_SUBMIT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusAccepted, gin.H{
		"job_id":  job.ID,
		"status":  string(job.Status),
		"message": "Forget job queued",
	})
}

// StartCheck handles POST /api/v1/check
func (h *Handlers) StartCheck(c *gin.Context) {
	h.mu.RLock()
	hasEngine := h.resticEngine != nil
	h.mu.RUnlock()

	if !hasEngine {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	var req CheckRequest
	// ShouldBindJSON on an empty body is fine; we use defaults if missing
	_ = c.ShouldBindJSON(&req)

	job, err := h.jobManager.Submit(jobs.JobTypeCheck, map[string]interface{}{
		"read_data":        req.ReadData,
		"read_data_subset": req.ReadDataSubset,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to submit check job",
			Code:    "JOB_SUBMIT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusAccepted, gin.H{
		"job_id":  job.ID,
		"status":  string(job.Status),
		"message": "Check job queued",
	})
}

// Unlock handles POST /api/v1/unlock
func (h *Handlers) Unlock(c *gin.Context) {
	h.mu.RLock()
	mode := config.ModeSimple
	if h.config != nil {
		mode = h.config.Backup.Mode
	}
	engine := h.resticEngine
	h.mu.RUnlock()

	if mode != config.ModeAdvanced {
		c.JSON(http.StatusForbidden, ErrorResponse{
			Error:   "Advanced mode required",
			Code:    "ADVANCED_MODE_REQUIRED",
			Details: "Switch to advanced mode via PUT /api/v1/mode to use unlock",
		})
		return
	}

	if engine == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	// Honor the caller's timeout (restic unlock over Tor; see ListSnapshots).
	ctx := c.Request.Context()

	if err := engine.Unlock(ctx); err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to unlock repository",
			Code:    "UNLOCK_ERROR",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Repository unlocked"})
}

// StreamJobProgress handles GET /api/v1/jobs/:id/progress (Server-Sent Events)
func (h *Handlers) StreamJobProgress(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Job ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	if h.jobManager.GetJob(id) == nil {
		c.JSON(http.StatusNotFound, ErrorResponse{
			Error: "Job not found",
			Code:  "NOT_FOUND",
		})
		return
	}

	c.Header("Content-Type", "text/event-stream")
	c.Header("Cache-Control", "no-cache")
	c.Header("Connection", "keep-alive")
	c.Header("X-Accel-Buffering", "no")

	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	reqCtx := c.Request.Context()

	c.Stream(func(w io.Writer) bool {
		select {
		case <-reqCtx.Done():
			return false
		case <-ticker.C:
			job := h.jobManager.GetJob(id)
			if job == nil {
				return false
			}

			payload, _ := json.Marshal(gin.H{
				"status":   string(job.Status),
				"progress": job.Progress,
			})

			switch job.Status {
			case jobs.JobStatusCompleted:
				resultPayload, _ := json.Marshal(gin.H{
					"status":   "completed",
					"progress": job.Progress,
					"result":   job.Metadata["result"],
				})
				fmt.Fprintf(w, "event: completed\ndata: %s\n\n", resultPayload)
				return false
			case jobs.JobStatusFailed:
				errPayload, _ := json.Marshal(gin.H{
					"status": "failed",
					"error":  job.Error,
				})
				fmt.Fprintf(w, "event: failed\ndata: %s\n\n", errPayload)
				return false
			default:
				fmt.Fprintf(w, "event: progress\ndata: %s\n\n", payload)
				return true
			}
		}
	})
}

// CreatePayment handles POST /api/v1/account/payment
func (h *Handlers) CreatePayment(c *gin.Context) {
	var req PaymentRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	if req.CreditsRequested <= 0 {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "credits_requested must be greater than zero",
			Code:    "INVALID_CREDITS",
			Details: "Specify a positive number of credits to purchase",
		})
		return
	}

	h.mu.RLock()
	serverClient := h.serverClient
	h.mu.RUnlock()

	if serverClient == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer cancel()

	intent, err := serverClient.CreatePaymentIntent(ctx, req.CreditsRequested)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to create payment intent",
			Code:    "PAYMENT_INTENT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusCreated, PaymentResponse{
		IntentID:         intent.IntentID,
		CreditsRequested: intent.CreditsRequested,
		AmountXMR:        intent.AmountXMR,
		PaymentAddress:   intent.PaymentAddress,
		ExpiresAt:        intent.ExpiresAt,
		RateLocked:       intent.RateLocked,
	})
}

// LTCPaymentResponse is the daemon's LTC payment-intent response. LTC is
// rate-locked (not a fixed amount): the UI computes the LTC needed for N credits
// as N * credit_price_zar / ltc_zar_rate_locked. A fiat card on-ramp (opt-in)
// can then buy that LTC and pay it to payment_address.
type LTCPaymentResponse struct {
	IntentID          string    `json:"intent_id"`
	PaymentAddress    string    `json:"payment_address"`
	LTCZARRate        string    `json:"ltc_zar_rate_locked"`
	CreditPriceZAR    string    `json:"credit_price_zar"`
	CreditsRequested  int       `json:"credits_requested"`
	ExpectedAmountLTC string    `json:"expected_amount_ltc"` // exact LTC to send for CreditsRequested
	ExpiresAt         time.Time `json:"expires_at"`
}

// CreateLTCPayment handles POST /api/v1/account/payment/ltc — issues a per-client
// LTC receive address with a locked LTC/ZAR rate. No request body (the rate-lock
// intent is created for the authenticated client).
func (h *Handlers) CreateLTCPayment(c *gin.Context) {
	h.mu.RLock()
	serverClient := h.serverClient
	h.mu.RUnlock()

	if serverClient == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first",
		})
		return
	}

	// Credits to buy (>= 1); the server computes the exact LTC to send for this
	// many. An absent/invalid body defaults to 1.
	credits := 1
	var body struct {
		Credits int `json:"credits"`
	}
	if err := c.ShouldBindJSON(&body); err == nil && body.Credits >= 1 {
		credits = body.Credits
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer cancel()

	intent, err := serverClient.CreateLTCPaymentIntent(ctx, credits)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to create LTC payment intent",
			Code:    "LTC_PAYMENT_INTENT_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusCreated, LTCPaymentResponse{
		IntentID:          intent.IntentID,
		PaymentAddress:    intent.PaymentAddress,
		LTCZARRate:        intent.LTCZARRate,
		CreditPriceZAR:    intent.CreditPriceZAR,
		CreditsRequested:  intent.CreditsRequested,
		ExpectedAmountLTC: intent.ExpectedAmountLTC,
		ExpiresAt:         intent.ExpiresAt,
	})
}

// GetLTCPaymentStatus handles GET /account/payment/ltc/status — proxies the
// pending LTC intent's payment shortfall so the UI can show "send $Z more".
func (h *Handlers) GetLTCPaymentStatus(c *gin.Context) {
	h.mu.RLock()
	serverClient := h.serverClient
	h.mu.RUnlock()

	if serverClient == nil {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error:   "Not registered",
			Code:    "NOT_REGISTERED",
			Details: "Please register with a server first",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer cancel()

	status, err := serverClient.GetLTCPaymentStatus(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to get LTC payment status",
			Code:    "LTC_PAYMENT_STATUS_FAILED",
			Details: err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, status)
}

// ListJobs handles GET /api/v1/jobs
func (h *Handlers) ListJobs(c *gin.Context) {
	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	// Get status filter from query param
	var statusFilter *jobs.JobStatus
	if statusStr := c.Query("status"); statusStr != "" {
		status := jobs.JobStatus(statusStr)
		statusFilter = &status
	}

	jobList := h.jobManager.ListJobs(statusFilter)

	// Convert to response format
	response := make([]JobResponse, 0, len(jobList))
	for _, job := range jobList {
		response = append(response, JobResponse{
			ID:          job.ID,
			Type:        string(job.Type),
			Status:      string(job.Status),
			Progress:    job.Progress,
			CreatedAt:   job.CreatedAt,
			StartedAt:   job.StartedAt,
			CompletedAt: job.CompletedAt,
			Error:       job.Error,
		})
	}

	c.JSON(http.StatusOK, response)
}

// GetJob handles GET /api/v1/jobs/:id
func (h *Handlers) GetJob(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Job ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	job := h.jobManager.GetJob(id)
	if job == nil {
		c.JSON(http.StatusNotFound, ErrorResponse{
			Error: "Job not found",
			Code:  "NOT_FOUND",
		})
		return
	}

	c.JSON(http.StatusOK, JobResponse{
		ID:          job.ID,
		Type:        string(job.Type),
		Status:      string(job.Status),
		Progress:    job.Progress,
		CreatedAt:   job.CreatedAt,
		StartedAt:   job.StartedAt,
		CompletedAt: job.CompletedAt,
		Error:       job.Error,
	})
}

// CancelJob handles POST /api/v1/jobs/:id/cancel
func (h *Handlers) CancelJob(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Job ID is required",
			Code:  "MISSING_ID",
		})
		return
	}

	if h.jobManager == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error: "Job manager not available",
			Code:  "SERVICE_UNAVAILABLE",
		})
		return
	}

	err := h.jobManager.CancelJob(id)
	if err != nil {
		c.JSON(http.StatusNotFound, ErrorResponse{
			Error: err.Error(),
			Code:  "NOT_FOUND",
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{"cancelled": true})
}

// ListSchedules handles GET /api/v1/schedules
func (h *Handlers) ListSchedules(c *gin.Context) {
	c.JSON(http.StatusOK, []ScheduleResponse{})
}

// CreateSchedule handles POST /api/v1/schedules
func (h *Handlers) CreateSchedule(c *gin.Context) {
	var req ScheduleRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	c.JSON(http.StatusCreated, ScheduleResponse{
		Type:     req.Type,
		Interval: req.Interval,
		Enabled:  req.Enabled,
	})
}

// GetSchedule handles GET /api/v1/schedules/:name
func (h *Handlers) GetSchedule(c *gin.Context) {
	name := c.Param("name")
	if name == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Schedule name is required",
			Code:  "MISSING_NAME",
		})
		return
	}

	c.JSON(http.StatusNotFound, ErrorResponse{
		Error: "Schedule not found",
		Code:  "NOT_FOUND",
	})
}

// UpdateSchedule handles PUT /api/v1/schedules/:name
func (h *Handlers) UpdateSchedule(c *gin.Context) {
	name := c.Param("name")
	if name == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Schedule name is required",
			Code:  "MISSING_NAME",
		})
		return
	}

	var req ScheduleRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	c.JSON(http.StatusNotFound, ErrorResponse{
		Error: "Schedule not found",
		Code:  "NOT_FOUND",
	})
}

// DeleteSchedule handles DELETE /api/v1/schedules/:name
func (h *Handlers) DeleteSchedule(c *gin.Context) {
	name := c.Param("name")
	if name == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Schedule name is required",
			Code:  "MISSING_NAME",
		})
		return
	}

	c.JSON(http.StatusNotFound, ErrorResponse{
		Error: "Schedule not found",
		Code:  "NOT_FOUND",
	})
}

// TriggerSchedule handles POST /api/v1/schedules/:name/trigger
func (h *Handlers) TriggerSchedule(c *gin.Context) {
	name := c.Param("name")
	if name == "" {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Schedule name is required",
			Code:  "MISSING_NAME",
		})
		return
	}

	c.JSON(http.StatusNotFound, ErrorResponse{
		Error: "Schedule not found",
		Code:  "NOT_FOUND",
	})
}

// GetConfig handles GET /api/v1/config
func (h *Handlers) GetConfig(c *gin.Context) {
	h.mu.RLock()
	cfg := h.config
	h.mu.RUnlock()

	resp := ConfigResponse{
		BackupPaths:         []string{},
		ExcludePatterns:     []string{},
		ServerAddress:       h.serverAddress,
		TorEnabled:          true,
		Mode:                string(config.ModeSimple),
		AutoPrune:           true,
		MissingPathBehavior: "warn",
	}

	if cfg != nil {
		resp.BackupPaths = cfg.Backup.Paths
		resp.ExcludePatterns = cfg.Backup.DefaultExcludes
		resp.Mode = string(cfg.Backup.Mode)
		resp.AutoPrune = cfg.Backup.AutoPrune
		resp.MissingPathBehavior = cfg.Backup.MissingPathBehavior
		if cfg.Server.Address != "" {
			resp.ServerAddress = cfg.Server.Address
		}
	}

	c.JSON(http.StatusOK, resp)
}

// UpdateConfig handles PUT /api/v1/config
func (h *Handlers) UpdateConfig(c *gin.Context) {
	var req ConfigUpdateRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	h.mu.Lock()
	if h.config != nil {
		if req.BackupPaths != nil {
			h.config.Backup.Paths = req.BackupPaths
		}
		if req.ExcludePatterns != nil {
			h.config.Backup.DefaultExcludes = req.ExcludePatterns
		}
		if req.MissingPathBehavior != "" {
			h.config.Backup.MissingPathBehavior = req.MissingPathBehavior
		}
	}
	h.mu.Unlock()

	// Return updated config
	h.GetConfig(c)
}

// GetMode handles GET /api/v1/mode
func (h *Handlers) GetMode(c *gin.Context) {
	h.mu.RLock()
	mode := config.ModeSimple
	if h.config != nil {
		mode = h.config.Backup.Mode
	}
	h.mu.RUnlock()

	resp := ModeResponse{
		Mode: string(mode),
	}

	if mode == config.ModeSimple {
		resp.Description = "Simple Mode - Automatic retention, guided backup paths"
		resp.Features = []string{
			"backup", "restore", "snapshots", "snapshots delete",
			"status", "pay", "export-key", "restore-account",
			"paths add", "paths remove", "paths list",
			"mode", "check",
		}
	} else {
		resp.Description = "Advanced Mode - Full control over restic operations (use with caution)"
		resp.Features = []string{
			"All simple mode commands",
			"forget (custom retention)",
			"prune (manual cleanup)",
			"unlock (remove locks)",
			"key (manage keys)",
			"stats (repository stats)",
			"restic (direct pass-through)",
		}
	}

	c.JSON(http.StatusOK, resp)
}

// UpdateMode handles PUT /api/v1/mode
func (h *Handlers) UpdateMode(c *gin.Context) {
	var req ModeUpdateRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	// Validate mode
	newMode := config.Mode(req.Mode)
	if newMode != config.ModeSimple && newMode != config.ModeAdvanced {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Invalid mode",
			Code:    "INVALID_MODE",
			Details: "Mode must be 'simple' or 'advanced'",
		})
		return
	}

	// Require acknowledgement for switching to advanced mode
	if newMode == config.ModeAdvanced && !req.Acknowledge {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Advanced mode requires acknowledgement",
			Code:    "ACKNOWLEDGEMENT_REQUIRED",
			Details: "Set 'acknowledge: true' to confirm switching to advanced mode. Warning: You can accidentally delete all your backups in advanced mode.",
		})
		return
	}

	h.mu.Lock()
	if h.config != nil {
		h.config.Backup.Mode = newMode
	}
	h.mu.Unlock()

	// Return updated mode
	h.GetMode(c)
}

// ListPaths handles GET /api/v1/paths
func (h *Handlers) ListPaths(c *gin.Context) {
	h.mu.RLock()
	cfg := h.config
	h.mu.RUnlock()

	paths := []string{}
	if cfg != nil {
		paths = cfg.Backup.Paths
	}

	pathInfos := make([]PathInfo, 0, len(paths))
	for _, p := range paths {
		info := PathInfo{Path: p}

		// Check if path exists and get info
		if fi, err := os.Stat(p); err == nil {
			info.Exists = true
			info.IsDir = fi.IsDir()
			if !fi.IsDir() {
				info.SizeBytes = fi.Size()
			}
		} else if os.IsNotExist(err) {
			info.Exists = false
		} else {
			info.AccessError = err.Error()
		}

		pathInfos = append(pathInfos, info)
	}

	c.JSON(http.StatusOK, PathsResponse{
		Paths:      pathInfos,
		TotalPaths: len(pathInfos),
	})
}

// AddPath handles POST /api/v1/paths
func (h *Handlers) AddPath(c *gin.Context) {
	var req PathAddRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	// Normalize path
	absPath, err := filepath.Abs(req.Path)
	if err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Invalid path",
			Code:    "INVALID_PATH",
			Details: err.Error(),
		})
		return
	}

	// Check if path exists
	fi, err := os.Stat(absPath)
	if err != nil {
		if os.IsNotExist(err) {
			c.JSON(http.StatusBadRequest, ErrorResponse{
				Error:   "Path does not exist",
				Code:    "PATH_NOT_FOUND",
				Details: absPath,
			})
			return
		}
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Cannot access path",
			Code:    "PATH_ACCESS_ERROR",
			Details: err.Error(),
		})
		return
	}

	if !fi.IsDir() {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Path is not a directory",
			Code:    "NOT_DIRECTORY",
			Details: "Backup paths must be directories",
		})
		return
	}

	h.mu.Lock()
	if h.config != nil {
		// Check for duplicates
		for _, existing := range h.config.Backup.Paths {
			existingAbs, _ := filepath.Abs(existing)
			if existingAbs == absPath {
				h.mu.Unlock()
				c.JSON(http.StatusConflict, ErrorResponse{
					Error:   "Path already configured",
					Code:    "DUPLICATE_PATH",
					Details: absPath,
				})
				return
			}
		}
		h.config.Backup.Paths = append(h.config.Backup.Paths, absPath)
	}

	// Build response with all paths
	paths := []string{}
	if h.config != nil {
		paths = h.config.Backup.Paths
	}
	h.mu.Unlock()

	pathInfos := make([]PathInfo, 0, len(paths))
	for _, p := range paths {
		info := PathInfo{Path: p}
		if fi, err := os.Stat(p); err == nil {
			info.Exists = true
			info.IsDir = fi.IsDir()
		}
		pathInfos = append(pathInfos, info)
	}

	c.JSON(http.StatusCreated, PathsResponse{
		Paths:      pathInfos,
		TotalPaths: len(pathInfos),
	})
}

// RemovePath handles DELETE /api/v1/paths
func (h *Handlers) RemovePath(c *gin.Context) {
	var req PathRemoveRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error: "Invalid request body",
			Code:  "INVALID_REQUEST",
		})
		return
	}

	// Normalize path
	absPath, err := filepath.Abs(req.Path)
	if err != nil {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "Invalid path",
			Code:    "INVALID_PATH",
			Details: err.Error(),
		})
		return
	}

	h.mu.Lock()
	found := false
	var paths []string
	if h.config != nil {
		newPaths := make([]string, 0, len(h.config.Backup.Paths))
		for _, p := range h.config.Backup.Paths {
			pAbs, _ := filepath.Abs(p)
			if pAbs == absPath {
				found = true
				continue
			}
			newPaths = append(newPaths, p)
		}
		if found {
			h.config.Backup.Paths = newPaths
		}
		paths = h.config.Backup.Paths
	}
	h.mu.Unlock()

	if !found {
		c.JSON(http.StatusNotFound, ErrorResponse{
			Error:   "Path not in backup list",
			Code:    "PATH_NOT_FOUND",
			Details: absPath,
		})
		return
	}

	// Build response with remaining paths
	pathInfos := make([]PathInfo, 0, len(paths))
	for _, p := range paths {
		info := PathInfo{Path: p}
		if fi, err := os.Stat(p); err == nil {
			info.Exists = true
			info.IsDir = fi.IsDir()
		}
		pathInfos = append(pathInfos, info)
	}

	c.JSON(http.StatusOK, PathsResponse{
		Paths:      pathInfos,
		TotalPaths: len(pathInfos),
	})
}

// CredentialsResponse represents the S3 credentials info (does NOT expose the secret key).
type CredentialsResponse struct {
	AccessKeyID    string `json:"access_key_id"`
	BucketName     string `json:"bucket_name"`
	HasCredentials bool   `json:"has_credentials"`
	DataBackend    string `json:"data_backend"`
}

// GetCredentials handles GET /api/v1/credentials
func (h *Handlers) GetCredentials(c *gin.Context) {
	h.mu.RLock()
	kfMgr := h.keyfileMgr
	h.mu.RUnlock()

	if kfMgr == nil || !kfMgr.Exists() {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	kf, err := kfMgr.Load()
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to load keyfile",
			Code:    "KEYFILE_ERROR",
			Details: err.Error(),
		})
		return
	}

	resp := CredentialsResponse{
		HasCredentials: kf.HasS3Credentials(),
	}

	if kf.HasS3Credentials() {
		resp.AccessKeyID = kf.S3AccessKeyID
		resp.BucketName = kf.S3BucketName
		resp.DataBackend = "s3"
	} else {
		resp.DataBackend = "rest"
	}

	c.JSON(http.StatusOK, resp)
}

// RotateCredentials handles POST /api/v1/credentials/rotate
func (h *Handlers) RotateCredentials(c *gin.Context) {
	h.mu.RLock()
	kfMgr := h.keyfileMgr
	serverClient := h.serverClient
	h.mu.RUnlock()

	if kfMgr == nil || !kfMgr.Exists() {
		c.JSON(http.StatusPreconditionFailed, ErrorResponse{
			Error: "Not registered",
			Code:  "NOT_REGISTERED",
		})
		return
	}

	if serverClient == nil {
		c.JSON(http.StatusServiceUnavailable, ErrorResponse{
			Error:   "Server not available",
			Code:    "SERVER_NOT_CONNECTED",
			Details: "Cannot rotate credentials without server connection.",
		})
		return
	}

	// Load current keyfile
	kf, err := kfMgr.Load()
	if err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to load keyfile",
			Code:    "KEYFILE_ERROR",
			Details: err.Error(),
		})
		return
	}

	if !kf.HasS3Credentials() {
		c.JSON(http.StatusBadRequest, ErrorResponse{
			Error:   "No S3 credentials to rotate",
			Code:    "NO_S3_CREDENTIALS",
			Details: "This account uses legacy REST authentication. Credentials cannot be rotated.",
		})
		return
	}

	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer cancel()

	// Request rotation from server
	rotateResp, err := serverClient.RotateS3Credentials(ctx)
	if err != nil {
		c.JSON(http.StatusBadGateway, ErrorResponse{
			Error:   "Credential rotation failed",
			Code:    "ROTATION_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Update the keyfile with new secret
	if err := kf.UpdateS3Secret(rotateResp.SecretAccessKey); err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to update credentials",
			Code:    "UPDATE_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Save updated keyfile
	if err := kfMgr.Save(kf); err != nil {
		c.JSON(http.StatusInternalServerError, ErrorResponse{
			Error:   "Failed to save updated keyfile",
			Code:    "SAVE_FAILED",
			Details: err.Error(),
		})
		return
	}

	// Reinitialize restic engine with new credentials
	if err := h.reinitializeServices(kf, h.serverAddress); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"status":     "rotated",
			"access_key": rotateResp.AccessKeyID,
			"rotated_at": rotateResp.RotatedAt,
			"warning":    "Service re-initialization failed, restart may be required",
			"init_error": err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"status":     "rotated",
		"access_key": rotateResp.AccessKeyID,
		"rotated_at": rotateResp.RotatedAt,
	})
}

// GetRetention handles GET /api/v1/retention
func (h *Handlers) GetRetention(c *gin.Context) {
	h.mu.RLock()
	cfg := h.config
	h.mu.RUnlock()

	retention := config.DefaultRetentionConfig()
	if cfg != nil {
		retention = cfg.Backup.Retention
	}

	c.JSON(http.StatusOK, RetentionResponse{
		KeepWithinDaily:   retention.KeepWithinDaily,
		KeepWithinWeekly:  retention.KeepWithinWeekly,
		KeepWithinMonthly: retention.KeepWithinMonthly,
		KeepWithinYearly:  retention.KeepWithinYearly,
		Description: fmt.Sprintf(
			"Keep: all daily snapshots from last %s, weekly from last %s, monthly from last %s, yearly from last %s",
			retention.KeepWithinDaily,
			retention.KeepWithinWeekly,
			retention.KeepWithinMonthly,
			retention.KeepWithinYearly,
		),
	})
}
