namespace Mbproxy.Proxy;

/// <summary>
/// Immutable snapshot of per-PLC counters. Consumed by the status page.
/// All fields are point-in-time reads; no ordering guarantees across fields.
///
/// <para><b>Backwards-compat policy:</b> fields are <i>added</i>, never
/// renamed or removed.</para>
/// </summary>
public sealed record CounterSnapshot(
    long PdusForwarded,
    long Fc03,
    long Fc04,
    long Fc06,
    long Fc16,
    long FcOther,
    long RewrittenSlots,
    long PartialBcdWarnings,
    long InvalidBcdWarnings,
    long BackendException01,
    long BackendException02,
    long BackendException03,
    long BackendException04,
    long BackendExceptionOther,
    long BytesUpstreamIn,
    long BytesUpstreamOut,
    /// <summary>
    /// Total number of failed listener bind attempts over the lifetime of the supervisor.
    /// Accumulates; never resets. See <see cref="SupervisorSnapshot.RecoveryAttempts"/> doc.
    /// </summary>
    long RecoveryAttempts,
    /// <summary>
    /// Most recent bind failure message (up to 256 chars); <c>null</c> if the listener
    /// has never failed to bind.
    /// </summary>
    string? LastBindError,
    /// <summary>
    /// EWMA of recent backend round-trip times in milliseconds (α = 0.2).
    /// Zero when no successful round-trips have been observed yet.
    /// Stored internally as fixed-point microseconds (long * 1000) for Interlocked
    /// compatibility; converted to double ms on snapshot.
    /// </summary>
    double LastRoundTripMs,
    /// <summary>
    /// Number of backend connections successfully established (Polly final success).
    /// </summary>
    long ConnectsSuccess,
    /// <summary>
    /// Number of backend connections that failed on all Polly attempts.
    /// </summary>
    long ConnectsFailed,
    /// <summary>
    /// Number of Modbus requests currently in flight on this PLC's multiplexed backend
    /// connection (point-in-time snapshot of the correlation map size).
    /// </summary>
    long InFlightCount,
    /// <summary>
    /// Peak <see cref="InFlightCount"/> observed since the multiplexer was constructed.
    /// Updated via <see cref="Interlocked"/> CAS so concurrent in-flight increments do
    /// not lose the high-water mark.
    /// </summary>
    long MaxInFlight,
    /// <summary>
    /// Number of times the per-PLC TxId allocator's rolling cursor has wrapped
    /// 0xFFFF → 0x0000. A non-zero value is benign; a sudden burst suggests extreme
    /// in-flight churn.
    /// </summary>
    long TxIdWraps,
    /// <summary>
    /// Cumulative count of upstream pipes closed as a side effect of a backend
    /// disconnect. Each backend reconnect cycle adds the number of attached upstream
    /// clients at the time of the disconnect.
    /// </summary>
    long BackendDisconnectCascades,
    /// <summary>
    /// Current depth of the per-PLC outbound channel feeding the backend writer task
    /// (frames queued, not yet on the wire). A sustained non-zero value indicates the
    /// backend is slower than upstream demand.
    /// </summary>
    long BackendQueueDepth,
    /// <summary>
    /// Cumulative count of FC03/FC04 requests that attached to an already-in-flight
    /// peer instead of opening a fresh backend round-trip.
    /// <c>CoalescedHitCount + CoalescedMissCount</c> equals total FC03/FC04 requests
    /// seen by the multiplexer.
    /// </summary>
    long CoalescedHitCount,
    /// <summary>
    /// Cumulative count of FC03/FC04 requests that opened a fresh in-flight entry (no
    /// matching peer was in flight, or the matching peer had reached its
    /// <c>MaxParties</c> cap). With <c>ReadCoalescing.Enabled = false</c>, every
    /// FC03/FC04 request becomes a miss.
    /// </summary>
    long CoalescedMissCount,
    /// <summary>
    /// Count of coalesced response fan-outs that were skipped because the attached
    /// upstream pipe had already disconnected. A spike is a churn indicator; the metric
    /// itself is informational.
    /// </summary>
    long CoalescedResponseToDeadUpstream,
    /// <summary>
    /// Cumulative count of FC03/FC04 requests served from the response cache.
    /// <c>CacheHitCount + CacheMissCount</c> equals total FC03/FC04 requests whose
    /// resolved TTL was &gt; 0 (cache-eligible). Reads against tags with TTL = 0
    /// increment neither.
    /// </summary>
    long CacheHitCount,
    /// <summary>
    /// Cumulative count of cache-eligible FC03/FC04 requests that fell through to
    /// coalescing / backend (no fresh entry was present or the entry had expired).
    /// </summary>
    long CacheMissCount,
    /// <summary>
    /// Cumulative count of cache entries invalidated by overlapping FC06/FC16 write
    /// responses. A high rate suggests caching is fighting writes; consider lower TTLs
    /// on cache-overlapping tags.
    /// </summary>
    long CacheInvalidations,
    /// <summary>
    /// Point-in-time snapshot of the per-PLC <see cref="Cache.ResponseCache"/> entry
    /// count. Read on the snapshot path; 0 when no cache is wired.
    /// </summary>
    long CacheEntryCount,
    /// <summary>
    /// Point-in-time approximation of cached PDU bytes for this PLC. Sum of
    /// <see cref="Cache.CacheEntry.Length"/> across entries. Read on the snapshot path.
    /// </summary>
    long CacheBytes,
    /// <summary>
    /// Cumulative count of backend response frames the per-PLC reader task dropped
    /// because the destination upstream pipe's bounded response channel was full. A
    /// non-zero value indicates one or more upstream clients are not draining their
    /// socket fast enough to keep up with the backend; the wedged client loses its own
    /// responses but its peers on the same PLC continue to receive theirs.
    /// </summary>
    long ResponseDropForFullUpstream,
    /// <summary>
    /// Cumulative count of backend keepalive heartbeat probes issued (synthetic FC03
    /// qty=1 reads sent on an idle backend socket).
    /// </summary>
    long BackendHeartbeatsSent,
    /// <summary>
    /// Cumulative count of backend keepalive heartbeat probes that were not answered
    /// within <c>BackendRequestTimeoutMs</c>. Each failure triggers a proactive backend
    /// teardown (see <see cref="BackendIdleDisconnects"/>).
    /// </summary>
    long BackendHeartbeatsFailed,
    /// <summary>
    /// Cumulative count of backend teardowns triggered by a failed keepalive heartbeat.
    /// Distinct from <see cref="BackendDisconnectCascades"/> (which counts cascaded
    /// pipes); this counts the disconnect <i>events</i> attributed to keepalive.
    /// </summary>
    long BackendIdleDisconnects);

