// Command mciasdb is the MCIAS database maintenance tool.
//
// It operates directly on the SQLite file, bypassing the mciassrv API.
// Use it for break-glass recovery, offline inspection, schema verification,
// and maintenance tasks when the server is unavailable.
//
// mciasdb requires the same master key configuration as mciassrv (passphrase
// environment variable or keyfile) to decrypt secrets at rest.
//
// Usage:
//
//	mciasdb --config /srv/mcias/mcias.toml <command> [subcommand] [flags]
//
// Commands:
//
//	schema verify
//	schema migrate
//	schema force --version N
//
//	account list
//	account get     --id UUID
//	account create  --username NAME --type human|system
//	account set-password --id UUID
//	account set-status   --id UUID --status active|inactive|deleted
//	account reset-totp   --id UUID
//
//	role list   --id UUID
//	role grant  --id UUID --role ROLE
//	role revoke --id UUID --role ROLE
//
//	token list       --id UUID
//	token revoke     --jti JTI
//	token revoke-all --id UUID
//
//	prune tokens
//
//	audit tail  [--n N]
//	audit query [--account UUID] [--type TYPE] [--since RFC3339] [--json]
//
//	pgcreds get --id UUID
//	pgcreds set --id UUID --host H --port P --db D --user U
//
//	snapshot [--retain-days N]
package main

import (
	"errors"
	"flag"
	"fmt"
	"os"

	"git.wntrmute.dev/mc/mcias/internal/config"
	"git.wntrmute.dev/mc/mcias/internal/crypto"
	"git.wntrmute.dev/mc/mcias/internal/db"
)

func main() {
	configPath := flag.String("config", "/srv/mcias/mcias.toml", "path to TOML configuration file")
	flag.Usage = usage
	flag.Parse()

	args := flag.Args()
	if len(args) == 0 {
		usage()
		os.Exit(1)
	}

	command := args[0]
	subArgs := args[1:]

	// snapshot loads only the config (no master key needed — VACUUM INTO does
	// not access encrypted columns) and must be handled before openDB, which
	// requires the master key passphrase env var.
	if command == "snapshot" {
		runSnapshot(*configPath, subArgs)
		return
	}

	// schema subcommands manage migrations themselves and must not trigger
	// auto-migration on open (a dirty database would prevent the tool from
	// opening at all, blocking recovery operations like "schema force").
	var (
		database  *db.DB
		masterKey []byte
		err       error
	)
	if command == "schema" {
		database, masterKey, err = openDBRaw(*configPath)
	} else {
		database, masterKey, err = openDB(*configPath)
	}
	if err != nil {
		fatalf("%v", err)
	}
	defer func() {
		_ = database.Close()
		// Zero the master key when done to reduce the window of in-memory exposure.
		for i := range masterKey {
			masterKey[i] = 0
		}
	}()

	tool := &tool{db: database, masterKey: masterKey}

	switch command {
	case "schema":
		tool.runSchema(subArgs)
	case "account":
		tool.runAccount(subArgs)
	case "role":
		tool.runRole(subArgs)
	case "token":
		tool.runToken(subArgs)
	case "prune":
		tool.runPrune(subArgs)
	case "audit":
		tool.runAudit(subArgs)
	case "pgcreds":
		tool.runPGCreds(subArgs)
	case "webauthn":
		tool.runWebAuthn(subArgs)
	case "rekey":
		tool.runRekey(subArgs)
	default:
		fatalf("unknown command %q; run with no args for usage", command)
	}
}

// tool holds shared state for all subcommand handlers.
type tool struct {
	db        *db.DB
	masterKey []byte
}

// openDB loads the config, derives the master key, opens and migrates the DB.
//
// Security: Master key derivation uses the same logic as mciassrv so that
// the same passphrase always yields the same key and encrypted secrets remain
// readable. The passphrase env var is unset immediately after reading.
func openDB(configPath string) (*db.DB, []byte, error) {
	database, masterKey, err := openDBRaw(configPath)
	if err != nil {
		return nil, nil, err
	}
	if err := db.Migrate(database); err != nil {
		_ = database.Close()
		return nil, nil, fmt.Errorf("migrate database: %w", err)
	}
	return database, masterKey, nil
}

// openDBRaw opens the database without running migrations.  Used by schema
// subcommands so they remain operational even when the database is in a dirty
// migration state (e.g. to allow "schema force" to clear a dirty flag).
func openDBRaw(configPath string) (*db.DB, []byte, error) {
	cfg, err := config.Load(configPath)
	if err != nil {
		return nil, nil, fmt.Errorf("load config: %w", err)
	}

	database, err := db.Open(cfg.Database.Path)
	if err != nil {
		return nil, nil, fmt.Errorf("open database %q: %w", cfg.Database.Path, err)
	}

	masterKey, err := deriveMasterKey(cfg, database)
	if err != nil {
		_ = database.Close()
		return nil, nil, fmt.Errorf("derive master key: %w", err)
	}

	return database, masterKey, nil
}

