package store

import (
	"context"
	"fmt"
)

// checkpointQuery — пассивный чекпойнт журнала WAL.
//
// Режим PASSIVE, а не TRUNCATE или RESTART, и причина измерена: TRUNCATE
// двигает `data_version`, то есть каждый чекпойнт обнулял бы условный запрос у
// всех потребителей. PASSIVE не двигает его даже перенося 12502 страницы.
// Второй довод известнее: PASSIVE ничего не ждёт — ни читателей, ни писателей.
const checkpointQuery = `PRAGMA wal_checkpoint(PASSIVE)`

// pageSizeQuery — размер страницы базы. Свойство ФАЙЛА, зафиксированное при его
// создании, а не настройка соединения: журнал считается в страницах, а предел
// файла назван в байтах, и без этого числа их не связать.
const pageSizeQuery = `PRAGMA page_size`

// Checkpoint — исход одного чекпойнта: сколько страниц лежало в журнале и
// сколько из них перенесено в базу.
//
// Отдаётся без интерпретации: решение, считать ли это бедой, принимает не
// хранилище. Знать при этом надо обе величины, а не одну — признаком служит
// именно их расхождение.
type Checkpoint struct {
	// Busy — SQLite не смог взять блокировку чекпойнта. Признаком беды флаг НЕ
	// является: измерено `busy=0` при 6256 страницах в журнале и пяти
	// перенесённых — обработчик занятости в пассивном режиме не зовётся, и
	// «не продвинулись» флагом не выражается.
	//
	// Зато он выражает другое, и это обязано читаться: при `busy=1` SQLite
	// отдаёт `log = checkpointed = -1`, то есть исход НЕ ИЗМЕРЕН. Воспроизведено
	// шестью соединениями, чекпойнтящими один файл: 200 ответов `(1, -1, -1)`
	// против 40 измеренных.
	Busy bool
	// Log — страниц в журнале, Checkpointed — из них перенесено в базу.
	// Равенство означает, что журнал разобран целиком. Отрицательные значения
	// означают «не измерено» (см. Busy) и на шкале страниц не сравниваются.
	Log          int
	Checkpointed int
	// PageSize — размер страницы этой базы в байтах. Ноль означает **«не
	// измерено»** и нулём не является: без него страницы не перевести в байты,
	// и признак беды молчит, а не гадает.
	PageSize int
}

// Known отвечает, измерен ли исход вообще.
//
// SQLite отдаёт `-1` там, где ответа нет: чекпойнт не взял блокировку либо
// журнала не существует. Внутриполосный признак («-1 на шкале страниц») —
// ровно тот приём, который Effective Go называет неуклюжим, и он опасен
// буквально: `-1 >= -1` истинно, то есть незамеренный исход читался бы как
// «журнал разобран целиком», а владельцу уходила бы строка о выздоровлении
// посреди болезни, с числом, которого не бывает.
func (c Checkpoint) Known() bool { return !c.Busy && c.Log >= 0 && c.Checkpointed >= 0 }

// Complete отвечает, разобран ли журнал целиком. Неизмеренный исход
// разобранным не считается — «не знаем» и «разобран» разные ответы.
//
// Пассивный чекпойнт не идёт дальше снимка самого старого активного читателя и
// ошибки при этом не возвращает — растущий файл единственный след. Поэтому
// «журнал не разбирается» выражается здесь, а не флагом занятости.
func (c Checkpoint) Complete() bool { return c.Known() && c.Checkpointed >= c.Log }

// Stuck отвечает, перестал ли журнал разбираться: неразобранного накопилось
// больше, чем держит верхняя граница файла, и перенести это не вышло.
//
// Порог не своё число: это тот же `journal_size_limit`, только журнал считается
// в страницах, а предел назван в байтах. Одна величина в двух ролях (предел
// возвращает файл, порог сообщает, что вернуть его не выходит) — двумя
// константами они разъехались бы молча, сделав признак либо недостижимым, либо
// шумным.
//
// Размер страницы берётся у самой базы, а не предполагается: он фиксируется при
// создании файла, и база, созданная чужим инструментом с другим умолчанием,
// сместила бы порог в разы. Неизвестен — предикат молчит: гадать о пороге хуже,
// чем не сказать.
//
// Предикат живёт в хранилище, а не у вызывающего: семантика тройки
// `busy/log/checkpointed` принадлежит SQLite, и второй её экземпляр разошёлся
// бы с первым молча.
func (c Checkpoint) Stuck() bool {
	if !c.Known() || c.PageSize <= 0 {
		return false
	}
	return !c.Complete() && c.Log*c.PageSize >= journalSizeLimit
}

// CheckpointWAL переносит страницы журнала в базу и говорит, сколько удалось.
//
// Автоматический чекпойнт SQLite (`wal_autocheckpoint`, 1000 страниц) остаётся
// первой линией и отключать его незачем; этот вызов страхует случай, которого
// автоматический не закрывает по построению — запись прекратилась, а журнал
// остался неразобранным. Поток пачечный: ночью телефон молчит часами.
func (s *Store) CheckpointWAL(ctx context.Context) (Checkpoint, error) {
	var busy, logPages, checkpointed int
	if err := s.db.QueryRowContext(ctx, checkpointQuery).Scan(&busy, &logPages, &checkpointed); err != nil {
		return Checkpoint{}, fmt.Errorf("wal checkpoint: %w", err)
	}

	// Отказ ЭТОГО запроса отказом чекпойнта не является: страницы уже
	// перенесены, и объявить это провалом значило бы отправить владельца искать
	// беду в чекпойнте. Неизвестный размер страницы предикат и так трактует как
	// «не измерено» и молчит.
	var pageSize int
	if err := s.db.QueryRowContext(ctx, pageSizeQuery).Scan(&pageSize); err != nil {
		pageSize = 0
	}
	return Checkpoint{Busy: busy != 0, Log: logPages, Checkpointed: checkpointed, PageSize: pageSize}, nil
}