/// <summary>
/// Thread-safe per-PLC counters backed by <see cref="System.Threading.Interlocked"/> longs.
/// All increment methods are allocation-free (no boxing, no heap traffic on the hot path).
/// <see cref="Snapshot"/> may allocate (record construction) — it is off-path (status page only).
/// </summary>
internal sealed class ProxyCounters
{
    // ── Hot-path fields (Interlocked longs) ─────────────────────────────────

    private long _pdusForwarded;
    private long _fc03;
    private long _fc04;
    private long _fc06;
    private long _fc16;
    private long _fcOther;
    private long _rewrittenSlots;
    private long _partialBcdWarnings;
    private long _invalidBcdWarnings;
    private long _backendException01;
    private long _backendException02;
    private long _backendException03;
    private long _backendException04;
    private long _backendExceptionOther;
    private long _bytesUpstreamIn;
    private long _bytesUpstreamOut;
    private long _recoveryAttempts;
    private long _connectsSuccess;
    private long _connectsFailed;

    // Multiplexer telemetry.
    private long _maxInFlight;
    private long _backendDisconnectCascades;

    // Coalescing counters. Hit + Miss = total FC03/FC04 requests.
    private long _coalescedHitCount;
    private long _coalescedMissCount;
    private long _coalescedResponseToDeadUpstream;

    // Response-cache counters. Hit + Miss = total cache-eligible FC03/FC04.
    private long _cacheHitCount;
    private long _cacheMissCount;
    private long _cacheInvalidations;

    // Backend-reader fan-out drop counter. Increments when the reader task tried to
    // enqueue a response to an upstream pipe whose bounded response channel was full.
    // Without the non-blocking enqueue this would deadlock the reader; with it we drop
    // and account.
    private long _responseDropForFullUpstream;

    // Backend keepalive heartbeat counters.
    private long _backendHeartbeatsSent;
    private long _backendHeartbeatsFailed;
    private long _backendIdleDisconnects;

    // Live cache state pulled from a per-PLC ResponseCache on each snapshot. The
    // multiplexer registers a single provider via SetCacheStatsProvider so the status
    // page sees current entry-count / bytes without a separate poll.
    private volatile ICacheStatsProvider? _cacheStatsProvider;

    // Live state pulled from the multiplexer's allocator/map/queue on each snapshot.
    // The multiplexer registers a single provider via SetMultiplexProvider. We use a
    // volatile reference for lock-free read on the snapshot path.
    private volatile IMultiplexCountersProvider? _multiplexProvider;
    // LastBindError is a string (not a long); accessed via volatile field on ProxyCounters
    // but actually stored on the supervisor. We expose it here for snapshot parity.
    // Supervisor sets this via SetLastBindError; Snapshot reads it.
    private volatile string? _lastBindError;

