using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using Microsoft.Extensions.DependencyInjection;
namespace GB5Shared.ScanEvent
{
// ─────────────────────────────────────────────────────────────────────
// RAM Phase 2 — platform-side scan/event-engine framework.
// Reference: RAM_Modernization_Design_Reference.md §4.13.
//
// This layer is module-agnostic: it resolves identity, exposes context,
// and dispatches to whichever module owns the scanned entity. It does
// NOT contain module business logic (that lives in each module's own
// IScanEventHandler implementation, built alongside that module's own
// RAM touchpoint in a later phase — FM/FAM in Phase 3, CRM-Service in
// Phase 4, per the phased migration plan). It also does NOT yet
// implement phantom-read filtering, zone RSSI validation, or configured
// state-machine transition enforcement — those are Scan Event Engine
// concerns layered on top of this extension point in a later increment.
// ─────────────────────────────────────────────────────────────────────
///
/// A resolved reference to the entity a scan identifier points at —
/// mirrors MSCANIDENTIFIER/TSCANEVENT's generic (ENTITYTYPE, ENTITYID)
/// bridge. EntityId is a string for the same reason MSCANIDENTIFIER's
/// column is: the bridge must span mixed-key-type targets in principle,
/// even though the two currently-known targets (MASSET.ASSETID,
/// MBIN.BINID) both happen to be INT.
///
public sealed class TrackableEntity
{
public required string EntityType { get; init; } // e.g. "MASSET", "MBIN"
public required string EntityId { get; init; }
public int OUId { get; init; }
public int TenantId { get; init; }
}
///
/// The scan being processed — a lightweight projection of one TSCANEVENT
/// row, not the row itself (module handlers should not depend on the
/// DAL-owned TSCANEVENT shape).
///
public sealed class ScanEvent
{
public int ScanEventId { get; init; }
public int ScanSessionId { get; init; }
public string RawScannedValue { get; init; } = string.Empty;
public byte IdentifierType { get; init; } // (0.RFID_EPC,1.RFID_TID,2.QR,3.BARCODE,4.NFC,5.MANUAL)
public int ScanZoneId { get; init; }
public int ScanDeviceId { get; init; }
public int OperatorId { get; init; }
public byte ScanIntent { get; init; } // (0.IDENTIFY,1.VERIFY,2.TRANSACT,3.AUDIT)
}
///
/// What a module reports back after ResolveContextAsync — current
/// state/position plus what the platform should offer the operator next.
/// AllowedBizTransactionTypeIds/AllowedTransitionStateCodes are advisory
/// to the caller (e.g. a mobile scan UI); enforcement of an actually
/// chosen transition happens in HandleScanAsync.
///
public sealed class ScanContext
{
public string? CurrentStateCode { get; init; }
public IReadOnlyList AllowedTransitionStateCodes { get; init; } = [];
public IReadOnlyList AllowedBizTransactionTypeIds { get; init; } = [];
public bool Found { get; init; }
}
/// Outcome of HandleScanAsync — success/failure plus the ledger row it produced, if any.
public sealed class ScanResult
{
public bool IsSuccess { get; init; }
public string Message { get; init; } = string.Empty;
public int? AssetLedgerId { get; init; } // set when a TASSETLEDGER row was posted
public static ScanResult Success(string message, int? assetLedgerId = null)
=> new() { IsSuccess = true, Message = message, AssetLedgerId = assetLedgerId };
public static ScanResult Failure(string message)
=> new() { IsSuccess = false, Message = message };
}
///
/// What the operator/caller decided to do with a TRANSACT-intent scan — distinct from
/// ScanEvent (which is *what was scanned*). All fields optional: a handler falls back to
/// its own default behavior (e.g. custody unchanged) when a field isn't supplied, so
/// handlers that never need custody/reference tracking aren't forced to populate this.
///
public sealed class ScanTransactionContext
{
/// Target custodian type (0.INTERNAL_OU,1.EMPLOYEE,2.PARTNER,3.CUSTOMER) — null keeps custody unchanged.
public int? ToCustodianType { get; init; }
public int? ToCustodianId { get; init; }
/// e.g. "TMMHEAD", "TPRODUCTION" — set only when a live external document exists to reference.
public string? ReferenceType { get; init; }
public int? ReferenceId { get; init; }
/// Free-text fallback for tenants with no live document to point ReferenceId at.
public string? ReferenceNumber { get; init; }
public DateTime? ReferenceDate { get; init; }
}
///
/// Implemented once per owning module (FM, FAM, Maintenance, CRM-Service, ...).
/// The platform's dispatcher resolves the right handler by EntityType and
/// delegates — no module ever needs to know about any other module's
/// entity types.
///
public interface IScanEventHandler
{
/// Entity-type strings this handler owns, e.g. ["MASSET"].
IEnumerable SupportedEntityTypes { get; }
/// Resolve current state/allowed-next-actions for a scanned/identified entity.
Task ResolveContextAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login);
///
/// Execute the business action after platform-level validation (identity
/// resolution, phantom-read filtering, zone/intent checks) has already
/// passed. confirmedBizTransactionType is the operator-confirmed choice
/// from among ScanContext.AllowedBizTransactionTypeIds. context carries any
/// custody-target/reference-document details the operator supplied — null when none apply.
///
Task HandleScanAsync(
TrackableEntity entity, ScanEvent scanEvent, int confirmedBizTransactionTypeId,
ScanTransactionContext? context, LoginDTO login);
///
/// Record an AUDIT-intent scan (physical presence verification) — no state/location/
/// custody transition, no operator-confirmed BizTransactionType. Distinct from
/// HandleScanAsync so AUDIT never has to overload confirmedBizTransactionTypeId with a
/// synthetic/sentinel value to mean "nothing moved."
///
Task RecordAttestationAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login);
}
///
/// Resolves and invokes the right IScanEventHandler for a scanned entity's
/// EntityType. Handlers are looked up by string key (not by C# generic
/// type, unlike IListDispatcher) because EntityType is only known at
/// runtime — it comes off the MSCANIDENTIFIER/TSCANEVENT row, not off a
/// compile-time query type.
///
public interface IScanEventDispatcher
{
Task ResolveContextAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login);
Task HandleScanAsync(
TrackableEntity entity, ScanEvent scanEvent, int confirmedBizTransactionTypeId,
ScanTransactionContext? context, LoginDTO login);
Task RecordAttestationAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login);
}
///
/// Builds an EntityType → handler lookup once from every registered
/// IScanEventHandler (DI is the registry — no manual switch statements,
/// same principle as ListDispatcher).
///
public sealed class ScanEventDispatcher : IScanEventDispatcher
{
private readonly Lazy> _byEntityType;
public ScanEventDispatcher(IEnumerable handlers)
{
_byEntityType = new Lazy>(() =>
{
var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var handler in handlers)
foreach (var entityType in handler.SupportedEntityTypes)
{
if (map.ContainsKey(entityType))
throw new InvalidOperationException(
$"Multiple IScanEventHandler registrations claim EntityType '{entityType}'.");
map[entityType] = handler;
}
return map;
});
}
public Task ResolveContextAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login)
=> Resolve(entity.EntityType).ResolveContextAsync(entity, scanEvent, login);
public Task HandleScanAsync(
TrackableEntity entity, ScanEvent scanEvent, int confirmedBizTransactionTypeId,
ScanTransactionContext? context, LoginDTO login)
=> Resolve(entity.EntityType).HandleScanAsync(entity, scanEvent, confirmedBizTransactionTypeId, context, login);
public Task RecordAttestationAsync(TrackableEntity entity, ScanEvent scanEvent, LoginDTO login)
=> Resolve(entity.EntityType).RecordAttestationAsync(entity, scanEvent, login);
private IScanEventHandler Resolve(string entityType)
=> _byEntityType.Value.TryGetValue(entityType, out var handler)
? handler
: throw new InvalidOperationException(
$"No IScanEventHandler registered for EntityType '{entityType}'. " +
$"Register an IScanEventHandler whose SupportedEntityTypes includes it.");
}
/// DI registration helper — call once in Program.cs per module hosting scan handlers.
public static class ScanEventServiceExtensions
{
///
/// Registers IScanEventDispatcher and every IScanEventHandler implementation
/// found in . Safe to call from multiple
/// modules' Program.cs — each module registers only its own assembly's
/// handlers; IScanEventDispatcher itself is process-wide only within a
/// single service's DI container (matching this platform's one-module-
/// per-process hosting model).
///
public static IServiceCollection AddScanEventHandlers(
this IServiceCollection services, params Assembly[] scanAssemblies)
{
services.AddScoped();
if (scanAssemblies is null or { Length: 0 }) return services;
foreach (var assembly in scanAssemblies)
foreach (var type in assembly.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface
&& typeof(IScanEventHandler).IsAssignableFrom(t)))
{
services.AddScoped(typeof(IScanEventHandler), type);
}
return services;
}
}
}