using System.Text.Json;
using Deal.Modules.Discovery.Application.Abstractions;
using Deal.Modules.Discovery.Application.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
namespace Deal.Modules.Discovery.Application.Services;
///
/// Бан-гард авто-вступлений Discovery
///
public sealed class DiscoveryBanGuard
{
private readonly IDiscoveryStore _store;
private readonly ISettingsStore _settings;
private readonly Func _utcNow;
///
/// Создаёт бан-гард.
///
/// Хранилище Discovery (счётчик DiscLog за сутки).
/// KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).
/// Источник текущего времени (UTC). По умолчанию — .
public DiscoveryBanGuard(
IDiscoveryStore store,
ISettingsStore settings,
Func? utcNow = null)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(settings);
_store = store;
_settings = settings;
_utcNow = utcNow ?? (() => DateTimeOffset.UtcNow);
}
///
/// Ручной стоп-кран авто-вступлений.
///
/// True — вступления на паузе (воркер не делает сетевых шагов).
public async Task GlobalPausedAsync(CancellationToken ct)
{
TenantSettingsSnapshot settingsSnapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return settingsSnapshot.GetBool(SettingsKeys.DiscPaused, SettingsDefaults.DiscPaused);
}
///
/// Авто-вступления за текущие UTC-сутки.
///
/// Число авто-вступлений сегодня (0 — вступлений нет).
public Task JoinsTodayAutoAsync(CancellationToken ct)
=> _store.CountLogEventAsync(DiscoveryLogEvents.JoinAuto, StartOfDayUtc(), ct);
///
/// Была ли flood-блокировка в текущие UTC-сутки.
///
/// True — фиксировалась сегодня (стоп до конца суток).
public async Task FloodTodayAsync(CancellationToken ct)
{
TenantSettingsSnapshot snapshot = await TenantSettingsSnapshot.LoadAsync(_settings, ct);
return IsFloodToday(snapshot.GetLong(SettingsKeys.DiscFloodDay, 0), StartOfDayMs());
}
///
/// Разрешено ли авто-вступление
///
/// 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
///
public Task NoteFloodAsync(CancellationToken ct)
=> _settings.SetAsync(SettingsKeys.DiscFloodDay, JsonSerializer.Serialize(StartOfDayMs()), ct);
private DateTimeOffset StartOfDayUtc()
{
DateTimeOffset now = _utcNow().ToUniversalTime();
return new DateTimeOffset(now.Year, now.Month, now.Day, 0, 0, 0, TimeSpan.Zero);
}
private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds();
private static bool IsFloodToday(long floodDay, long startOfDayMs)
=> floodDay != 0 && floodDay == startOfDayMs;
}