    // EWMA round-trip: stored as fixed-point microseconds (value * 1000) so we can use
    // Interlocked.CompareExchange on a long. The EWMA smoothing factor α = 0.2 gives a
    // half-life of ~3 samples (responds quickly to changes without being noisy).
    // Updated by PlcMultiplexer on each successful response (request→response round-trip,
    // measured against InFlightRequest.SentAtUtc).
    // 0 = no samples observed yet.
    private long _lastRoundTripUsEwma; // fixed-point microseconds

    // ── Increment methods ────────────────────────────────────────────────────

    public void IncrementPdusForwarded()
        => Interlocked.Increment(ref _pdusForwarded);

    public void IncrementFcCount(byte fc)
    {
        switch (fc)
        {
            case 0x03: Interlocked.Increment(ref _fc03);    break;
            case 0x04: Interlocked.Increment(ref _fc04);    break;
            case 0x06: Interlocked.Increment(ref _fc06);    break;
            case 0x10: Interlocked.Increment(ref _fc16);    break;
            default:   Interlocked.Increment(ref _fcOther); break;
        }
    }

    public void AddRewrittenSlots(int n)
        => Interlocked.Add(ref _rewrittenSlots, n);

    public void IncrementPartialBcd()
        => Interlocked.Increment(ref _partialBcdWarnings);

    public void IncrementInvalidBcd()
        => Interlocked.Increment(ref _invalidBcdWarnings);

    /// <summary>
    /// Increments the backend-exception counter for the given Modbus exception code.
    /// Codes 1–4 map to individual counters; anything else goes to "Other".
    /// </summary>
    public void IncrementBackendException(byte code)
    {
        switch (code)
        {
            case 1: Interlocked.Increment(ref _backendException01);    break;
            case 2: Interlocked.Increment(ref _backendException02);    break;
            case 3: Interlocked.Increment(ref _backendException03);    break;
            case 4: Interlocked.Increment(ref _backendException04);    break;
            default: Interlocked.Increment(ref _backendExceptionOther); break;
        }
    }

    /// <summary>
    /// Adds byte counts for both upstream directions atomically.
    /// </summary>
    public void AddBytes(long up, long down)
    {
        Interlocked.Add(ref _bytesUpstreamIn,  up);
        Interlocked.Add(ref _bytesUpstreamOut, down);
    }

    /// <summary>
    /// Records one successful backend TCP connect (Polly pipeline returned success).
    /// </summary>
    public void IncrementConnectSuccess()
        => Interlocked.Increment(ref _connectsSuccess);

    /// <summary>
    /// Records one failed backend TCP connect (all Polly attempts exhausted).
    /// </summary>
    public void IncrementConnectFailed()
        => Interlocked.Increment(ref _connectsFailed);

    /// <summary>
    /// Records <paramref name="n"/> upstream pipes closed by a backend disconnect
    /// cascade.
    /// </summary>
    public void AddDisconnectCascades(int n)
        => Interlocked.Add(ref _backendDisconnectCascades, n);

    /// <summary>
    /// Records one FC03/FC04 request that attached to an already-in-flight peer.
    /// </summary>
    public void IncrementCoalescedHit()
        => Interlocked.Increment(ref _coalescedHitCount);

    /// <summary>
    /// Records one FC03/FC04 request that opened a fresh in-flight entry (no matching
    /// peer was in flight, or the matching peer had reached MaxParties).
    /// </summary>
    public void IncrementCoalescedMiss()
        => Interlocked.Increment(ref _coalescedMissCount);

    /// <summary>
    /// Records one coalesced response fan-out that was skipped because the attached
    /// upstream pipe had already disconnected. Informational only.
    /// </summary>
    public void IncrementCoalescedResponseToDeadUpstream()
        => Interlocked.Increment(ref _coalescedResponseToDeadUpstream);

    /// <summary>Records one FC03/FC04 cache hit.</summary>
    public void IncrementCacheHit()
        => Interlocked.Increment(ref _cacheHitCount);

    /// <summary>Records one cache-eligible FC03/FC04 read that missed.</summary>
    public void IncrementCacheMiss()
        => Interlocked.Increment(ref _cacheMissCount);

    /// <summary>Records <paramref name="n"/> cache entries invalidated by a write.</summary>
    public void AddCacheInvalidations(int n)
        => Interlocked.Add(ref _cacheInvalidations, n);

