using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Settings.Application;
namespace Deal.Modules.Kanban.Application;
///
/// Сервис правил хранения: автоархив и очистка архива/корзины по срокам — Ruling 8, план Task 10.
///
///
/// Чистый сервис модуля (без EF/HTTP), повторяет leads.py tick_storage (L454–493). Настройки
/// autoArchive/archiveAfterDays/archiveClearDays/trashClearDays читает типизированным снимком
/// (C30: один GetAllAsync на тик; отсутствие/повреждение → дефолт из
/// SettingsDefaults). Один «now»
/// (UTC) на весь тик, как в прототипе. Последовательность 1:1 с tick_storage:
/// (1) автоархив — карточки досок и «Неразобранного» со ReceivedAt старше archiveAfterDays → col=archive,
/// is_new=false, archived_at=now (PrevCol НЕ трогается — Ruling 8: null в CardColumnUpdateDto), пачкой
/// (один UPDATE вместо N);
/// (2) очистка архива — col='archive' с ArchivedAt старше archiveClearDays (дефолт 90) → жёсткое удаление;
/// (3) очистка корзины — col='trash' с ReceivedAt старше trashClearDays (дефолт 7) → жёсткое удаление.
/// Удаление — пачкой (Cards + комментарии каскадом; журнал CardMoves
/// и MlOutbox не трогаются, Ruling 10). purgedRejected — отсев пайплайна (этап 4); в этапе 3 всегда 0.
/// SSE-тосты сервис НЕ публикует: это обязанность эндпоинтов Api (Ruling 5) по статистике TickAsync.
///
/// Порт хранилища канбана: кандидаты тика, batch-перенос в архив, жёсткое удаление пачки.
/// KV-хранилище настроек тенанта (снимок правил хранения, Ruling 8).
public sealed class StorageTickService(ICardStore store, ISettingsStore settings)
{
///
/// Один тик правил хранения тенанта (Ruling 8; leads.py tick_storage L454–493).
///
/// Токен отмены.
/// Статистика тика: сколько карточек архивировано/очищено из архива и корзины (purgedRejected=0).
public async Task TickAsync(CancellationToken ct)
{
// Все правила хранения — из одного типизированного снимка настроек (C30): один GetAllAsync на тик.
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(settings, ct);
bool autoArchive = settingsSnapshot.GetBool(SettingsKeys.AutoArchive, SettingsDefaults.AutoArchive);
int archiveAfterDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveAfterDays, SettingsDefaults.ArchiveAfterDays);
int archiveClearDays = settingsSnapshot.GetInt(SettingsKeys.ArchiveClearDays, SettingsDefaults.ArchiveClearDays);
int trashClearDays = settingsSnapshot.GetInt(SettingsKeys.TrashClearDays, SettingsDefaults.TrashClearDays);
// Один «now» на весь тик — границы автоархива и очисток от одного момента (tick_storage L459).
DateTimeOffset now = DateTimeOffset.UtcNow;
int archived = 0;
if (autoArchive)
{
// Автоархив: карточки досок и «Неразобранного» (tick_storage L462–467). В архив — пачкой одним
// UPDATE (архивация как системный перенос: col=archive, is_new=false, archived_at=now, matchHits
// пусто; prev_col не трогается, Ruling 8); по-карточные UPDATE'ы тика заменены на batch.
IReadOnlyList candidates = await store.ListArchiveCandidatesAsync(
now.AddDays(-archiveAfterDays), ct);
archived = candidates.Count == 0 ? 0 : await store.ArchiveAsync(candidates, now, ct);
}
// Очистка архива по сроку хранения (tick_storage L475–478).
int purgedArchive = await PurgeAsync(
await store.ListExpiredArchiveCandidatesAsync(now.AddDays(-archiveClearDays), ct), ct);
// Очистка корзины по сроку хранения (tick_storage L480–483).
int purgedTrash = await PurgeAsync(
await store.ListTrashCandidatesAsync(now.AddDays(-trashClearDays), ct), ct);
// purgedRejected — отсев пайплайна живёт в Pipeline (этап 4); в этапе 3 всегда 0 (Ruling 8).
return new StorageTickStatsDto(archived, purgedArchive, purgedTrash, PurgedRejected: 0);
}
// Жёстко удаляет пачку кандидатов очистки (Ruling 8; KanbanStore.PurgeAsync — комментарии каскадом).
// cardIds: Id карточек-кандидатов из выборки хранилища.
// ct: Токен отмены.
// Возвращает: Сколько карточек реально удалено (0 — кандидатов не было).
private async Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct)
{
return cardIds.Count == 0 ? 0 : await store.PurgeAsync(cardIds, ct);
}
}