package lib

import (
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

// BackupInfo describes a single backup file.
type BackupInfo struct {
	Name      string `json:"name"`
	Size      int64  `json:"size"`
	CreatedAt int64  `json:"created_at"` // unix seconds
}

// BackupDir returns the backup directory for a given data dir, creating it if needed.
func BackupDir(dataDir string) string {
	dir := filepath.Join(dataDir, "backups")
	os.MkdirAll(dir, 0700)
	return dir
}

// RunBackups backs up vaults whose ID falls in this hour's byte range.
// First byte of vault ID (0x00–0xFF) is mapped across the 168 hours of a week,
// so a POP with thousands of vaults spreads the work evenly.
// Retention: delete backups older than 3 months.
func RunBackups(dataDir string) {
	backupDir := BackupDir(dataDir)
	now := time.Now()

	// Current hour of the week: Sun 0:00 = 0, Sat 23:00 = 167
	weekHour := int(now.Weekday())*24 + now.Hour()

	// This hour's first-byte range: [lo, hi)
	lo := weekHour * 256 / 168
	hi := (weekHour + 1) * 256 / 168

	dbs, _ := filepath.Glob(filepath.Join(dataDir, "*.db"))
	for _, dbPath := range dbs {
		name := strings.TrimSuffix(filepath.Base(dbPath), ".db")
		if len(name) < 2 {
			continue
		}
		b, err := hex.DecodeString(name[:2])
		if err != nil {
			continue
		}
		slot := int(b[0])
		if slot < lo || slot >= hi {
			continue
		}

		// Skip if already backed up in the last 6 days
		if hasRecentBackup(backupDir, name, 6*24*time.Hour) {
			continue
		}

		createBackup(dbPath, backupDir, now)
	}

	pruneBackups(backupDir, 90*24*time.Hour)
}

// hasRecentBackup checks if a vault has a backup newer than maxAge.
func hasRecentBackup(backupDir, vaultID string, maxAge time.Duration) bool {
	files, _ := filepath.Glob(filepath.Join(backupDir, vaultID+"_*.db"))
	for _, f := range files {
		info, err := os.Stat(f)
		if err != nil {
			continue
		}
		if time.Since(info.ModTime()) < maxAge {
			return true
		}
	}
	return false
}

// createBackup copies a DB using VACUUM INTO (consistent, compacted snapshot).
func createBackup(dbPath, backupDir string, now time.Time) error {
	name := strings.TrimSuffix(filepath.Base(dbPath), ".db")
	stamp := now.Format("20060102-150405")
	dest := filepath.Join(backupDir, fmt.Sprintf("%s_%s.db", name, stamp))

	src, err := OpenDB(dbPath)
	if err != nil {
		return err
	}
	defer src.Close()

	_, err = src.Conn.Exec(fmt.Sprintf("VACUUM INTO '%s'", dest))
	return err
}

// pruneBackups deletes all backup files older than maxAge.
func pruneBackups(backupDir string, maxAge time.Duration) {
	files, _ := filepath.Glob(filepath.Join(backupDir, "*.db"))
	cutoff := time.Now().Add(-maxAge)
	for _, f := range files {
		info, err := os.Stat(f)
		if err != nil {
			continue
		}
		if info.ModTime().Before(cutoff) {
			os.Remove(f)
		}
	}
}

// ListBackups returns all backup files sorted newest first.
func ListBackups(dataDir string) []BackupInfo {
	backupDir := BackupDir(dataDir)
	files, _ := filepath.Glob(filepath.Join(backupDir, "*.db"))

	var backups []BackupInfo
	for _, f := range files {
		info, err := os.Stat(f)
		if err != nil {
			continue
		}
		backups = append(backups, BackupInfo{
			Name:      filepath.Base(f),
			Size:      info.Size(),
			CreatedAt: info.ModTime().Unix(),
		})
	}

	sort.Slice(backups, func(i, j int) bool {
		return backups[i].CreatedAt > backups[j].CreatedAt
	})
	return backups
}

// RestoreBackup replaces the active DB with a backup file.
// The current DB is saved as a pre-restore backup first.
func RestoreBackup(dataDir, backupName string) error {
	backupDir := BackupDir(dataDir)
	backupPath := filepath.Join(backupDir, backupName)

	if strings.Contains(backupName, "/") || strings.Contains(backupName, "\\") || strings.Contains(backupName, "..") {
		return fmt.Errorf("invalid backup name")
	}
	if _, err := os.Stat(backupPath); err != nil {
		return fmt.Errorf("backup not found")
	}

	parts := strings.SplitN(backupName, "_", 2)
	if len(parts) < 2 {
		return fmt.Errorf("invalid backup name format")
	}
	dbPath := filepath.Join(dataDir, parts[0]+".db")

	// Save current DB as pre-restore backup
	if _, err := os.Stat(dbPath); err == nil {
		preRestore := filepath.Join(backupDir, fmt.Sprintf("%s_prerestore_%s.db", parts[0], time.Now().Format("20060102-150405")))
		src, err := OpenDB(dbPath)
		if err == nil {
			src.Conn.Exec(fmt.Sprintf("VACUUM INTO '%s'", preRestore))
			src.Close()
		}
	}

	data, err := os.ReadFile(backupPath)
	if err != nil {
		return fmt.Errorf("read backup: %w", err)
	}
	return os.WriteFile(dbPath, data, 0600)
}

// StartBackupTimer checks for due backups every hour.
func StartBackupTimer(dataDir string) {
	go func() {
		for {
			RunBackups(dataDir)
			time.Sleep(1 * time.Hour)
		}
	}()
}