    /// <summary>
    /// Records one backend response frame dropped because the destination upstream
    /// pipe's response channel was full.
    /// </summary>
    public void IncrementResponseDropForFullUpstream()
        => Interlocked.Increment(ref _responseDropForFullUpstream);

    /// <summary>Records one backend keepalive heartbeat probe sent.</summary>
    public void IncrementBackendHeartbeatSent()
        => Interlocked.Increment(ref _backendHeartbeatsSent);

    /// <summary>Records one backend keepalive heartbeat probe that timed out.</summary>
    public void IncrementBackendHeartbeatFailed()
        => Interlocked.Increment(ref _backendHeartbeatsFailed);

    /// <summary>Records one backend teardown triggered by a failed keepalive heartbeat.</summary>
    public void IncrementBackendIdleDisconnect()
        => Interlocked.Increment(ref _backendIdleDisconnects);

    /// <summary>
    /// Wires the per-PLC <see cref="Cache.ResponseCache"/> as the live stats source for
    /// the snapshot path. Pass <c>null</c> to detach during disposal.
    /// </summary>
    internal void SetCacheStatsProvider(ICacheStatsProvider? provider)
        => _cacheStatsProvider = provider;

    /// <summary>
    /// CAS-updates the peak in-flight high-water mark. Called on every successful
    /// allocation by the multiplexer.
    /// </summary>
    public void ObserveInFlight(int currentInFlight)
    {
        long sample = currentInFlight;
        long old;
        do
        {
            old = Interlocked.Read(ref _maxInFlight);
            if (sample <= old) return;
        }
        while (Interlocked.CompareExchange(ref _maxInFlight, sample, old) != old);
    }

    /// <summary>
    /// Wires the live multiplexer telemetry source into this counter set. Called by
    /// <see cref="Mbproxy.Proxy.Multiplexing.PlcMultiplexer"/> at construction time so
    /// the status page's <see cref="Snapshot"/> can include live in-flight / queue-depth
    /// values without polling the multiplexer separately.
    /// </summary>
    internal void SetMultiplexProvider(IMultiplexCountersProvider? provider)
        => _multiplexProvider = provider;

    /// <summary>
    /// Increments the recovery-attempt counter and records the bind error message
    /// (truncated to 256 chars). Called by the supervisor on each failed bind.
    /// </summary>
    public void IncrementRecoveryAttempt(string errorMessage)
    {
        Interlocked.Increment(ref _recoveryAttempts);
        _lastBindError = errorMessage.Length > 256 ? errorMessage[..256] : errorMessage;
    }

    /// <summary>
    /// Clears the last bind error after a successful bind.
    /// </summary>
    public void ClearLastBindError()
    {
        _lastBindError = null;
    }

    /// <summary>
    /// Updates the EWMA round-trip estimate with a new sample.
    /// Uses α = 0.2: new_ewma = 0.2 * sample + 0.8 * old_ewma.
    /// <paramref name="elapsedTicks"/> is from <see cref="System.Diagnostics.Stopwatch.GetTimestamp"/>.
    /// Thread-safe via CAS loop on a fixed-point microsecond long.
    /// </summary>
    public void UpdateRoundTripEwma(long elapsedTicks)
    {
        // Convert ticks to microseconds.
        double sampleMs = (double)elapsedTicks / System.Diagnostics.Stopwatch.Frequency * 1000.0;

        // Fixed-point: store microseconds * 1000 (i.e. nanoseconds) as long for CAS.
        // This gives ~1 µs resolution which is fine for Modbus round-trips (1–100 ms range).
        long sampleFixed = (long)(sampleMs * 1000.0);

        long old, newVal;
        do
        {
            old = Interlocked.Read(ref _lastRoundTripUsEwma);
            // If no previous sample, seed with first sample; otherwise apply EWMA.
            newVal = old == 0
                ? sampleFixed
                : (long)(0.2 * sampleFixed + 0.8 * old);
        }
        while (Interlocked.CompareExchange(ref _lastRoundTripUsEwma, newVal, old) != old);
    }

    // ── Snapshot (off hot-path, may allocate) ────────────────────────────────

