using System.Text.Json;
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Registrars;
using Deal.Modules.Settings.Application.Services;
using Deal.Modules.Telegram.Application;
using Deal.Modules.Telegram.Application.Models;
using Deal.Api.Services;
using Deal.Api.Dtos;
namespace Deal.Api.Telegram;
///
/// Сборка статуса вкладки Telegram — GET /api/tg/status (Ruling 8, api-map §4.9 L357–359).
///
///
/// Форма 1:1 с status() python L103–119 в терминах этапа 6:
///
/// - live-поля (phase/connected/listener/error/qrUrl) — из гейта
/// (живой telegram-service); сервис недоступен/сессии нет (RPC-отказ) → idle-форма (Ruling 8);
/// - account — из KV tgAccount (источник истины — ReportStatus ингресса, Ruling 7);
/// - monitored — count(Dialogs WHERE Monitor) ядра (DialogsService.ListMonitoredIds);
/// - keysSet — оба глобальных ключа приложения заданы оператором (TelegramKeysService, Ruling 3, ТЗ §4.1/§8.1).
///
/// Scoped: зависимости живут на контекстах запроса (ISettingsStore — схема тенанта, IGlobalSettingsStore — public).
///
/// Порт-гейт telegram-service (живой статус аккаунта).
/// Сервис каталога диалогов ядра (счётчик мониторящихся).
/// KV-хранилище настроек тенанта (tgAccount).
/// Глобальные ключи приложения Telegram (keysSet).
public sealed class TgStatusService(
ITelegramGateway gateway,
DialogsService dialogs,
ISettingsStore settings,
TelegramKeysService keys)
{
// Фаза idle-формы (аккаунт не подключён/сервис недоступен — Ruling 8).
private const string IdlePhase = "idle";
// Опции JSON KV-значений статуса: camelCase (как пишет ингресс) + терпимость регистра.
private static readonly JsonSerializerOptions KvJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
};
///
/// Форма GET /api/tg/status текущего тенанта (поля §4.9).
///
/// Токен отмены.
/// Полный статус вкладки Telegram.
public async Task GetAsync(CancellationToken ct)
{
TelegramAccountStatusDto live = await ReadLiveAsync(ct).ConfigureAwait(false);
string account = await ReadAccountAsync(ct).ConfigureAwait(false);
int monitored = (await dialogs.ListMonitoredIdsAsync(ct).ConfigureAwait(false)).Count;
TgKeysSnapshot snapshot = await keys.GetAsync(ct).ConfigureAwait(false);
return new TgStatusDto(
Phase: live.Phase,
Connected: live.Connected,
Listener: live.Listener,
Account: account,
Monitored: monitored,
KeysSet: snapshot.KeysSet,
Error: live.Error,
QrUrl: live.QrUrl);
}
// Живой статус из гейта; сбой (сервис недоступен/нет сессии) → idle-форма (Ruling 8).
// ct: Токен отмены.
// Возвращает: Статус гейта либо idle-поля.
private async Task ReadLiveAsync(CancellationToken ct)
{
try
{
return await gateway.StatusAsync(ct).ConfigureAwait(false);
}
catch (Exception exception) when (exception is not OperationCanceledException || !ct.IsCancellationRequested)
{
// «Сервис недоступен → idle-форма» (Ruling 8): connected=false, live-поля пусты. Аккаунт/счётчики
// ядро всё равно докладывает из своего KV/БД (ниже) — как python при отключённом клиенте.
return new TelegramAccountStatusDto(IdlePhase, Connected: false, Listener: false, string.Empty, null, null);
}
}
// Аккаунт «@username» из KV tgAccount (JSON-строка, пишет ReportStatus ингресса, Ruling 7).
// ct: Токен отмены.
// Возвращает: Аккаунт или пустая строка.
private async Task ReadAccountAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.TgAccount, ct).ConfigureAwait(false);
if (row is null)
{
return string.Empty;
}
try
{
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
return document.RootElement.ValueKind == JsonValueKind.String
? document.RootElement.GetString() ?? string.Empty
: string.Empty;
}
catch (JsonException)
{
return string.Empty;
}
}
}