using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.GenerateAutoNumber;
using GB5Shared.QueryExecutor;
using GB5Shared.Telemetry;
using static GB5Shared.GB5Constant.Constant;
namespace GB5Shared.ScanIdentifier
{
///
/// Generic Generate/Lookup/Retire/Reactivate/MarkPrinted engine over MSCANIDENTIFIER.
/// No entity-specific code — existence checks go through IScanTargetResolverDispatcher,
/// generation defaults come from MSCANTARGETCONFIG. Register once per host (same DI shape
/// as AutoNumber/IQueryExecutor) via plain constructor injection — no assembly-scan needed
/// for this class itself, only for the IScanTargetResolver implementations it delegates to.
///
public class ScanIdentifierEngine : IScanIdentifierEngine
{
private readonly IQueryExecutor _qe;
private readonly AutoNumber _autoNumber;
private readonly IScanTargetResolverDispatcher _resolvers;
public ScanIdentifierEngine(IQueryExecutor qe, AutoNumber autoNumber, IScanTargetResolverDispatcher resolvers)
{
_qe = qe;
_autoNumber = autoNumber;
_resolvers = resolvers;
}
public async Task GenerateAsync(
GenerateScanIdentifierRequestDTO request, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("scanidentifier-generate-validate", new { request.EntityType, request.EntityId, request.Quantity });
var exists = await _resolvers.ExistsAsync(request.EntityType, request.EntityId, login, ct).ConfigureAwait(false);
if (!exists)
throw new InvalidOperationException($"{request.EntityType} '{request.EntityId}' not found.");
var config = await GetEffectiveConfigAsync(request.EntityType, login, ct).ConfigureAwait(false);
var quantity = request.Quantity <= 0 ? 1 : request.Quantity;
if (quantity > 1 && !config.AllowMultiQty)
throw new InvalidOperationException($"{request.EntityType} does not allow multi-quantity generation.");
if (config.MaxQty is int maxQty && quantity > maxQty)
throw new InvalidOperationException($"Quantity {quantity} exceeds the configured maximum of {maxQty} for {request.EntityType}.");
var identifierType = request.IdentifierType ?? config.DefaultIdentifierType;
var baseValue = BuildBaseValue(config, request.EntityType, request.EntityId, identifierType);
GB5Trace.Step("scanidentifier-generate-save", new { request.EntityType, request.EntityId, quantity });
var allocation = await _autoNumber.GetNumberAsync(quantity, AUTONUMBERCONSTANT.SCANIDENTIFIER, login).ConfigureAwait(false);
var suffixWidth = quantity.ToString().Length;
var generated = new List(quantity);
await using var tx = await _qe.BeginTransactionAsync(login).ConfigureAwait(false);
try
{
for (var i = 0; i < quantity; i++)
{
var scanIdentifierId = allocation.StartNumber + i;
var identifierValue = quantity == 1
? baseValue
: $"{baseValue}/{(i + 1).ToString().PadLeft(suffixWidth, '0')}";
await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.INSERT_SCANIDENTIFIER,
new
{
ScanIdentifierId = scanIdentifierId,
IdentifierType = identifierType,
IdentifierValue = identifierValue,
request.EntityType,
request.EntityId,
UserId = login.UserId,
OUId = login.WorkOUId,
TenantId = login.ClientId
},
tx, cancellationToken: ct).ConfigureAwait(false);
generated.Add(new GeneratedScanIdentifierDTO
{
ScanIdentifierId = scanIdentifierId,
IdentifierType = identifierType,
IdentifierValue = identifierValue,
EntityType = request.EntityType,
EntityId = request.EntityId,
ValidFrom = DateTime.UtcNow,
Status = 1,
TenantId = login.ClientId
});
}
await tx.CommitAsync(ct).ConfigureAwait(false);
}
catch (Exception ex)
{
await tx.RollbackAsync(ct).ConfigureAwait(false);
await _autoNumber.RollbackAutoNumber(AUTONUMBERCONSTANT.SCANIDENTIFIER, allocation.StartNumber, login).ConfigureAwait(false);
GB5Trace.MarkFailed("scanidentifier-generate-failed", ex);
throw;
}
return new GenerateScanIdentifierResultDTO { Generated = generated };
}
public async Task GetByValueAsync(string identifierValue, LoginDTO login, CancellationToken ct)
{
var rows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_BY_VALUE,
new { IdentifierValue = identifierValue, TenantId = login.ClientId },
cancellationToken: ct).ConfigureAwait(false);
return rows.FirstOrDefault();
}
public async Task> GetListPagedAsync(
string entityType, int statusFilter, string searchTerm, DateTime generatedFrom,
int page, int pageSize, LoginDTO login, CancellationToken ct)
{
var param = new
{
TenantId = login.ClientId,
EntityType = entityType ?? string.Empty,
StatusFilter = statusFilter, // -1 All, 1 Active (VALIDTO IS NULL), 0 Inactive
SearchTerm = searchTerm ?? string.Empty,
GeneratedFrom = generatedFrom,
Offset = (page - 1) * pageSize,
PageSize = pageSize
};
return await _qe.QueryPagedAsync(login, ScanIdentifierEngineQB.GET_LIST_PAGED, param)
.ConfigureAwait(false);
}
public async Task RetireAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("scanidentifier-retire", new { scanIdentifierId });
await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.RETIRE_SCANIDENTIFIER,
new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId },
cancellationToken: ct).ConfigureAwait(false);
}
public async Task ReactivateAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("scanidentifier-reactivate", new { scanIdentifierId });
await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.REACTIVATE_SCANIDENTIFIER,
new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId },
cancellationToken: ct).ConfigureAwait(false);
}
public async Task MarkPrintedAsync(int scanIdentifierId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("scanidentifier-mark-printed", new { scanIdentifierId });
await _qe.ExecuteAsync(login, ScanIdentifierEngineQB.MARK_PRINTED,
new { ScanIdentifierId = scanIdentifierId, UserId = login.UserId, TenantId = login.ClientId },
cancellationToken: ct).ConfigureAwait(false);
}
public async Task> GetPrinterTypesAsync(LoginDTO login, CancellationToken ct)
=> await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_PRINTER_TYPES, cancellationToken: ct).ConfigureAwait(false);
public async Task> GetLabelTypesAsync(LoginDTO login, CancellationToken ct)
=> await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_LABEL_TYPES, cancellationToken: ct).ConfigureAwait(false);
public async Task PrintBatchAsync(PrintBatchRequestDTO request, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("scanidentifier-print-batch", new { request.LabelTypeGcmId, count = request.ScanIdentifierIds.Count });
var templateRows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_LABEL_TEMPLATE,
new { GcmId = request.LabelTypeGcmId }, cancellationToken: ct).ConfigureAwait(false);
var template = templateRows.FirstOrDefault()
?? throw new InvalidOperationException($"Label type {request.LabelTypeGcmId} has no stored dimensions (MLABELTEMPLATE).");
var rows = new List<(GeneratedScanIdentifierDTO, string, string)>(request.ScanIdentifierIds.Count);
foreach (var id in request.ScanIdentifierIds)
{
var idRows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_BY_ID,
new { ScanIdentifierId = id, TenantId = login.ClientId }, cancellationToken: ct).ConfigureAwait(false);
var identifier = idRows.FirstOrDefault();
if (identifier is null) continue;
var display = await _resolvers.GetDisplayAsync(identifier.EntityType, identifier.EntityId, login, ct).ConfigureAwait(false);
rows.Add((identifier, display?.Code ?? identifier.EntityType, display?.Name ?? identifier.EntityId));
}
var pdfBytes = ScanLabelPdfRenderer.RenderBatch(rows, template);
foreach (var id in request.ScanIdentifierIds)
await MarkPrintedAsync(id, login, ct).ConfigureAwait(false);
return pdfBytes;
}
public Task GetEffectiveConfigAsync(string entityType, LoginDTO login, CancellationToken ct)
=> GetEffectiveConfigInternalAsync(entityType, login, ct);
private async Task GetEffectiveConfigInternalAsync(string entityType, LoginDTO login, CancellationToken ct)
{
var rows = await _qe.QueryAsync(login, ScanIdentifierEngineQB.GET_TARGET_CONFIG,
new { EntityType = entityType, TenantId = login.ClientId },
cancellationToken: ct).ConfigureAwait(false);
// No MSCANTARGETCONFIG row yet for this EntityType — a resolver can be registered before
// an admin ever configures it; fall back to conservative single-QR defaults rather than fail.
return rows.FirstOrDefault() ?? new ScanTargetConfigDTO { EntityType = entityType, DefaultIdentifierType = 2, ValueScheme = 0, AllowMultiQty = false };
}
private static string BuildBaseValue(ScanTargetConfigDTO config, string entityType, string entityId, byte identifierType)
{
if (config.ValueScheme == 1)
{
// OpaqueCode: no entity info encoded — the code only ever resolves via a MSCANIDENTIFIER lookup.
return Guid.NewGuid().ToString("N")[..12].ToUpperInvariant();
}
var prefix = string.IsNullOrWhiteSpace(config.Prefix) ? entityType : config.Prefix;
return $"{prefix}:{entityId}";
}
}
}