package store

import (
	"context"
	"database/sql"
	"fmt"
	"sort"
	"strings"
)

// WipeCount is one table and the number of rows a wipe would remove from it.
type WipeCount struct {
	Table string
	Rows  int
}

// WipeCounts reports every table in the database and its row count, so a caller
// can show what a wipe is about to destroy before it destroys it. Tables with
// no rows are included: a QA reader needs to see that the table was considered,
// not guess whether it was missed.
func (s *Store) WipeCounts(ctx context.Context) ([]WipeCount, error) {
	names, err := userTables(ctx, s.db)
	if err != nil {
		return nil, err
	}
	out := make([]WipeCount, 0, len(names))
	for _, name := range names {
		var n int
		// name comes from sqlite_master, not from a caller, and is quoted.
		q := fmt.Sprintf(`SELECT count(*) FROM %s`, quoteIdent(name))
		if err := s.db.QueryRowContext(ctx, q).Scan(&n); err != nil {
			return nil, fmt.Errorf("count %s: %w", name, err)
		}
		out = append(out, WipeCount{Table: name, Rows: n})
	}
	return out, nil
}

// Wipe removes every piece of personal data and leaves the install standing.
//
// It drops every object in the database and rebuilds the schema from
// schema.sql plus the migration list, rather than deleting from a hand-written
// list of tables. That is the whole point: a list has to be edited every time a
// table is added, and the one time it is not, a wipe leaves personal data
// behind while reporting success. Dropping what the database says exists cannot
// miss a table (Vikunja #494).
//
// Nothing outside the database is touched. Config, models, the WebAuthn
// credentials in passkeys.json and the encryption key all live in files, so
// this leaves a working install with no memory of anyone.
//
// The caller is responsible for the daemon being the only thing holding the
// store, and for asking whoever ran it whether they meant it.
func (s *Store) Wipe(ctx context.Context) error {
	names, err := userTables(ctx, s.db)
	if err != nil {
		return err
	}
	// Foreign keys are on per connection. Dropping tables in an arbitrary order
	// trips them, and the drop order does not matter once everything goes.
	if _, err := s.db.ExecContext(ctx, `PRAGMA foreign_keys = OFF`); err != nil {
		return fmt.Errorf("wipe: disable foreign keys: %w", err)
	}
	defer func() {
		if _, err := s.db.ExecContext(ctx, `PRAGMA foreign_keys = ON`); err != nil {
			// The handle is about to be closed by the caller either way.
			_ = err
		}
	}()

	var drops strings.Builder
	for _, name := range names {
		fmt.Fprintf(&drops, "DROP TABLE IF EXISTS %s;\n", quoteIdent(name))
	}
	// user_version back to zero, or migrate() applies nothing to the fresh
	// schema and every ALTER that a later migration assumes is missing.
	drops.WriteString("PRAGMA user_version = 0;\n")
	if _, err := s.db.ExecContext(ctx, drops.String()); err != nil {
		return fmt.Errorf("wipe: drop tables: %w", err)
	}
	if _, err := s.db.ExecContext(ctx, schemaSQL); err != nil {
		return fmt.Errorf("wipe: reapply schema: %w", err)
	}
	if err := migrate(ctx, s.db); err != nil {
		return fmt.Errorf("wipe: re-migrate: %w", err)
	}
	// The file keeps the pages the dropped rows used until it is vacuumed, and
	// a wipe that leaves readable text in free pages is not a wipe.
	if _, err := s.db.ExecContext(ctx, `VACUUM`); err != nil {
		return fmt.Errorf("wipe: vacuum: %w", err)
	}
	return nil
}

// userTables lists every table the schema owns, sorted, excluding sqlite's own
// internal tables (sqlite_sequence and friends, which cannot be dropped).
func userTables(ctx context.Context, db *sql.DB) ([]string, error) {
	rows, err := db.QueryContext(ctx,
		`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`)
	if err != nil {
		return nil, fmt.Errorf("list tables: %w", err)
	}
	defer rows.Close()
	var names []string
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, fmt.Errorf("list tables: %w", err)
		}
		names = append(names, name)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list tables: %w", err)
	}
	sort.Strings(names)
	return names, nil
}

func quoteIdent(name string) string {
	return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
}