    /// <summary>
    /// Returns a point-in-time snapshot of all counters.
    /// Each field is read atomically via <see cref="Interlocked.Read"/>.
    /// May allocate (record construction); intended for the status-page path only.
    /// </summary>
    public CounterSnapshot Snapshot()
    {
        var provider = _multiplexProvider;
        long inFlightNow  = provider?.InFlightCount ?? 0;
        long txWraps      = provider?.TxIdWraps ?? 0;
        long queueDepth   = provider?.BackendQueueDepth ?? 0;

        var cacheProvider = _cacheStatsProvider;
        long cacheEntries = cacheProvider?.EntryCount ?? 0;
        long cacheBytes   = cacheProvider?.ApproximateBytes ?? 0;

        return new(
            PdusForwarded:             Interlocked.Read(ref _pdusForwarded),
            Fc03:                      Interlocked.Read(ref _fc03),
            Fc04:                      Interlocked.Read(ref _fc04),
            Fc06:                      Interlocked.Read(ref _fc06),
            Fc16:                      Interlocked.Read(ref _fc16),
            FcOther:                   Interlocked.Read(ref _fcOther),
            RewrittenSlots:            Interlocked.Read(ref _rewrittenSlots),
            PartialBcdWarnings:        Interlocked.Read(ref _partialBcdWarnings),
            InvalidBcdWarnings:        Interlocked.Read(ref _invalidBcdWarnings),
            BackendException01:        Interlocked.Read(ref _backendException01),
            BackendException02:        Interlocked.Read(ref _backendException02),
            BackendException03:        Interlocked.Read(ref _backendException03),
            BackendException04:        Interlocked.Read(ref _backendException04),
            BackendExceptionOther:     Interlocked.Read(ref _backendExceptionOther),
            BytesUpstreamIn:           Interlocked.Read(ref _bytesUpstreamIn),
            BytesUpstreamOut:          Interlocked.Read(ref _bytesUpstreamOut),
            RecoveryAttempts:          Interlocked.Read(ref _recoveryAttempts),
            LastBindError:             _lastBindError,
            LastRoundTripMs:           Interlocked.Read(ref _lastRoundTripUsEwma) / 1000.0,
            ConnectsSuccess:           Interlocked.Read(ref _connectsSuccess),
            ConnectsFailed:            Interlocked.Read(ref _connectsFailed),
            InFlightCount:             inFlightNow,
            MaxInFlight:               Interlocked.Read(ref _maxInFlight),
            TxIdWraps:                 txWraps,
            BackendDisconnectCascades: Interlocked.Read(ref _backendDisconnectCascades),
            BackendQueueDepth:         queueDepth,
            CoalescedHitCount:         Interlocked.Read(ref _coalescedHitCount),
            CoalescedMissCount:        Interlocked.Read(ref _coalescedMissCount),
            CoalescedResponseToDeadUpstream: Interlocked.Read(ref _coalescedResponseToDeadUpstream),
            CacheHitCount:             Interlocked.Read(ref _cacheHitCount),
            CacheMissCount:            Interlocked.Read(ref _cacheMissCount),
            CacheInvalidations:        Interlocked.Read(ref _cacheInvalidations),
            CacheEntryCount:           cacheEntries,
            CacheBytes:                cacheBytes,
            ResponseDropForFullUpstream: Interlocked.Read(ref _responseDropForFullUpstream),
            BackendHeartbeatsSent:     Interlocked.Read(ref _backendHeartbeatsSent),
            BackendHeartbeatsFailed:   Interlocked.Read(ref _backendHeartbeatsFailed),
            BackendIdleDisconnects:    Interlocked.Read(ref _backendIdleDisconnects));
    }
}

/// <summary>
/// Read-only window into the per-PLC multiplexer's live state (allocator counts,
/// outbound-queue depth). Implemented by <see cref="Mbproxy.Proxy.Multiplexing.PlcMultiplexer"/>
/// and registered with <see cref="ProxyCounters.SetMultiplexProvider"/> so
/// <see cref="ProxyCounters.Snapshot"/> can include live mux telemetry without holding
/// a direct reference to the multiplexer (which would couple counter snapshots to the
/// connection layer's lifecycle).
/// </summary>
internal interface IMultiplexCountersProvider
{
    /// <summary>Number of currently-in-flight requests on the backend socket.</summary>
    long InFlightCount { get; }

    /// <summary>Cumulative 0xFFFF → 0x0000 wrap events from the TxId allocator.</summary>
    long TxIdWraps { get; }

    /// <summary>Current depth of the outbound channel (frames queued for the backend writer).</summary>
    long BackendQueueDepth { get; }
}

/// <summary>
/// Read-only window into the per-PLC <see cref="Cache.ResponseCache"/>'s live state
/// for the snapshot path. The multiplexer wires this on cache construction so the
/// status page sees live counts without holding a direct reference to the cache.
/// </summary>
internal interface ICacheStatsProvider
{
    /// <summary>Current cache entry count.</summary>
    long EntryCount { get; }

    /// <summary>Approximation of cached PDU bytes (sum of <see cref="Cache.CacheEntry.Length"/>).</summary>
    long ApproximateBytes { get; }
}
