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; } } }