using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using GB5Shared.Draft;
using GB5Shared.Connection;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Framework.ServerConfig;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Npgsql;
using static GB5Shared.GB5Constant.Constant;
namespace FrameworkBLL.OrphanCleanup
{
///
/// Periodic background job that expires TDRAFT sessions inactive beyond
/// DraftSettings.OrphanRetentionHours. Runs once per active tenant
/// resolved from MSERVERCONFIG.
///
public sealed class OrphanDraftCleanupJob : BackgroundService
{
private readonly IServiceProvider _services;
private readonly DraftSettings _cfg;
private readonly ILogger _logger;
public OrphanDraftCleanupJob(
IServiceProvider services,
IOptions options,
ILogger logger)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_cfg = options.Value;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"OrphanDraftCleanupJob started. Interval={Interval}m Retention={Retention}h Batch={Batch}",
_cfg.OrphanIntervalMinutes, _cfg.OrphanRetentionHours, _cfg.OrphanCleanupBatchSize);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RunCycleAsync(stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex,
"OrphanDraftCleanupJob cycle failed at {Time:HH:mm:ss}", DateTime.UtcNow);
}
try
{
await Task.Delay(
TimeSpan.FromMinutes(_cfg.OrphanIntervalMinutes), stoppingToken)
.ConfigureAwait(false);
}
catch (TaskCanceledException)
{
break;
}
}
_logger.LogInformation("OrphanDraftCleanupJob stopped.");
}
private async Task RunCycleAsync(CancellationToken ct)
{
var tenants = await GetTenantsAsync().ConfigureAwait(false);
foreach (var tenant in tenants)
{
try
{
await RunTenantCycleAsync(tenant, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex,
"OrphanDraftCleanupJob failed for tenant {DatabaseName}", tenant.DatabaseName);
}
}
}
private async Task RunTenantCycleAsync(ServerConfigDTO tenant, CancellationToken ct)
{
var login = BuildLogin(tenant);
using var scope = _services.CreateScope();
var draftRowDal = scope.ServiceProvider.GetRequiredService();
int expired = await draftRowDal
.CleanupOrphansAsync(_cfg.OrphanRetentionHours, _cfg.OrphanCleanupBatchSize, login, ct)
.ConfigureAwait(false);
if (expired > 0)
_logger.LogInformation(
"OrphanDraftCleanup [{Db}]: expired {Count} draft session(s) older than {Hours}h",
tenant.DatabaseName, expired, _cfg.OrphanRetentionHours);
}
private async Task> GetTenantsAsync()
{
using var scope = _services.CreateScope();
var appConnection = scope.ServiceProvider.GetRequiredService();
var databaseDTO = scope.ServiceProvider.GetRequiredService>();
var systemConn = await appConnection.Gb5SystemConnectionString().ConfigureAwait(false);
int dbType = databaseDTO.CurrentValue.DataBaseType;
const string sql = @"
SELECT
SERVERCONFIG1.CLIENTID AS ClientId,
SERVERCONFIG1.DATABASENAME AS DatabaseName,
SERVERCONFIG1.DATABASETYPE AS DbType,
SERVERCONFIG1.CONNECTIONNAME AS ConnectionName
FROM MSERVERCONFIG SERVERCONFIG1
JOIN MSERVER SERVER1
ON SERVERCONFIG1.SERVERID = SERVER1.SERVERID
WHERE SERVERCONFIG1.STATUS = 1
AND SERVERCONFIG1.CONNECTIONNAME <> 'ACTIVITI'
-- Tracker §47/§48 — per-tenant opt-in, default Disabled; this job previously
-- ran unconditionally against every registered tenant on every FrameworkSL
-- startup, including tenants missing the relevant tables entirely.
AND SERVERCONFIG1.SCHEDULERENABLED = 1";
using IDbConnection connection = dbType switch
{
DBTYPE.SQL => new SqlConnection(systemConn),
DBTYPE.POSTGRESQL => new NpgsqlConnection(systemConn),
_ => throw new NotSupportedException($"Unsupported DB type: {dbType}")
};
return (await connection.QueryAsync(sql).ConfigureAwait(false)).ToList();
}
private static LoginDTO BuildLogin(ServerConfigDTO tenant) => new()
{
UserId = -1,
ClientId = tenant.ClientId,
ConnectionDatabaseName = tenant.ConnectionName,
DatabaseName = tenant.DatabaseName,
DatabaseType = tenant.DbType,
UserName = "OrphanDraftCleanupJob"
};
}
}