// deriveMasterKey derives or loads the AES-256-GCM master key from config,
// using identical logic to mciassrv so that encrypted DB secrets are readable.
//
// Security: Key file must be exactly 32 bytes (AES-256). Passphrase is read
// from the environment variable named in cfg.MasterKey.PassphraseEnv and
// cleared from the environment immediately after. The Argon2id KDF salt is
// loaded from the database; if absent the DB has no encrypted secrets yet.
func deriveMasterKey(cfg *config.Config, database *db.DB) ([]byte, error) {
	if cfg.MasterKey.KeyFile != "" {
		data, err := os.ReadFile(cfg.MasterKey.KeyFile)
		if err != nil {
			return nil, fmt.Errorf("read key file: %w", err)
		}
		if len(data) != 32 {
			return nil, fmt.Errorf("key file must be exactly 32 bytes, got %d", len(data))
		}
		key := make([]byte, 32)
		copy(key, data)
		for i := range data {
			data[i] = 0
		}
		return key, nil
	}

	passphrase := os.Getenv(cfg.MasterKey.PassphraseEnv)
	if passphrase == "" {
		return nil, fmt.Errorf("environment variable %q is not set or empty", cfg.MasterKey.PassphraseEnv)
	}
	_ = os.Unsetenv(cfg.MasterKey.PassphraseEnv)

	salt, err := database.ReadMasterKeySalt()
	if errors.Is(err, db.ErrNotFound) {
		// No salt means the database has no encrypted secrets yet.
		// Generate a new salt so future writes are consistent.
		salt, err = crypto.NewSalt()
		if err != nil {
			return nil, fmt.Errorf("generate master key salt: %w", err)
		}
		if err := database.WriteMasterKeySalt(salt); err != nil {
			return nil, fmt.Errorf("store master key salt: %w", err)
		}
	} else if err != nil {
		return nil, fmt.Errorf("read master key salt: %w", err)
	}

	key, err := crypto.DeriveKey(passphrase, salt)
	if err != nil {
		return nil, fmt.Errorf("derive master key: %w", err)
	}
	return key, nil
}

// fatalf prints an error message to stderr and exits with code 1.
func fatalf(format string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, "mciasdb: "+format+"\n", args...)
	os.Exit(1)
}

// exitCode1 exits with code 1 without printing any message.
// Used when the message has already been printed.
func exitCode1() {
	os.Exit(1)
}

func usage() {
	fmt.Fprint(os.Stderr, `mciasdb - MCIAS database maintenance tool

Usage: mciasdb --config PATH <command> [subcommand] [flags]

Global flags:
  --config  Path to TOML config file (default: mcias.toml)

Commands:
  schema verify          Check schema version; exit 1 if migrations pending
  schema migrate         Apply any pending schema migrations
  schema force           --version N  Force schema version (clears dirty state)

  account list           List all accounts
  account get            --id UUID
  account create         --username NAME --type human|system
  account set-password   --id UUID  (prompts interactively)
  account set-status     --id UUID --status active|inactive|deleted
  account reset-totp     --id UUID
  account reset-webauthn --id UUID

  webauthn list          --id UUID
  webauthn delete        --id UUID --credential-id N
  webauthn reset         --id UUID

  role list              --id UUID
  role grant             --id UUID --role ROLE
  role revoke            --id UUID --role ROLE

  token list             --id UUID
  token revoke           --jti JTI
  token revoke-all       --id UUID

  prune tokens           Delete expired token_revocation rows

  audit tail             [--n N]  (default 50)
  audit query            [--account UUID] [--type TYPE] [--since RFC3339] [--json]

  pgcreds get            --id UUID
  pgcreds set            --id UUID --host H [--port P] --db D --user U
                         (password is prompted interactively)

  rekey                  Re-encrypt all secrets under a new master passphrase
                         (prompts interactively; requires server to be stopped)

  snapshot               Write a timestamped VACUUM INTO backup to
                         <db-dir>/backups/; prune backups older than
                         --retain-days days (default 30, 0 = keep all).
                         Does not require the master key passphrase.

NOTE: mciasdb bypasses the mciassrv API and operates directly on the SQLite
file. Use it only when the server is unavailable or for break-glass recovery.
All write operations are recorded in the audit log.
`)
}
