using System.Collections.Concurrent;
using Deal.Grpc.Sources;
using Deal.Telegram.Core;
using Deal.Telegram.Sessions;
using Deal.Telegram.Telegram;
namespace Deal.Telegram.Dialogs;
///
/// Backfill диалога
///
public sealed class BackfillService
{
///
/// Сколько последних сообщений читает backfill.
///
public const int MessagesLimit = 10;
///
/// Нижняя граница анти-бан-паузы между сообщениями.
///
public const double MinPerMessageDelaySeconds = 1.5;
///
/// Верхняя граница анти-бан-паузы между сообщениями.
///
public const double MaxPerMessageDelaySeconds = 3.0;
///
/// Нижняя граница паузы между диалогами одного тенанта.
///
public const double MinPerDialogDelaySeconds = 3.0;
///
/// Верхняя граница паузы между диалогами одного тенанта.
///
public const double MaxPerDialogDelaySeconds = 6.0;
private readonly SessionFarm _sessionFarm;
private readonly ICoreIngressClient _ingress;
private readonly IBackfillPacer _pacer;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<(string TenantId, string DialogId), byte> _running = new();
private readonly ConcurrentDictionary _tenantGates = new(StringComparer.Ordinal);
// Момент завершения последнего backfill тенанта (пауза 3–6 с между диалогами).
private readonly Dictionary _tenantLastFinishUtc = new(StringComparer.Ordinal);
private readonly object _lastFinishGate = new();
///
/// Создаёт службу backfill'а.
///
/// Пул сессий тенантов (read-операции диалогов).
/// Канал в ядро (PushSource).
/// Анти-бан-паузы (реальный — случайные, тесты — фейк).
/// Логгер.
public BackfillService(
SessionFarm sessionFarm,
ICoreIngressClient ingress,
IBackfillPacer pacer,
ILogger logger)
{
_sessionFarm = sessionFarm;
_ingress = ingress;
_pacer = pacer;
_logger = logger;
}
///
/// Перечитывает последние сообщения диалога в ядро
///
/// Id тенанта.
/// Подписанный id диалога.
/// True — «Перечитать» по кнопке (семантика флага — в ядре, см. класс).
/// Отмена операции.
/// Сколько сообщений отправлено в ядро (0 — диалог уже перечитывается/нет текстов).
public async Task ExecuteAsync(
string tenantId,
string dialogId,
bool force,
CancellationToken cancellationToken)
{
var key = (tenantId, dialogId);
if (!_running.TryAdd(key, 0))
{
_logger.LogInformation("Аудит: backfill {TenantId} {DialogId} → пропущен (уже перечитывается)", tenantId, dialogId);
return 0;
}
try
{
SemaphoreSlim gate = _tenantGates.GetOrAdd(tenantId, static _ => new SemaphoreSlim(1, 1));
await gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await EnsureDialogSpacingAsync(tenantId, cancellationToken).ConfigureAwait(false);
int processed = await BackfillDialogAsync(tenantId, dialogId, cancellationToken).ConfigureAwait(false);
lock (_lastFinishGate)
{
_tenantLastFinishUtc[tenantId] = DateTime.UtcNow;
}
_logger.LogInformation(
"Аудит: backfill {TenantId} {DialogId} → {Processed} сообщений в ядро (force {Force})",
tenantId,
dialogId,
processed,
force);
return processed;
}
finally
{
gate.Release();
}
}
finally
{
_running.TryRemove(key, out _);
}
}
private async Task EnsureDialogSpacingAsync(string tenantId, CancellationToken cancellationToken)
{
DateTime? lastFinishUtc;
lock (_lastFinishGate)
{
lastFinishUtc = _tenantLastFinishUtc.TryGetValue(tenantId, out DateTime value) ? value : null;
}
if (lastFinishUtc is null)
{
return;
}
double elapsedSeconds = (DateTime.UtcNow - lastFinishUtc.Value).TotalSeconds;
if (elapsedSeconds >= MaxPerDialogDelaySeconds)
{
return;
}
double minSeconds = Math.Max(0, MinPerDialogDelaySeconds - elapsedSeconds);
double maxSeconds = MaxPerDialogDelaySeconds - elapsedSeconds;
if (maxSeconds < minSeconds)
{
maxSeconds = minSeconds;
}
await _pacer.WaitAsync(minSeconds, maxSeconds, cancellationToken).ConfigureAwait(false);
}
// Читает и отправляет сообщения одного диалога (паузы между сообщениями, read-ack).
// tenantId: Id тенанта.
// dialogId: Подписанный id диалога.
// cancellationToken: Отмена операции.
// Возвращает: Сколько сообщений отправлено в ядро.
private async Task BackfillDialogAsync(
string tenantId,
string dialogId,
CancellationToken cancellationToken)
{
IReadOnlyList messages = await _sessionFarm
.GetMessagesAsync(tenantId, dialogId, MessagesLimit, cancellationToken)
.ConfigureAwait(false);
int processed = 0;
foreach (TelegramMessage message in messages.Reverse())
{
PushSourceReply reply = await _ingress
.PushSourceAsync(tenantId, DialogProtoMapper.ToSourceRequest(message), cancellationToken)
.ConfigureAwait(false);
if (reply.Accepted && !reply.Duplicate)
{
processed++;
}
await _pacer.WaitAsync(MinPerMessageDelaySeconds, MaxPerMessageDelaySeconds, cancellationToken).ConfigureAwait(false);
}
await _sessionFarm.MarkReadAsync(tenantId, dialogId, cancellationToken).ConfigureAwait(false);
return processed;
}
}