using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;

/// <summary>
/// Parsed native-alarm condition filter.
///
/// <para>
/// A source's <c>conditionFilter</c> is a comma-separated, case-insensitive list
/// of alarm/condition <em>type names</em>, matched against
/// <see cref="NativeAlarmTransition.AlarmTypeName"/>. A <c>null</c>, blank, or
/// all-empty list means "mirror every condition" (the historical behaviour),
/// represented here by <see cref="IsEmpty"/>.
/// </para>
///
/// <para>
/// This is the authoritative <em>client-side</em> gate consulted in the
/// <c>DataConnectionActor</c> routing path, so it applies uniformly across OPC UA
/// (whose server-side <c>WhereClause</c> is only a bandwidth optimisation) and the
/// MxGateway (whose single gateway-wide feed has no server-side filter at all).
/// Parse once at subscribe time; <see cref="IsAllowed"/> is the hot-path check.
/// </para>
/// </summary>
public sealed class AlarmConditionFilter
{
    /// <summary>The shared allow-all instance (empty filter set).</summary>
    public static readonly AlarmConditionFilter AllowAll = new(new HashSet<string>(StringComparer.OrdinalIgnoreCase));

    private readonly HashSet<string> _names;

    private AlarmConditionFilter(HashSet<string> names) => _names = names;

    /// <summary><c>true</c> when no type names are configured — every condition is allowed.</summary>
    public bool IsEmpty => _names.Count == 0;

    /// <summary>The normalized (trimmed) type names, for the OPC UA server-side WhereClause optimisation.</summary>
    public IReadOnlyCollection<string> Names => _names;

    /// <summary>
    /// Parses a raw <c>conditionFilter</c> string into a normalized, case-insensitive
    /// type-name set. <c>null</c>/blank/all-empty input yields an empty (allow-all) filter.
    /// </summary>
    /// <param name="conditionFilter">The raw comma-separated filter string, or <c>null</c>.</param>
    /// <returns>A parsed <see cref="AlarmConditionFilter"/>; never <c>null</c>.</returns>
    public static AlarmConditionFilter Parse(string? conditionFilter)
    {
        if (string.IsNullOrWhiteSpace(conditionFilter))
            return AllowAll;

        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var raw in conditionFilter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            names.Add(raw);

        return names.Count == 0 ? AllowAll : new AlarmConditionFilter(names);
    }

    /// <summary>
    /// Returns <c>true</c> when <paramref name="transition"/> should be delivered:
    /// the filter is empty (allow all), the transition is a framing sentinel
    /// (<see cref="AlarmTransitionKind.SnapshotComplete"/>, which carries no condition
    /// type and must never be swallowed or the snapshot swap never completes), or its
    /// <see cref="NativeAlarmTransition.AlarmTypeName"/> is in the configured set.
    /// </summary>
    /// <param name="transition">The protocol-neutral transition to test.</param>
    /// <returns><c>true</c> to deliver the transition; <c>false</c> to drop it.</returns>
    public bool IsAllowed(NativeAlarmTransition transition)
    {
        if (_names.Count == 0)
            return true;

        // SnapshotComplete is pure framing (no condition payload) — never filter it.
        if (transition.Kind == AlarmTransitionKind.SnapshotComplete)
            return true;

        return _names.Contains(transition.AlarmTypeName);
    }
}
