using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using Microsoft.Extensions.DependencyInjection;
namespace GB5Shared.ScanIdentifier
{
// ─────────────────────────────────────────────────────────────────────
// Generic Scan-Identifier & Label Platform — per-module plug-in.
// Mirrors GB5Shared/ScanEvent/IScanEventHandler.cs's exact registration pattern
// (SupportedEntityTypes + DI-assembly-scan), but for generation/display instead
// of business-transaction handling. Deliberately NOT the same interface as
// IScanEventHandler: that one is for state-machine-driven transactions (FM's
// AssetScanEventHandler posts ledger entries, enforces state transitions); this
// one only needs to answer "does this entity exist" and "how should it be
// displayed" so ScanIdentifierEngine can generate/list codes without any
// entity-specific code of its own.
// ─────────────────────────────────────────────────────────────────────
/// Display projection of one entity, for the picker/search step and result lists.
public sealed class ScanTargetDisplayDTO
{
public required string EntityId { get; init; }
public string Code { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
/// Optional extra columns a resolver wants shown (e.g. "WorkCenterName") — key is the column label.
public IReadOnlyDictionary ExtraFields { get; init; } = new Dictionary();
}
///
/// Implemented once per owning module for its own entity types (e.g. WI for "MWI"/
/// "MWORKSTATION"/"MEMPLOYEE", MM for "TINDENTDETAIL"/"MASSET"). The platform's
/// dispatcher resolves the right resolver by EntityType — no module ever needs to
/// know about any other module's entity types.
///
public interface IScanTargetResolver
{
/// Entity-type strings this resolver owns, e.g. ["MWI"].
IEnumerable SupportedEntityTypes { get; }
Task ExistsAsync(string entityId, LoginDTO login, CancellationToken ct);
Task GetDisplayAsync(string entityId, LoginDTO login, CancellationToken ct);
/// Backs the Generate/Print facilities' entity search box. Empty/null term returns a default list.
Task> SearchAsync(string? term, LoginDTO login, CancellationToken ct);
}
///
/// Resolves and invokes the right IScanTargetResolver for a given EntityType. Handlers are
/// looked up by string key (not by C# generic type) because EntityType is only known at
/// runtime — it comes off the caller's request or an MSCANIDENTIFIER row, never off a
/// compile-time query type. Same shape as GB5Shared/ScanEvent's ScanEventDispatcher.
///
public interface IScanTargetResolverDispatcher
{
/// Every EntityType registered in this host's own process — backs GetRegisteredEntityTypes.
IEnumerable RegisteredEntityTypes { get; }
Task ExistsAsync(string entityType, string entityId, LoginDTO login, CancellationToken ct);
Task GetDisplayAsync(string entityType, string entityId, LoginDTO login, CancellationToken ct);
Task> SearchAsync(string entityType, string? term, LoginDTO login, CancellationToken ct);
}
public sealed class ScanTargetResolverDispatcher : IScanTargetResolverDispatcher
{
private readonly Lazy> _byEntityType;
public ScanTargetResolverDispatcher(IEnumerable resolvers)
{
_byEntityType = new Lazy>(() =>
{
var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var resolver in resolvers)
foreach (var entityType in resolver.SupportedEntityTypes)
{
if (map.ContainsKey(entityType))
throw new InvalidOperationException(
$"Multiple IScanTargetResolver registrations claim EntityType '{entityType}'.");
map[entityType] = resolver;
}
return map;
});
}
public IEnumerable RegisteredEntityTypes => _byEntityType.Value.Keys;
public Task ExistsAsync(string entityType, string entityId, LoginDTO login, CancellationToken ct)
=> Resolve(entityType).ExistsAsync(entityId, login, ct);
public Task GetDisplayAsync(string entityType, string entityId, LoginDTO login, CancellationToken ct)
=> Resolve(entityType).GetDisplayAsync(entityId, login, ct);
public Task> SearchAsync(string entityType, string? term, LoginDTO login, CancellationToken ct)
=> Resolve(entityType).SearchAsync(term, login, ct);
private IScanTargetResolver Resolve(string entityType)
=> _byEntityType.Value.TryGetValue(entityType, out var resolver)
? resolver
: throw new InvalidOperationException(
$"No IScanTargetResolver registered for EntityType '{entityType}'. " +
$"Register an IScanTargetResolver whose SupportedEntityTypes includes it.");
}
/// DI registration helper — call once in Program.cs per module hosting scan targets.
public static class ScanTargetResolverServiceExtensions
{
///
/// Registers IScanTargetResolverDispatcher and every IScanTargetResolver implementation
/// found in . Safe to call from multiple modules'
/// Program.cs — each module registers only its own assembly's resolvers; the dispatcher
/// itself is process-wide only within a single service's DI container (matching this
/// platform's one-module-per-process hosting model, same as AddScanEventHandlers).
///
public static IServiceCollection AddScanTargetResolvers(
this IServiceCollection services, params Assembly[] resolverAssemblies)
{
services.AddScoped();
if (resolverAssemblies is null or { Length: 0 }) return services;
foreach (var assembly in resolverAssemblies)
foreach (var type in assembly.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface
&& typeof(IScanTargetResolver).IsAssignableFrom(t)))
{
services.AddScoped(typeof(IScanTargetResolver), type);
}
return services;
}
}
}