package backup

import (
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/alexei/docker-watcher/internal/store"
)

// Engine manages database backup operations.
type Engine struct {
	mu        sync.Mutex
	store     *store.Store
	dbPath    string
	backupDir string
}

// New creates a new backup engine. It ensures the backup directory exists.
func New(st *store.Store, dbPath, dataDir string) (*Engine, error) {
	backupDir := filepath.Join(dataDir, "backups")
	if err := os.MkdirAll(backupDir, 0o755); err != nil {
		return nil, fmt.Errorf("create backup directory: %w", err)
	}
	return &Engine{
		store:     st,
		dbPath:    dbPath,
		backupDir: backupDir,
	}, nil
}

// BackupDir returns the path to the backup directory.
func (e *Engine) BackupDir() string {
	return e.backupDir
}

// CreateBackup creates a new database backup using VACUUM INTO.
// Returns the backup metadata record.
func (e *Engine) CreateBackup(backupType string) (store.Backup, error) {
	// Validate backup type to prevent path traversal via filename.
	switch backupType {
	case "manual", "auto", "pre-restore":
		// valid
	default:
		return store.Backup{}, fmt.Errorf("invalid backup type: %q", backupType)
	}

	e.mu.Lock()
	defer e.mu.Unlock()

	timestamp := time.Now().UTC().Format("20060102-150405")
	filename := fmt.Sprintf("docker-watcher-%s-%s.db", backupType, timestamp)
	destPath := filepath.Join(e.backupDir, filename)

	// VACUUM INTO creates a clean, standalone copy of the database.
	// It is safe to use while the database is open and in WAL mode.
	_, err := e.store.DB().Exec(`VACUUM INTO ?`, destPath)
	if err != nil {
		return store.Backup{}, fmt.Errorf("vacuum into %s: %w", destPath, err)
	}

	// Get file size.
	info, err := os.Stat(destPath)
	if err != nil {
		return store.Backup{}, fmt.Errorf("stat backup file: %w", err)
	}

	// Store metadata.
	backup, err := e.store.CreateBackup(store.Backup{
		Filename:   filename,
		SizeBytes:  info.Size(),
		BackupType: backupType,
	})
	if err != nil {
		// Best effort: remove the file if metadata insert fails.
		os.Remove(destPath)
		return store.Backup{}, fmt.Errorf("store backup metadata: %w", err)
	}

	slog.Info("backup created", "id", backup.ID, "filename", filename, "size", info.Size(), "type", backupType)
	return backup, nil
}

// ListBackups returns all backup records.
func (e *Engine) ListBackups() ([]store.Backup, error) {
	return e.store.ListBackups()
}

// GetBackup returns a single backup record.
func (e *Engine) GetBackup(id string) (store.Backup, error) {
	return e.store.GetBackup(id)
}

// FilePath returns the full filesystem path for a backup.
func (e *Engine) FilePath(backup store.Backup) string {
	return filepath.Join(e.backupDir, backup.Filename)
}

// DeleteBackup removes a backup file and its metadata record.
func (e *Engine) DeleteBackup(id string) error {
	backup, err := e.store.GetBackup(id)
	if err != nil {
		return fmt.Errorf("get backup: %w", err)
	}

	// Remove file.
	filePath := filepath.Join(e.backupDir, backup.Filename)
	if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("remove backup file: %w", err)
	}

	// Remove metadata.
	if err := e.store.DeleteBackup(id); err != nil {
		return fmt.Errorf("delete backup metadata: %w", err)
	}

	slog.Info("backup deleted", "id", id, "filename", backup.Filename)
	return nil
}

// RestorePath returns the path of a backup file for restore operations.
// The caller is responsible for actually replacing the database.
func (e *Engine) RestorePath(id string) (string, error) {
	backup, err := e.store.GetBackup(id)
	if err != nil {
		return "", fmt.Errorf("get backup: %w", err)
	}

	filePath := filepath.Join(e.backupDir, backup.Filename)
	if _, err := os.Stat(filePath); err != nil {
		return "", fmt.Errorf("backup file not found: %w", err)
	}

	return filePath, nil
}

// Prune removes old backups exceeding the retention count.
// Returns the number of backups pruned.
func (e *Engine) Prune(retentionCount int) (int, error) {
	if retentionCount <= 0 {
		return 0, nil
	}

	count, err := e.store.CountBackups()
	if err != nil {
		return 0, fmt.Errorf("count backups: %w", err)
	}

	excess := count - retentionCount
	if excess <= 0 {
		return 0, nil
	}

	oldest, err := e.store.GetOldestBackups(excess)
	if err != nil {
		return 0, fmt.Errorf("get oldest backups: %w", err)
	}

	pruned := 0
	for _, b := range oldest {
		if err := e.DeleteBackup(b.ID); err != nil {
			slog.Warn("prune: failed to delete backup", "id", b.ID, "error", err)
			continue
		}
		pruned++
	}

	if pruned > 0 {
		slog.Info("backups pruned", "pruned", pruned, "retention", retentionCount)
	}
	return pruned, nil
}

// CleanOrphans removes backup files in the backup directory that have no metadata record.
func (e *Engine) CleanOrphans() (int, error) {
	entries, err := os.ReadDir(e.backupDir)
	if err != nil {
		return 0, fmt.Errorf("read backup directory: %w", err)
	}

	backups, err := e.store.ListBackups()
	if err != nil {
		return 0, fmt.Errorf("list backups: %w", err)
	}

	tracked := make(map[string]bool, len(backups))
	for _, b := range backups {
		tracked[b.Filename] = true
	}

	cleaned := 0
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		if !tracked[entry.Name()] {
			filePath := filepath.Join(e.backupDir, entry.Name())
			if err := os.Remove(filePath); err != nil {
				slog.Warn("clean orphan: failed to remove file", "file", entry.Name(), "error", err)
				continue
			}
			slog.Info("removed orphaned backup file", "file", entry.Name())
			cleaned++
		}
	}
	return cleaned, nil
}
