using GB5Shared.DTO.Framework.Login;
using GB5Shared.QueryExecutor;
using GB5Shared.Resource.Response;
using SwBLL.ClientDatabase;
using SwBLL.DbServer;
using SwBLL.DdlScript;
using SwBLL.UpgradePackage;
using SwBLL.UserResourceRole;
using SwDAL.DTO.Provisioning;
using SwDAL.Enums;
using SwDAL.Query.Provisioning;
namespace SwBLL.Provisioning;
///
/// Orchestrates provisioning of a client database:
/// 1. Loads the upgrade package DDL lines, ordered by SEQUENCE then DdlScriptId.
/// 2. Resolves the DbServer and builds a connection string.
/// 3. Executes each Approved DDL script; logs result AFTER each execution with correct status.
/// 4. Sets ClientDatabase to Active if all scripts succeed, Failed if any fail.
///
/// Architecture rule (§4.1): injects BLL interfaces only — never foreign DALs.
///
public class ProvisioningBLL : IProvisioningBLL
{
private readonly IClientDatabaseBLL _clientDatabaseBLL;
private readonly IDdlScriptBLL _ddlScriptBLL;
private readonly IUpgradePackageBLL _upgradePackageBLL;
private readonly IDbServerBLL _dbServerBLL;
private readonly ITargetDbExecutor _targetDbExecutor;
private readonly IUserResourceRoleBLL _userResourceRoleBLL;
private readonly IQueryExecutor _queryExecutor;
public ProvisioningBLL(
IClientDatabaseBLL clientDatabaseBLL,
IDdlScriptBLL ddlScriptBLL,
IUpgradePackageBLL upgradePackageBLL,
IDbServerBLL dbServerBLL,
ITargetDbExecutor targetDbExecutor,
IUserResourceRoleBLL userResourceRoleBLL,
IQueryExecutor queryExecutor)
{
_clientDatabaseBLL = clientDatabaseBLL;
_ddlScriptBLL = ddlScriptBLL;
_upgradePackageBLL = upgradePackageBLL;
_dbServerBLL = dbServerBLL;
_targetDbExecutor = targetDbExecutor;
_userResourceRoleBLL = userResourceRoleBLL;
_queryExecutor = queryExecutor;
}
public async Task ProvisionClientDatabase(
int clientDbId, int upgradePackageId, LoginDTO loginDTO, CancellationToken ct)
{
// ── 1. Load and validate client database ─────────────────────────
var clientDb = await _clientDatabaseBLL.GetById(clientDbId, loginDTO, ct)
?? throw new InvalidOperationException($"Client database {clientDbId} not found.");
var server = await _dbServerBLL.GetByIdWithCredentials(clientDb.DbServerId, loginDTO, ct)
?? throw new InvalidOperationException($"DB server {clientDb.DbServerId} not found.");
// Provisioning always needs the flat DbServer admin connection — contained users don't
// exist yet, there's nothing to route a role-based connection through. Fail-closed only
// when the caller has an EXPLICIT, insufficient grant on record (e.g. a ReadOnly-only
// grant attempting to provision) — no grant configured at all is fail-open, consistent
// with the other enforcement call sites in a module where every endpoint is currently
// AllowAnonymous().
var grant = await _userResourceRoleBLL
.GetEffectiveRole(loginDTO.UserId, clientDb.DbServerId, clientDb.ClientDbId, loginDTO, ct)
.ConfigureAwait(false);
if (grant.HasValue && grant.Value != ClientDbLoginRole.Dba)
throw new UnauthorizedAccessException(
$"User {loginDTO.UserId} is granted {grant.Value} on this target, which is insufficient to provision a client database (requires Dba).");
string connStr = _targetDbExecutor.BuildConnectionString(server, clientDb.DatabaseName);
// ── 2. Load upgrade package DDL lines ────────────────────────────
var ddlLines = (await _upgradePackageBLL.GetDdlLines(upgradePackageId, loginDTO, ct))
.ToList();
if (ddlLines.Count == 0)
throw new InvalidOperationException(
$"Upgrade package {upgradePackageId} contains no DDL scripts.");
// ── 3. Load each DDL script and order by SEQUENCE, then DdlScriptId (§4.4) ──
var scripts = new List<(int DdlScriptId, string SqlScript, short Sequence)>();
foreach (var line in ddlLines)
{
var script = await _ddlScriptBLL.GetById(line.DdlScriptId, loginDTO, ct);
if (script is null) continue;
// Only execute Approved scripts
if (script.ScriptStatus != (byte)DdlScriptStatus.Approved) continue;
scripts.Add((script.DdlScriptId, script.SqlScript ?? string.Empty, line.Sequence));
}
// Order: Sequence==0 last, otherwise ascending, tie-break by DdlScriptId ASC (§4.4)
var orderedScripts = scripts
.OrderBy(s => s.Sequence == 0 ? short.MaxValue : s.Sequence)
.ThenBy(s => s.DdlScriptId)
.ToList();
// ── 4. Execute scripts — log AFTER each, with correct status (§4.3) ──
bool allSucceeded = true;
foreach (var (ddlScriptId, sql, _) in orderedScripts)
{
try
{
string appliedSql = DdlScriptTemplating.SubstituteTenantId(sql, clientDb.TenantId);
await _targetDbExecutor.ExecuteScriptAsync(connStr, appliedSql, ct).ConfigureAwait(false);
// Log SUCCESS after successful execution
await _ddlScriptBLL.LogExecutionAsync(
ddlScriptId, clientDbId,
(byte)DdlExecStatus.Success,
errorMessage: null,
loginDTO, ct).ConfigureAwait(false);
// Provisioning-run-scoped log — carries PackageId/SyncGroupId context that
// LSWDDLEXECUTION (per-script only) does not.
await LogProvisioningAsync(
clientDbId, upgradePackageId, ddlScriptId,
(byte)DdlExecStatus.Success, message: null, errorMessage: null,
loginDTO, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
allSucceeded = false;
// Log FAILED after failed execution — do not rethrow; continue remaining scripts
await _ddlScriptBLL.LogExecutionAsync(
ddlScriptId, clientDbId,
(byte)DdlExecStatus.Failed,
errorMessage: ex.Message,
loginDTO, ct).ConfigureAwait(false);
await LogProvisioningAsync(
clientDbId, upgradePackageId, ddlScriptId,
(byte)DdlExecStatus.Failed, message: null, errorMessage: ex.Message,
loginDTO, ct).ConfigureAwait(false);
}
}
// ── 5. Set final ClientDatabase status (§4.5) ────────────────────
// Active only when ALL scripts succeed; Failed on any failure.
byte finalStatus = allSucceeded
? (byte)ClientDbStatus.Active
: (byte)ClientDbStatus.Failed;
await _clientDatabaseBLL.UpdateStatusAsync(clientDbId, finalStatus, loginDTO, ct)
.ConfigureAwait(false);
return allSucceeded
? $"{SuccessResponse.SaveSuccessMessage} {clientDbId}"
: $"Provisioning completed with failures. Client database {clientDbId} status set to Failed.";
}
// ── Current-version rollup ───────────────────────────────────────────
// Uses only the existing injected dependencies (_clientDatabaseBLL, _upgradePackageBLL,
// _queryExecutor) — no new constructor dependency. The provisioning-log read goes straight
// through _queryExecutor, same as LogProvisioningAsync's write below, per this class's own
// architecture rule (§4.1: injects BLL interfaces only — never foreign DALs) rather than
// introducing a new IProvisioningLogDAL for one read query.
public async Task GetCurrentVersion(int clientDbId, LoginDTO loginDTO, CancellationToken ct)
{
// ClientDbId is not sign-constrained — like most GB5 identifiers, real registered
// SW.MSWCLIENTDATABASE rows commonly carry large negative autonumber IDs. Only the
// unset/default value (0) is treated as "not provided"; existence is otherwise verified
// below via _clientDatabaseBLL.GetById.
if (clientDbId == 0) throw new ArgumentException("ClientDbId is required.", nameof(clientDbId));
var clientDb = await _clientDatabaseBLL.GetById(clientDbId, loginDTO, ct).ConfigureAwait(false)
?? throw new InvalidOperationException($"Client database {clientDbId} not found.");
var result = new ClientVersionRollupDTO { ClientDbId = clientDbId };
var mostRecent = await _queryExecutor.QuerySingleAsync(
loginDTO, ProvisioningLogQB.GET_MOST_RECENTLY_APPLIED_PACKAGE,
new { ClientDbId = clientDbId, TenantId = loginDTO.ClientId }, cancellationToken: ct)
.ConfigureAwait(false);
if (mostRecent is null) return result; // nothing fully applied yet — unversioned
result.MostRecentlyAppliedPackageId = mostRecent.PackageId;
var lineage = (await _upgradePackageBLL.GetLineage(clientDb.DbModelId, loginDTO, ct).ConfigureAwait(false))
.ToDictionary(n => n.PackageId);
int? cursor = mostRecent.PackageId;
var visited = new HashSet();
const int maxHops = 500; // defends against a corrupt/cyclic SupersedesPackageId graph
for (int hop = 0; cursor is int id && hop < maxHops && visited.Add(id); hop++)
{
if (!lineage.TryGetValue(id, out var node)) break;
if (!string.IsNullOrEmpty(node.ReleaseVersion))
{
result.CurrentReleaseVersion = node.ReleaseVersion;
result.ResolvedFromPackageId = node.PackageId;
result.IsVersioned = true;
return result;
}
cursor = node.SupersedesPackageId;
}
return result; // walked to root (or exhausted the bound) without finding a version — unversioned
}
// ── Log Provisioning (direct QueryExecutor — log table, not M-table) ───
private async Task LogProvisioningAsync(
int clientDbId, int packageId, int ddlScriptId, byte status,
string? message, string? errorMessage, LoginDTO loginDTO, CancellationToken ct)
{
var logDto = new ProvisioningLogDTO
{
ClientDbId = clientDbId,
PackageId = packageId,
DdlScriptId = ddlScriptId,
LogType = (byte)ProvisioningLogType.DdlExecution,
LogStatus = status,
Message = message,
ErrorMessage = errorMessage,
CreatedOn = DateTime.UtcNow,
CreatedById = loginDTO.UserId,
TenantId = loginDTO.ClientId
};
await _queryExecutor.ExecuteAsync(loginDTO, ProvisioningLogQB.LOG_INSERT, logDto, cancellationToken: ct)
.ConfigureAwait(false);
}
}