using FrameworkDAL.CustomCode.AuditQuery;
using FrameworkDAL.CustomCode.ChangeTracking;
using FrameworkDAL.DTO.AuditQuery;
using GB5Shared.DTO.Framework.Login;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace FrameworkBLL.AuditQuery
{
///
/// Read-only service for querying audit history.
///
/// Archive fan-out: when predates the
/// current hot-table retention boundary (UtcNow - ArchiveSettings.RetentionMonths), results
/// are retrieved from both TEVENTLOG and TEVENTLOG_ARCHIVE and merged.
///
/// Change detail: for GetByEntity and GetByUser calls, the BLL loads the associated
/// TEVENTLOGCHANGE rows for each result and attaches them to the DTO.
/// For SearchAudit (potentially large result sets) changes are NOT loaded by default
/// to avoid N+1 — callers that need changes should call GetByEntity for individual records.
///
public sealed class AuditQueryBLL : IAuditQueryBLL
{
private readonly IAuditQueryDAL _dal;
private readonly IChangeTrackingDAL _changeDal;
private readonly IOptions _archiveSettings;
private readonly ILogger _logger;
public AuditQueryBLL(
IAuditQueryDAL dal,
IChangeTrackingDAL changeDal,
IOptions archiveSettings,
ILogger logger)
{
_dal = dal;
_changeDal = changeDal;
_archiveSettings = archiveSettings;
_logger = logger;
}
public async Task GetByEntityAsync(
int entityType, int entityId, int pageOffset, int pageSize,
LoginDTO login, CancellationToken ct = default)
{
var rows = await _dal.GetByEntityAsync(entityType, entityId, pageOffset, pageSize, login, ct)
.ConfigureAwait(false);
await EnrichWithChangesAsync(rows, login, ct).ConfigureAwait(false);
return JsonSerializer.Serialize(rows);
}
public async Task GetByUserAsync(
AuditSearchCriteriaDTO criteria, LoginDTO login, CancellationToken ct = default)
{
var rows = await _dal.GetByUserAsync(criteria, login, ct).ConfigureAwait(false);
if (ShouldQueryArchive(criteria.DateFrom))
{
var archiveRows = await _dal.GetByUserArchiveAsync(criteria, login, ct).ConfigureAwait(false);
rows = MergeAndPage(rows, archiveRows, criteria.PageOffset, criteria.PageSize);
}
await EnrichWithChangesAsync(rows, login, ct).ConfigureAwait(false);
return JsonSerializer.Serialize(rows);
}
public async Task SearchAsync(
AuditSearchCriteriaDTO criteria, LoginDTO login, CancellationToken ct = default)
{
var rows = await _dal.SearchAsync(criteria, login, ct).ConfigureAwait(false);
if (ShouldQueryArchive(criteria.DateFrom))
{
var archiveRows = await _dal.SearchArchiveAsync(criteria, login, ct).ConfigureAwait(false);
rows = MergeAndPage(rows, archiveRows, criteria.PageOffset, criteria.PageSize);
}
// Search results do not load per-row change detail — avoid N+1 on large result sets.
// Callers needing changes should call GetByEntity for individual rows.
return JsonSerializer.Serialize(rows);
}
// ── Helpers ───────────────────────────────────────────────────────────
private bool ShouldQueryArchive(DateTime? dateFrom)
{
if (dateFrom is null) return false;
var boundary = DateTime.UtcNow.AddMonths(-_archiveSettings.Value.RetentionMonths);
return dateFrom.Value < boundary;
}
private static List MergeAndPage(
List hot,
List archive,
int pageOffset,
int pageSize)
{
return hot.Concat(archive)
.OrderByDescending(e => e.Timestamp)
.Skip(pageOffset)
.Take(pageSize)
.ToList();
}
private async Task EnrichWithChangesAsync(
List rows, LoginDTO login, CancellationToken ct)
{
foreach (var row in rows)
{
try
{
row.Changes = await _changeDal
.GetChangesByEventLogAsync(row.EventLogId, login, ct)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"AuditQueryBLL: failed to load changes for EventLogId={EventLogId}", row.EventLogId);
// Leave Changes as empty list — do not abort the whole query
}
}
}
}
///
/// Configuration for the audit archive boundary.
/// Bound from appsettings section "ArchiveSettings".
///
public sealed class ArchiveSettings
{
public const string Section = "ArchiveSettings";
/// How many months of events to keep in the hot TEVENTLOG table. Default: 6.
public int RetentionMonths { get; set; } = 6;
}
}