using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
///
/// Canonical bridge for Site Call Audit operational rows between the
/// wire-format exchanged on the
/// CachedCallTelemetry packet and the in-process
/// persistence entity central writes into the SiteCalls table.
///
///
///
/// This mapper lives in ZB.MOM.WW.ScadaBridge.Communication (which owns the generated
/// and references Commons for
/// ) so both SiteStreamGrpcServer and
/// ZB.MOM.WW.ScadaBridge.AuditLog can share one implementation without the
/// project-reference cycle that would result from hosting it in
/// ZB.MOM.WW.ScadaBridge.AuditLog (AuditLog → Communication, never the reverse).
/// Mirrors the sibling .
///
///
/// Two directions are provided. rehydrates the central
/// entity central writes into the SiteCalls table.
/// projects a site-local
/// onto the wire — used by the Site Call Audit PullSiteCalls
/// reconciliation handler (the central→site self-heal pull). The
/// entity itself is never mapped back onto the wire:
/// sites emit operational state from , never
/// from the central , so a SiteCall→DTO method
/// would be dead code.
///
///
/// String nullability convention: proto3 scalar strings cannot be absent, so the
/// optional rehydrates from an empty string back
/// to null. The optional HttpStatus and TerminalAtUtc use proto
/// wrappers so they preserve true null semantics.
///
///
public static class SiteCallDtoMapper
{
///
/// Reconstructs a persistence entity from its
/// wire-format DTO. An empty LastError rehydrates as null; absent
/// HttpStatus/TerminalAtUtc wrappers stay null.
///
///
/// is stamped here as a placeholder
/// (); the central ingest actor overwrites it
/// inside the dual-write transaction so the AuditLog and SiteCalls rows
/// share one instant. The value sent on the wire is informational only.
///
/// The wire-format site call DTO to map.
/// A entity populated from the DTO fields.
public static SiteCall FromDto(SiteCallOperationalDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new SiteCall
{
TrackedOperationId = TrackedOperationId.Parse(dto.TrackedOperationId),
Channel = dto.Channel,
Target = dto.Target,
SourceSite = dto.SourceSite,
SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode,
Status = dto.Status,
RetryCount = dto.RetryCount,
LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError,
HttpStatus = dto.HttpStatus,
CreatedAtUtc = DateTime.SpecifyKind(dto.CreatedAtUtc.ToDateTime(), DateTimeKind.Utc),
UpdatedAtUtc = DateTime.SpecifyKind(dto.UpdatedAtUtc.ToDateTime(), DateTimeKind.Utc),
TerminalAtUtc = dto.TerminalAtUtc is null
? null
: DateTime.SpecifyKind(dto.TerminalAtUtc.ToDateTime(), DateTimeKind.Utc),
IngestedAtUtc = DateTime.UtcNow, // overwritten by AuditLogIngestActor
};
}
///
/// Projects a site-local onto its
/// wire-format DTO for the Site Call Audit PullSiteCalls
/// reconciliation RPC. The inverse of ; null
/// /
/// collapse to empty strings (proto3 scalar strings cannot be absent), while
/// the nullable HttpStatus and TerminalAtUtc stay unset on the
/// wire so true-null semantics survive the round-trip back through
/// .
///
/// The site-local operational state to project to wire format.
/// A populated ready for transmission.
public static SiteCallOperationalDto ToDto(SiteCallOperational operational)
{
ArgumentNullException.ThrowIfNull(operational);
var dto = new SiteCallOperationalDto
{
TrackedOperationId = operational.TrackedOperationId.ToString(),
Channel = operational.Channel,
Target = operational.Target,
SourceSite = operational.SourceSite,
SourceNode = operational.SourceNode ?? string.Empty,
Status = operational.Status,
RetryCount = operational.RetryCount,
LastError = operational.LastError ?? string.Empty,
CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.CreatedAtUtc)),
UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.UpdatedAtUtc)),
};
if (operational.HttpStatus.HasValue)
{
dto.HttpStatus = operational.HttpStatus.Value;
}
if (operational.TerminalAtUtc.HasValue)
{
dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.TerminalAtUtc.Value));
}
return dto;
}
// All ScadaBridge timestamps are UTC by invariant; Timestamp.FromDateTime
// requires UTC kind. Specify (never convert) so a row read back from SQLite
// with Kind=Utc passes through and a defensively-unspecified value is
// treated as the UTC it already is. Mirrors AuditEventDtoMapper.EnsureUtc.
private static DateTime EnsureUtc(DateTime value) =>
value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc);
}