using System.Text.Json;
using Deal.Modules.Tenants.Application.Models;
using Deal.SharedKernel.Observability;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Registrars;
namespace Deal.Modules.Tenants.Application.Services;
///
/// Прикладной сервис аудита (Ruling 4 этапа 7): append-only запись событий и чтение ленты оператором.
///
///
/// Единственная точка записи в public.audit_log: сам проставляет At=UTC-now, вызывается
/// из эндпоинтов/сервисов (входы, инвайты, impersonation, действия оператора — задачи 4–10). Update/Delete в
/// порту отсутствуют (append-only на уровне кода); TTL/авто-очистка не делаются (Ruling 4). Чтение — только
/// оператору (GET /api/operator/audit через QueryAsync/CountAsync). Каталог событий — ,
/// типы акторов — , JSON деталей — (camelCase, без секретов).
///
public sealed class AuditService(IAuditLogStore store)
{
///
/// Верхняя граница выборки аудита (Ruling 4: limit ≤500).
///
public const int MaxQueryLimit = 500;
///
/// Размер выборки по умолчанию при отсутствии limit в запросе (эталон DiscoveryLogService).
///
public const int DefaultQueryLimit = 100;
// Опции JSON деталей: camelCase (конвенция DetailJson/JSON проекта).
private static readonly JsonSerializerOptions DetailJsonOptions = new(JsonSerializerDefaults.Web);
///
/// Записывает событие аудита (append-only; At = сейчас, UTC).
///
/// Запись события (At и Id игнорируются: At проставляет сервис, Id — БД).
/// Токен отмены.
public async Task AppendAsync(AuditRecordDto record, CancellationToken ct)
{
await store.AppendAsync(record with { At = DateTimeOffset.UtcNow }, ct);
// Прикладная метрика (этап 12, пакет A): счётчик событий аудита по типу/актору
// (низкокардинальные метки — без tenantId/actorId).
DealMetrics.RecordAuditEvent(record.EventType, record.ActorType);
}
///
/// Записи по фильтру, новые сверху (прокси порта; чтение — операторский эндпоинт).
///
/// Фильтр выборки.
/// Токен отмены.
/// Записи от новых к старым.
public Task> QueryAsync(AuditQueryDto filter, CancellationToken ct) =>
store.QueryAsync(filter, ct);
///
/// Число записей по фильтру (для ответа {items, total}).
///
/// Фильтр выборки.
/// Токен отмены.
/// Полное число записей по фильтру.
public Task CountAsync(AuditQueryDto filter, CancellationToken ct) => store.CountAsync(filter, ct);
///
/// Сериализует детали события в JSON (camelCase; секреты в объект не класть — правило Ruling 4).
///
/// Объект деталей (обычно анонимный: { login = ... }).
/// JSON-строка деталей.
public static string ToDetailJson(object? details) => JsonSerializer.Serialize(details, DetailJsonOptions);
///
/// Актор «пользователь тенанта» по разрешённой сессии: (ActorType, ActorId, TenantId).
///
/// Идентичность пользователя тенанта.
/// Кортеж актора для полей записи аудита.
public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromUser(UserIdentityDto user) =>
(AuditActorTypes.Tenant, user.Id, user.TenantId);
///
/// Актор «оператор» по разрешённой операторской сессии: (ActorType, ActorId, TenantId=null).
///
/// Идентичность оператора.
/// Кортеж актора для полей записи аудита.
public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromOperator(OperatorIdentityDto operatorIdentity) =>
(AuditActorTypes.Operator, operatorIdentity.Id, null);
}