using System.Collections.ObjectModel;
namespace Deal.Telegram.Dialogs;
///
/// Зеркало каталога диалогов тенанта в памяти telegram-service (план Task 10, Dialogs/DialogCatalog.cs;
/// Ruling 7: «сервис держит зеркало мониторинга в памяти», ядро — владелец списка мониторинга в своей БД).
///
/// Тенант хранит два набора id диалогов: полный каталог (из refresh_dialogs/realtime_sweep, L468–519)
/// и подмножество с включённым мониторингом. Мониторинг актуализируется тремя путями:
/// * командой SetMonitor/SetMonitorAll ядра (обновление одной записи / всех сразу);
/// * ответом Ingress.SyncDialogs (ядро применило entries с autoMonitorNew — L244–246 «_reload_monitored»);
/// * очисткой после Logout/отключения аккаунта (прототип L203: `_monitored.clear()`).
/// Все операции потокобезопасны; зеркало чисто в памяти — потерю при рестарте догоняет realtime_sweep.
///
public sealed class DialogCatalog
{
private readonly object _gate = new();
private readonly Dictionary> _knownByTenant = new(StringComparer.Ordinal);
private readonly Dictionary> _monitoredByTenant = new(StringComparer.Ordinal);
///
/// Обновляет полный каталог диалогов тенанта (entries refresh/realtime_sweep). Мониторинг при этом
/// не трогается — актуальный monitored-набор приходит ответом SyncDialogs (или командами SetMonitor).
///
/// Id тенанта.
/// Актуальные id каталога (entries списка диалогов).
public void ReplaceKnown(string tenantId, IEnumerable dialogIds)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
lock (_gate)
{
_knownByTenant[tenantId] = new HashSet(dialogIds, StringComparer.Ordinal);
}
}
///
/// Включает/выключает мониторинг одного диалога (SetMonitor RPC, set_monitor L536–546). Команда
/// приходит от ядра после обновления его БД; зеркало повторяет решение без собственной проверки
/// «известности» — запись может появиться раньше каталога (включение после рестарта сервиса).
///
/// Id тенанта.
/// Id диалога.
/// Мониторить (true) или выключить (false).
public void SetMonitored(
string tenantId,
string dialogId,
bool enabled)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(dialogId);
lock (_gate)
{
HashSet monitored = MonitoredSet(tenantId);
if (enabled)
{
monitored.Add(dialogId);
}
else
{
monitored.Remove(dialogId);
}
}
}
///
/// Заменяет monitored-набор ответом SyncDialogs (актуальный список ядра после SyncFromTelegram —
/// авто-мониторинг новых/удаление отсутствующих, Ruling 7). Это авторитетный источник зеркала.
///
/// Id тенанта.
/// Id диалогов с включённым мониторингом.
public void ReplaceMonitored(string tenantId, IEnumerable monitoredIds)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
lock (_gate)
{
_monitoredByTenant[tenantId] = new HashSet(monitoredIds, StringComparer.Ordinal);
}
}
///
/// Включает/выключает мониторинг всех диалогов каталога (SetMonitorAll RPC, set_monitor_all
/// L548–567). При включении учитываются и уже мониторящиеся записи (каталог может отставать от БД
/// ядра после рестарта — «монитор» из ответа SyncDialogs приедет следующим циклом realtime_sweep).
///
/// Id тенанта.
/// Мониторить все (true) или снять мониторинг со всех (false).
/// Сколько диалогов в каталоге тенанта (SetMonitorAllReply.count).
public int SetAllMonitored(string tenantId, bool enabled)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
lock (_gate)
{
HashSet known = KnownSet(tenantId);
if (enabled)
{
HashSet monitored = MonitoredSet(tenantId);
monitored.UnionWith(known);
}
else
{
_monitoredByTenant[tenantId] = new HashSet(StringComparer.Ordinal);
}
return known.Count;
}
}
///
/// Проверяет, мониторится ли диалог (фильтр realtime-событий L262 и догона sweep L429).
///
/// Id тенанта.
/// Id диалога.
/// True — диалог в monitored-наборе тенанта.
public bool IsMonitored(string tenantId, string dialogId)
{
if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(dialogId))
{
return false;
}
lock (_gate)
{
return _monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored) && monitored.Contains(dialogId);
}
}
///
/// Сколько диалогов знает каталог тенанта (ответ monitor-all, count каталога).
///
/// Id тенанта.
/// Размер каталога; 0 — каталог ещё не синхронизирован (нет refresh/sweep).
public int KnownCount(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return 0;
}
lock (_gate)
{
return _knownByTenant.TryGetValue(tenantId, out HashSet? known) ? known.Count : 0;
}
}
///
/// Возвращает копию мониторящихся id тенанта (для логов/тестов).
///
/// Id тенанта.
/// Набор мониторящихся id (пустой, если тенанта нет).
public IReadOnlyCollection SnapshotMonitored(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return Array.Empty();
}
lock (_gate)
{
return _monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored)
? new ReadOnlyCollection(monitored.ToArray())
: Array.Empty();
}
}
///
/// Сбрасывает состояние тенанта (Logout/отключение аккаунта — прототип L203).
///
/// Id тенанта.
public void Reset(string tenantId)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return;
}
lock (_gate)
{
_knownByTenant.Remove(tenantId);
_monitoredByTenant.Remove(tenantId);
}
}
// Полный каталог тенанта (создаёт пустой при первом обращении). Вызывается под _gate.
// tenantId: Id тенанта.
private HashSet KnownSet(string tenantId)
{
if (!_knownByTenant.TryGetValue(tenantId, out HashSet? known))
{
known = new HashSet(StringComparer.Ordinal);
_knownByTenant[tenantId] = known;
}
return known;
}
// Monitored-набор тенанта (создаёт пустой при первом обращении). Вызывается под _gate.
// tenantId: Id тенанта.
private HashSet MonitoredSet(string tenantId)
{
if (!_monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored))
{
monitored = new HashSet(StringComparer.Ordinal);
_monitoredByTenant[tenantId] = monitored;
}
return monitored;
}
}