// Command mavseal encrypts a live tmpfs working copy back to the ciphertext
// file, for the case mavend could not do it itself.
//
// mavend seals its database in `defer st.Close()` when run() returns. A daemon
// that is killed rather than shut down never gets there, and because the
// working copy lives in the container's /dev/shm it dies with the container:
// everything written since the last clean shutdown is lost, and the next boot
// silently rolls back to the stale ciphertext. That is not hypothetical — on
// 2026-08-01 the deployed ciphertext was eleven days old.
//
// This is a recovery tool, not part of the daemon. It is safe to run against a
// live database: it takes a consistent snapshot with VACUUM INTO rather than
// mutating the working copy the daemon owns.
//
// Usage:
//
//	mavseal -plain /dev/shm/maven-plain.db -cipher /var/lib/maven/maven.db.enc
//
// The key is read from MAVEN_DB_KEY (base64, 32 bytes decoded), the same
// variable the daemon uses. It is never taken as an argument: an argument ends
// up in the shell history and in ps.
package main

import (
	"context"
	"database/sql"
	"encoding/base64"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/kami/maven/internal/store"
	_ "modernc.org/sqlite"
)

func main() {
	log.SetFlags(0)
	if err := run(); err != nil {
		log.Fatalf("mavseal: %v", err)
	}
}

func run() error {
	plain := flag.String("plain", "", "path to the plaintext working copy (required)")
	cipher := flag.String("cipher", "", "path to write the ciphertext to (required)")
	keep := flag.Bool("keep-snapshot", false, "leave the intermediate snapshot on disk for inspection")
	flag.Parse()

	if *plain == "" || *cipher == "" {
		flag.Usage()
		return fmt.Errorf("both -plain and -cipher are required")
	}
	key, err := readKey()
	if err != nil {
		return err
	}

	// VACUUM INTO rather than a WAL checkpoint on the file itself. The daemon
	// is usually still running and still writing when this is needed, and
	// checkpointing its working copy mutates a database it owns. VACUUM INTO
	// reads a consistent snapshot into a new file and touches nothing else, so
	// the worst case is a snapshot a few seconds stale instead of a torn one.
	snap := *plain + ".mavseal-snapshot"
	os.Remove(snap)
	if err := snapshot(*plain, snap); err != nil {
		return err
	}
	if !*keep {
		defer os.Remove(snap)
	}

	before := fileSize(*cipher)
	if err := store.SealPlaintext(snap, *cipher, key); err != nil {
		return err
	}
	log.Printf("sealed %s → %s (%d bytes, was %d)", *plain, *cipher, fileSize(*cipher), before)
	return nil
}

// readKey pulls the same base64 key the daemon reads. Fails closed: a short or
// unparseable key must not silently produce a file nothing can open.
func readKey() ([]byte, error) {
	raw := os.Getenv("MAVEN_DB_KEY")
	if raw == "" {
		return nil, fmt.Errorf("MAVEN_DB_KEY is not set")
	}
	key, err := base64.StdEncoding.DecodeString(raw)
	if err != nil {
		return nil, fmt.Errorf("MAVEN_DB_KEY is not valid base64: %w", err)
	}
	if len(key) != 32 {
		return nil, fmt.Errorf("MAVEN_DB_KEY decodes to %d bytes, want 32", len(key))
	}
	return key, nil
}

// snapshot writes a consistent copy of src to dst with VACUUM INTO. The copy
// includes everything committed to the write-ahead log, which is most of what
// is worth saving on a daemon that has been up for hours.
func snapshot(src, dst string) error {
	db, err := sql.Open("sqlite", src)
	if err != nil {
		return fmt.Errorf("open working copy: %w", err)
	}
	defer db.Close()
	if _, err := db.ExecContext(context.Background(), "VACUUM INTO ?", dst); err != nil {
		return fmt.Errorf("snapshot: %w", err)
	}
	return nil
}

func fileSize(path string) int64 {
	fi, err := os.Stat(path)
	if err != nil {
		return 0
	}
	return fi.Size()
}
