using System.Collections.Immutable;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;

/// <summary>
/// UI-side binding model for <see cref="AuditFilterBar"/>.
///
/// <para>
/// The model mirrors <see cref="AuditLogQueryFilter"/> but allows multi-select chip
/// state for Channel / Kind / Status / Site (each a <see cref="HashSet{T}"/>) 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.
/// </para>
///
/// <para>
/// The repository filter contract (<see cref="AuditLogQueryFilter"/>) is multi-value
/// per dimension: the chip multi-selects map straight through to the
/// <c>Channels</c> / <c>Kinds</c> / <c>Statuses</c> / <c>SourceSiteIds</c> filter
/// lists when the model is published via <see cref="ToFilter"/> — an empty set means
/// "do not constrain". Instance and Script free-text remain UI-only: the underlying
/// filter has no matching columns, so they are dropped when the model is published.
/// </para>
///
/// <para>
/// The Errors-only toggle is a convenience: when true AND no explicit Status chips
/// are selected, <see cref="ToFilter"/> targets the full error-status set
/// {<see cref="AuditStatus.Failed"/>, <see cref="AuditStatus.Parked"/>,
/// <see cref="AuditStatus.Discarded"/>}. When Status chips ARE selected the toggle
/// is a no-op — the explicit Status chips win.
/// </para>
/// </summary>
public sealed class AuditQueryModel
{
    /// <summary>Selected channel filter chips; empty means all channels.</summary>
    public HashSet<AuditChannel> Channels { get; } = new();
    /// <summary>Selected kind filter chips; empty means all kinds.</summary>
    public HashSet<AuditKind> Kinds { get; } = new();
    /// <summary>Selected status filter chips; empty means all statuses.</summary>
    public HashSet<AuditStatus> Statuses { get; } = new();
    /// <summary>Selected source-site identifier chips; empty means all sites.</summary>
    public HashSet<string> SiteIdentifiers { get; } = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Selected source-node identifiers (e.g. <c>"central-a"</c>,
    /// <c>"site-plant-a-node-a"</c>). Mirrors <see cref="SiteIdentifiers"/> —
    /// chip multi-select state, empty = "do not constrain", mapped through to
    /// <see cref="AuditLogQueryFilter.SourceNodes"/> by <see cref="ToFilter"/>.
    /// </summary>
    public HashSet<string> SourceNodes { get; } = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>Selected time-range preset controlling which historical window is queried.</summary>
    public AuditTimeRangePreset TimeRange { get; set; } = AuditTimeRangePreset.LastHour;
    /// <summary>Custom start of the time window; used only when <see cref="TimeRange"/> is <see cref="AuditTimeRangePreset.Custom"/>.</summary>
    public DateTime? CustomFromUtc { get; set; }
    /// <summary>Custom end of the time window; used only when <see cref="TimeRange"/> is <see cref="AuditTimeRangePreset.Custom"/>.</summary>
    public DateTime? CustomToUtc { get; set; }

    /// <summary>Free-text filter applied to instance names (UI-only; dropped when converting to <see cref="AuditLogQueryFilter"/>).</summary>
    public string InstanceSearch { get; set; } = string.Empty;
    /// <summary>Free-text filter applied to script names (UI-only; dropped when converting to <see cref="AuditLogQueryFilter"/>).</summary>
    public string ScriptSearch { get; set; } = string.Empty;
    /// <summary>Free-text filter applied to the target field (external system / DB name / notification list).</summary>
    public string TargetSearch { get; set; } = string.Empty;
    /// <summary>Free-text filter applied to the actor field (instance or inbound API key name).</summary>
    public string ActorSearch { get; set; } = string.Empty;

    /// <summary>
    /// Paste-in ExecutionId filter — the operator pastes the universal per-run
    /// correlation Guid. Stored as free text; <see cref="ToFilter"/> lax-parses it
    /// through <see cref="Guid.TryParse(string?, out Guid)"/> so a blank or
    /// unparseable value simply yields no constraint.
    /// </summary>
    public string ExecutionId { get; set; } = string.Empty;

    /// <summary>
    /// Paste-in ParentExecutionId filter — the operator pastes the spawner
    /// execution's Guid to find every run it spawned. Stored as free text;
    /// <see cref="ToFilter"/> lax-parses it through
    /// <see cref="Guid.TryParse(string?, out Guid)"/> so a blank or unparseable
    /// value simply yields no constraint, mirroring <see cref="ExecutionId"/>.
    /// </summary>
    public string ParentExecutionId { get; set; } = string.Empty;

    /// <summary>When true and no explicit status chips are selected, the filter targets the full non-success status set.</summary>
    public bool ErrorsOnly { get; set; }

    /// <summary>
    /// Maps each channel to the kinds it can emit (per Component-AuditLog.md §4).
    /// <c>CachedSubmit</c> and <c>CachedResolve</c> appear under both
    /// <see cref="AuditChannel.ApiOutbound"/> and <see cref="AuditChannel.DbOutbound"/>
    /// 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.
    /// </summary>
    public static readonly IReadOnlyDictionary<AuditChannel, ImmutableList<AuditKind>> KindsByChannel =
        new Dictionary<AuditChannel, ImmutableList<AuditKind>>
        {
            [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),
        };

    /// <summary>
    /// 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).
    /// </summary>
    /// <returns>The deduplicated, order-stable list of <see cref="AuditKind"/> values applicable to the selected channels.</returns>
    public IReadOnlyList<AuditKind> VisibleKinds()
    {
        if (Channels.Count == 0)
        {
            return Enum.GetValues<AuditKind>();
        }

        var seen = new HashSet<AuditKind>();
        var result = new List<AuditKind>();
        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;
    }

    /// <summary>
    /// Publishes this UI model as the repository's multi-value filter: each chip
    /// multi-select maps straight through to its filter list (an empty set yields
    /// <c>null</c> — "do not constrain"). See class doc for the Errors-only rule.
    /// </summary>
    /// <param name="utcNow">The current UTC timestamp used to compute relative time-range windows.</param>
    /// <returns>A populated <see cref="AuditLogQueryFilter"/> ready for the repository.</returns>
    public AuditLogQueryFilter ToFilter(DateTime utcNow)
    {
        var statuses = ResolveStatuses();

        var (fromUtc, toUtc) = ResolveTimeWindow(utcNow);

        // Lax-parse the pasted ExecutionId — blank or malformed text yields no
        // constraint rather than an error, mirroring the optional-filter contract.
        Guid? executionId = Guid.TryParse(ExecutionId, out var parsedExecutionId)
            ? parsedExecutionId
            : null;

        // Same lax-parse contract for the pasted ParentExecutionId.
        Guid? parentExecutionId = Guid.TryParse(ParentExecutionId, out var parsedParentExecutionId)
            ? parsedParentExecutionId
            : null;

        return new AuditLogQueryFilter(
            Channels: Channels.Count > 0 ? Channels.ToArray() : null,
            Kinds: Kinds.Count > 0 ? Kinds.ToArray() : null,
            Statuses: statuses,
            SourceSiteIds: SiteIdentifiers.Count > 0 ? SiteIdentifiers.ToArray() : null,
            Target: string.IsNullOrWhiteSpace(TargetSearch) ? null : TargetSearch.Trim(),
            Actor: string.IsNullOrWhiteSpace(ActorSearch) ? null : ActorSearch.Trim(),
            CorrelationId: null,
            ExecutionId: executionId,
            ParentExecutionId: parentExecutionId,
            FromUtc: fromUtc,
            ToUtc: toUtc,
            SourceNodes: SourceNodes.Count > 0 ? SourceNodes.ToArray() : null);
    }

    /// <summary>The non-success statuses targeted by the Errors-only toggle.</summary>
    private static readonly AuditStatus[] ErrorStatuses =
        { AuditStatus.Failed, AuditStatus.Parked, AuditStatus.Discarded };

    private IReadOnlyList<AuditStatus>? ResolveStatuses()
    {
        if (Statuses.Count > 0)
        {
            // Explicit chips win — Errors-only is a no-op.
            return Statuses.ToArray();
        }

        if (ErrorsOnly)
        {
            // Multi-value filter: Errors-only targets the full non-success set.
            return ErrorStatuses;
        }

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

/// <summary>
/// Time-range presets surfaced in the filter bar. <see cref="Custom"/> reveals the
/// FromUtc / ToUtc datetime pickers; the other presets compute From relative to
/// "now" at the moment Apply is clicked.
/// </summary>
public enum AuditTimeRangePreset
{
    Last5Minutes,
    LastHour,
    Last24Hours,
    Custom,
}
