using System.Collections.Immutable;
using ScadaLink.Commons.Types.Audit;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.CentralUI.Components.Audit;
///
/// UI-side binding model for (#23 M7-T2).
///
///
/// The model mirrors but allows multi-select chip
/// state for Channel / Kind / Status / Site (each a ) plus
/// extra UI-only fields the underlying filter does not carry: the Errors-only toggle,
/// the time-range preset, and free-text Instance / Script searches.
///
///
///
/// The repository filter contract () is single-value
/// per dimension today; the chip multi-selects therefore collapse to the FIRST
/// selected chip when the model is published via . That is a
/// deliberate Bundle B scope decision — the chip UI is preserved so a follow-up can
/// either repeat the query per chip or widen the filter contract without rewriting
/// the form. Instance and Script free-text are also UI-only today: the underlying
/// filter has no matching columns, so they are dropped during collapse.
///
///
///
/// The Errors-only toggle is a convenience: when true AND no explicit Status chips
/// are selected, the collapsed filter pins (the
/// first of {Failed, Parked, Discarded}). When Status chips ARE selected the toggle
/// is a no-op — the explicit Status filter wins.
///
///
public sealed class AuditQueryModel
{
public HashSet Channels { get; } = new();
public HashSet Kinds { get; } = new();
public HashSet Statuses { get; } = new();
public HashSet SiteIdentifiers { get; } = new(StringComparer.OrdinalIgnoreCase);
public AuditTimeRangePreset TimeRange { get; set; } = AuditTimeRangePreset.LastHour;
public DateTime? CustomFromUtc { get; set; }
public DateTime? CustomToUtc { get; set; }
public string InstanceSearch { get; set; } = string.Empty;
public string ScriptSearch { get; set; } = string.Empty;
public string TargetSearch { get; set; } = string.Empty;
public string ActorSearch { get; set; } = string.Empty;
public bool ErrorsOnly { get; set; }
///
/// Maps each channel to the kinds it can emit (per Component-AuditLog.md §4).
/// CachedSubmit and CachedResolve appear under both
/// and
/// because the cached-call lifecycle is channel-agnostic at submit/resolve time.
/// Used by the filter bar to narrow the Kind chip list once Channels are picked.
///
public static readonly IReadOnlyDictionary> KindsByChannel =
new Dictionary>
{
[AuditChannel.ApiOutbound] = ImmutableList.Create(
AuditKind.ApiCall, AuditKind.ApiCallCached,
AuditKind.CachedSubmit, AuditKind.CachedResolve),
[AuditChannel.DbOutbound] = ImmutableList.Create(
AuditKind.DbWrite, AuditKind.DbWriteCached,
AuditKind.CachedSubmit, AuditKind.CachedResolve),
[AuditChannel.Notification] = ImmutableList.Create(
AuditKind.NotifySend, AuditKind.NotifyDeliver),
[AuditChannel.ApiInbound] = ImmutableList.Create(
AuditKind.InboundRequest, AuditKind.InboundAuthFailure),
};
///
/// Returns the kinds visible in the Kind chip list given the currently selected
/// Channels. With no Channel selected, all 10 kinds are visible (no narrowing).
/// With one or more Channels selected, the union of the channel-specific kind
/// lists is returned (deduplicated and order-stable on first-seen).
///
public IReadOnlyList VisibleKinds()
{
if (Channels.Count == 0)
{
return Enum.GetValues();
}
var seen = new HashSet();
var result = new List();
foreach (var ch in Channels)
{
if (!KindsByChannel.TryGetValue(ch, out var kinds))
{
continue;
}
foreach (var k in kinds)
{
if (seen.Add(k))
{
result.Add(k);
}
}
}
return result;
}
///
/// Collapses this UI model to the repository's single-value filter.
/// See class doc for the multi-select → single-value contract.
///
public AuditLogQueryFilter ToFilter(DateTime utcNow)
{
var status = ResolveStatus();
var (fromUtc, toUtc) = ResolveTimeWindow(utcNow);
return new AuditLogQueryFilter(
Channel: Channels.Count > 0 ? Channels.First() : null,
Kind: Kinds.Count > 0 ? Kinds.First() : null,
Status: status,
SourceSiteId: SiteIdentifiers.Count > 0 ? SiteIdentifiers.First() : null,
Target: string.IsNullOrWhiteSpace(TargetSearch) ? null : TargetSearch.Trim(),
Actor: string.IsNullOrWhiteSpace(ActorSearch) ? null : ActorSearch.Trim(),
CorrelationId: null,
FromUtc: fromUtc,
ToUtc: toUtc);
}
private AuditStatus? ResolveStatus()
{
if (Statuses.Count > 0)
{
// Explicit chips win — Errors-only is a no-op.
return Statuses.First();
}
if (ErrorsOnly)
{
// Single-value filter contract: Failed is the lead non-success status.
// When the filter widens to multi-value the full {Failed, Parked, Discarded}
// set will flow through.
return AuditStatus.Failed;
}
return null;
}
private (DateTime? From, DateTime? To) ResolveTimeWindow(DateTime utcNow)
{
return TimeRange switch
{
AuditTimeRangePreset.Last5Minutes => (utcNow.AddMinutes(-5), null),
AuditTimeRangePreset.LastHour => (utcNow.AddHours(-1), null),
AuditTimeRangePreset.Last24Hours => (utcNow.AddHours(-24), null),
AuditTimeRangePreset.Custom => (CustomFromUtc, CustomToUtc),
_ => (null, null),
};
}
}
///
/// Time-range presets surfaced in the filter bar. reveals the
/// FromUtc / ToUtc datetime pickers; the other presets compute From relative to
/// "now" at the moment Apply is clicked.
///
public enum AuditTimeRangePreset
{
Last5Minutes,
LastHour,
Last24Hours,
Custom,
}