using System.Text.Json;
using Deal.Modules.Settings.Application;
namespace Deal.Modules.Discovery.Application;
///
/// Бан-гард авто-вступлений Discovery: суточный лимит, flood-день, стоп-кран (1:1 ban_guard.py L1–81, план Task 18, Ruling 10).
///
///
/// Суточный лимит (discJoinLimit, дефолт 50) считается по DiscLog: число событий event='join_auto' за текущие
/// UTC-сутки (дискретность совпадает с логом mark_joined авто-вступления — дискретный счётчик 1:1 с python
/// L29–35). При FloodWait от Telegram воркер зовёт — блокировка авто-вступлений до
/// конца суток (внутренний ключ SettingsKeys.DiscFloodDay, epoch-ms начала суток). Стоп-кран discPaused —
/// ручная пауза вступлений. — общий вердикт «разрешено ли следующее авто-
/// вступление». Настройки читаются типизированным снимком TenantSettingsSnapshot (C30): отсутствие
/// строки/повреждение → SettingsDefaults.
///
/// Паузу между вступлениями (discJoinDelayMin..Max, 50–70 с) гард НЕ держит — её делает
/// (рандом недоступен тестам; 1:1 ban_guard.wait_join_delay L44–56).
///
///
public sealed class DiscoveryBanGuard
{
private readonly IDiscoveryStore _store;
private readonly ISettingsStore _settings;
private readonly Func _utcNow;
///
/// Создаёт бан-гард.
///
/// Хранилище Discovery (счётчик DiscLog за сутки).
/// KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).
/// Источник текущего времени (UTC; тесты передают фиксированные «часы», эталон
/// FakeDiscoveryStore). По умолчанию — .
public DiscoveryBanGuard(IDiscoveryStore store, ISettingsStore settings, Func? utcNow = null)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(settings);
_store = store;
_settings = settings;
_utcNow = utcNow ?? (() => DateTimeOffset.UtcNow);
}
///
/// Ручной стоп-кран авто-вступлений (setting discPaused; ban_guard.global_paused L69–71).
///
/// Токен отмены.
/// True — вступления на паузе (воркер не делает сетевых шагов).
public async Task GlobalPausedAsync(CancellationToken ct)
{
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return settingsSnapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
}
///
/// Авто-вступления за текущие UTC-сутки (ban_guard.joins_today_auto L29–35).
///
/// События DiscLog event='join_auto' с CreatedAt ≥ начала текущих UTC-суток (все тенанты держат
/// времена UTC; счётчик учитывает и строки ручных вступлений с auto=false? — нет: фильтр по событию join_auto).
/// Токен отмены.
/// Число авто-вступлений сегодня (0 — вступлений нет).
public Task JoinsTodayAutoAsync(CancellationToken ct)
=> _store.CountLogEventAsync(DiscoveryLogEvents.JoinAuto, StartOfDayUtc(), ct);
///
/// Была ли flood-блокировка в текущие UTC-сутки (ban_guard.flood_today L64–66).
///
/// Токен отмены.
/// True — фиксировалась сегодня (стоп до конца суток).
public async Task FloodTodayAsync(CancellationToken ct)
{
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
}
///
/// Разрешено ли авто-вступление: лимит не исчерпан, нет flood на сегодня, нет стоп-крана
/// (ban_guard.can_auto_join L38–41).
///
/// Токен отмены.
/// True — следующее авто-вступление допустимо.
public async Task CanAutoJoinAsync(CancellationToken ct)
{
// Один типизированный снимок настроек на вердикт (C30): discJoinLimit/discFloodDay/discPaused
// читаются одним GetAllAsync вместо трёх GetAsync.
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
int limit = snapshot.GetInt(SettingsKeys.DiscJoinLimit, SettingsDefaults.DiscJoinLimit);
int joined = await JoinsTodayAutoAsync(ct).ConfigureAwait(false);
bool floodToday = IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
bool paused = snapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
return joined < Math.Max(1, limit) && !floodToday && !paused;
}
///
/// Фиксирует FloodWait: блокировка авто-вступлений до конца суток (ban_guard.note_flood L59–62).
///
/// Запись идемпотентна в течение суток: DiscFloodDay = epoch-ms начала текущих UTC-суток
/// (повторный note_flood в тот же день перезаписывает тем же значением).
/// Токен отмены.
public Task NoteFloodAsync(CancellationToken ct)
=> _settings.SetAsync(SettingsKeys.DiscFloodDay, JsonSerializer.Serialize(StartOfDayMs()), ct);
// Начало текущих UTC-суток (python _start_of_day_ms L23–26).
private DateTimeOffset StartOfDayUtc()
{
DateTimeOffset now = _utcNow().ToUniversalTime();
return new DateTimeOffset(now.Year, now.Month, now.Day, 0, 0, 0, TimeSpan.Zero);
}
// Начало текущих UTC-суток в epoch-ms (python L25).
private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds();
// Flood-день совпал с текущими сутками (ban_guard.flood_today L64–66: 0 — блокировок не было).
// floodDay: Epoch-ms начала суток flood-блокировки (0 — не было).
// startOfDayMs: Начало текущих UTC-суток в epoch-ms.
// Возвращает: True — flood фиксировался сегодня.
private static bool IsFloodToday(long floodDay, long startOfDayMs)
=> floodDay != 0 && floodDay == startOfDayMs;
}