// Package backup is the Phase 15 backup pipeline. SQLite is backed up via
// VACUUM INTO; the resulting file is optionally age-encrypted to the
// operator's identity and uploaded to S3-compatible storage.
package backup

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"filippo.io/age"
)

// LocalBackup runs `VACUUM INTO` to dest and returns the resulting size.
// dest's parent directory is created with mode 0o700.
func LocalBackup(ctx context.Context, db *sql.DB, dest string) (int64, error) {
	if dest == "" {
		return 0, errors.New("backup: empty destination")
	}
	if err := os.MkdirAll(filepath.Dir(dest), 0o700); err != nil {
		return 0, err
	}
	if _, err := db.ExecContext(ctx, "VACUUM INTO ?", dest); err != nil {
		return 0, fmt.Errorf("backup: VACUUM INTO: %w", err)
	}
	info, err := os.Stat(dest)
	if err != nil {
		return 0, err
	}
	return info.Size(), nil
}

// Encrypt age-encrypts srcPath to dstPath using the operator's identity.
// Reads the recipient from the identity file (the public key embedded as a
// comment, or derived from the X25519 private).
func Encrypt(srcPath, dstPath, identityPath string) error {
	identityFile, err := os.Open(identityPath)
	if err != nil {
		return err
	}
	defer identityFile.Close()
	ids, err := age.ParseIdentities(identityFile)
	if err != nil {
		return err
	}
	if len(ids) == 0 {
		return errors.New("backup: no identities in file")
	}
	x, ok := ids[0].(*age.X25519Identity)
	if !ok {
		return errors.New("backup: identity type not supported (need X25519)")
	}
	rec := x.Recipient()

	src, err := os.Open(srcPath)
	if err != nil {
		return err
	}
	defer src.Close()
	dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		return err
	}
	defer dst.Close()
	w, err := age.Encrypt(dst, rec)
	if err != nil {
		return err
	}
	if _, err := io.Copy(w, src); err != nil {
		_ = w.Close()
		return err
	}
	if err := w.Close(); err != nil {
		return err
	}
	return nil
}

// Prune keeps the most recent `keep` files matching pattern under dir.
// Returns the number of files deleted.
func Prune(dir, glob string, keep int) (int, error) {
	if keep <= 0 {
		return 0, nil
	}
	files, err := filepath.Glob(filepath.Join(dir, glob))
	if err != nil {
		return 0, err
	}
	if len(files) <= keep {
		return 0, nil
	}
	// Sort newest-first by mtime.
	type fileInfo struct {
		path string
		mod  time.Time
	}
	infos := make([]fileInfo, 0, len(files))
	for _, p := range files {
		st, err := os.Stat(p)
		if err != nil {
			continue
		}
		infos = append(infos, fileInfo{p, st.ModTime()})
	}
	sort.Slice(infos, func(i, j int) bool { return infos[i].mod.After(infos[j].mod) })
	toDelete := infos[keep:]
	for _, f := range toDelete {
		_ = os.Remove(f.path)
	}
	return len(toDelete), nil
}

// FormatTimestampedName returns a basename like "command-center-2026-05-18T12:30:45.db".
func FormatTimestampedName(prefix, ext string, t time.Time) string {
	if ext != "" && !strings.HasPrefix(ext, ".") {
		ext = "." + ext
	}
	return prefix + "-" + t.UTC().Format("2006-01-02T15-04-05") + ext
}
