using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.Stability;

/// <summary>
///     Demand-aware driver-wedge detector per <c>docs/v2/plan.md</c>.
///     Flips a driver to <see cref="WedgeVerdict.Faulted"/> only when BOTH of the following hold:
///     (a) there is pending work outstanding, AND (b) no progress has been observed for longer
///     than <see cref="Threshold"/>. Idle drivers, write-only burst drivers, and subscription-only
///     drivers whose signals don't arrive regularly all stay Healthy.
/// </summary>
/// <remarks>
///     <para>Pending work signal is supplied by the caller via <see cref="DemandSignal"/>:
///     non-zero Polly bulkhead depth, ≥1 active MonitoredItem, or ≥1 queued historian read
///     each qualifies. The detector itself is state-light: all it remembers is the last
///     <c>LastProgressUtc</c> it saw and the last wedge verdict. No history buffer.</para>
///
///     <para>Default threshold per plan: <c>5 × PublishingInterval</c>, with a minimum of 60 s.
///     Concrete values are driver-agnostic and configured per-instance by the caller.</para>
/// </remarks>
public sealed class WedgeDetector
{
    /// <summary>Wedge-detection threshold; pass &lt; 60 s and the detector clamps to 60 s.</summary>
    public TimeSpan Threshold { get; }

    /// <summary>
    ///     Construct with the wedge-detection threshold; values below 60 s clamp to 60 s so
    ///     the detector never fires below the documented floor.
    /// </summary>
    /// <param name="threshold">
    ///     Time without a successful unit of work after which a Healthy driver with pending
    ///     work is considered Faulted. Clamped to a minimum of 60 s per the plan-default of
    ///     5 × PublishingInterval.
    /// </param>
    public WedgeDetector(TimeSpan threshold)
    {
        Threshold = threshold < TimeSpan.FromSeconds(60) ? TimeSpan.FromSeconds(60) : threshold;
    }

    /// <summary>
    ///     Classify the current state against the demand signal. Does not retain state across
    ///     calls — each call is self-contained; the caller owns the <c>LastProgressUtc</c> clock.
    /// </summary>
    /// <param name="state">The current driver state.</param>
    /// <param name="demand">The current demand signal snapshot.</param>
    /// <param name="utcNow">The current UTC time.</param>
    /// <returns>The wedge verdict for the given state and demand.</returns>
    public WedgeVerdict Classify(DriverState state, DemandSignal demand, DateTime utcNow)
    {
        if (state != DriverState.Healthy)
            return WedgeVerdict.NotApplicable;

        if (!demand.HasPendingWork)
            return WedgeVerdict.Idle;

        var sinceProgress = utcNow - demand.LastProgressUtc;
        return sinceProgress > Threshold ? WedgeVerdict.Faulted : WedgeVerdict.Healthy;
    }
}

/// <summary>
///     Caller-supplied demand snapshot. All three counters are OR'd — any non-zero means work
///     is outstanding, which is the trigger for checking the <see cref="LastProgressUtc"/> clock.
/// </summary>
/// <param name="BulkheadDepth">Polly bulkhead depth (in-flight capability calls).</param>
/// <param name="ActiveMonitoredItems">Number of live OPC UA MonitoredItems bound to this driver.</param>
/// <param name="QueuedHistoryReads">Pending historian-read requests the driver owes the server.</param>
/// <param name="LastProgressUtc">Last time the driver reported a successful unit of work (read, subscribe-ack, publish).</param>
public readonly record struct DemandSignal(
    int BulkheadDepth,
    int ActiveMonitoredItems,
    int QueuedHistoryReads,
    DateTime LastProgressUtc)
{
    /// <summary>True when any of the three counters is &gt; 0.</summary>
    public bool HasPendingWork => BulkheadDepth > 0 || ActiveMonitoredItems > 0 || QueuedHistoryReads > 0;
}

/// <summary>Outcome of a single <see cref="WedgeDetector.Classify"/> call.</summary>
public enum WedgeVerdict
{
    /// <summary>Driver wasn't Healthy to begin with — wedge detection doesn't apply.</summary>
    NotApplicable,

    /// <summary>Driver claims Healthy + no pending work → stays Healthy.</summary>
    Idle,

    /// <summary>Driver claims Healthy + has pending work + has made progress within the threshold → stays Healthy.</summary>
    Healthy,

    /// <summary>Driver claims Healthy + has pending work + has NOT made progress within the threshold → wedged.</summary>
    Faulted,
}
