namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;

/// <summary>
///     The seam between the MTConnect driver and an MTConnect Agent's HTTP surface
///     (<c>/probe</c>, <c>/current</c>, <c>/sample</c>). This interface is transport-neutral by
///     design — every method returns/yields the plain DTOs in <c>MTConnectDtos.cs</c>, never a
///     TrakHound type, an <c>XElement</c>, or any other wire/library artifact. That is what lets
///     every driver behaviour above this seam (Tasks 6–13) be unit-tested against a fake serving
///     canned probe/current/sample XML, with no sockets involved.
/// </summary>
/// <remarks>
///     <para>
///     The production implementation, <see cref="MTConnectAgentClient"/>, carries no backend NuGet
///     at all: it is an <see cref="HttpClient"/> over the Agent's three request paths, feeding
///     hand-rolled <c>System.Xml.Linq</c> parsers (<c>MTConnectProbeParser</c> /
///     <c>MTConnectStreamsParser</c>) plus a <c>multipart/x-mixed-replace</c> frame reader for the
///     <c>/sample</c> long poll. (Task 0 planned to build on the TrakHound MTConnect.NET libraries;
///     Tasks 6 and 7 proved they can neither parse a document nor frame the stream socket-free, and
///     dropped the references — see the Task 0 CORRECTION block in the plan.) A fake for tests only
///     needs to implement these three instance members; <see cref="IsSequenceGap"/> is a static
///     protocol rule that lives here, rather than on the implementation, so a consumer written
///     against this seam never has to reference the concrete client.
///     </para>
///     <para>
///         <b>Why the seam is <see cref="IDisposable"/> rather than
///         <see cref="IAsyncDisposable"/>.</b> The driver obtains its client through a
///         <c>Func&lt;MTConnectDriverOptions, IMTConnectAgentClient&gt;</c> and must release it on
///         <c>ShutdownAsync</c> and on every endpoint-changing <c>ReinitializeAsync</c>. Declaring
///         disposal here — instead of the driver doing <c>as IDisposable</c> + a null-conditional
///         call — is deliberate: that idiom silently no-ops against any future non-disposable
///         implementation, and the thing it would leak is an <see cref="HttpClient"/> and its socket
///         handler, once per re-deploy, for the life of the process.
///     </para>
///     <para>
///         Synchronous disposal is the honest shape because <i>every</i> resource behind this seam is
///         synchronously disposable — two <see cref="HttpClient"/>s and a
///         <see cref="CancellationTokenSource"/>. An <see cref="IAsyncDisposable"/> here would be a
///         synchronous method wearing a <c>ValueTask</c>, and it would oblige every canned-XML fake
///         to grow one too. The <b>async</b> unwind that a streaming client does need is already
///         covered elsewhere and is not this method's job: the <see cref="SampleAsync"/> enumerator
///         is itself <see cref="IAsyncDisposable"/> and is owned by the pump that enumerates it,
///         while <see cref="IDisposable.Dispose"/> on the client converts any read still in flight
///         into an ordinary <see cref="OperationCanceledException"/>.
///     </para>
/// </remarks>
public interface IMTConnectAgentClient : IDisposable
{
    /// <summary>
    ///     Issues an Agent <c>/probe</c> request and returns the parsed device hierarchy. Called
    ///     once at driver initialization; the result is cached and later walked to build the OPC UA
    ///     browse tree, where each <see cref="MTConnectDataItem.Id"/> becomes a tag's FullName.
    /// </summary>
    /// <param name="ct">Cancellation token.</param>
    Task<MTConnectProbeModel> ProbeAsync(CancellationToken ct);

    /// <summary>
    ///     Issues an Agent <c>/current</c> request and returns the latest observed value for every
    ///     data item. Used both to prime the observation index at startup and to re-baseline after
    ///     a detected sequence gap in <see cref="SampleAsync"/>.
    /// </summary>
    /// <param name="ct">Cancellation token.</param>
    Task<MTConnectStreamsResult> CurrentAsync(CancellationToken ct);

    /// <summary>
    ///     Opens an Agent <c>/sample</c> long-poll stream starting at sequence <paramref name="from"/>
    ///     and yields one <see cref="MTConnectStreamsResult"/> per multipart chunk the Agent sends.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b>The only way this enumeration ends without throwing is <paramref name="ct"/> being
    ///         cancelled.</b> Every other way a stream can stop — the connection dropping, the Agent
    ///         writing its closing boundary, or the endpoint turning out not to stream at all —
    ///         throws an <see cref="MTConnectStreamException"/> naming which. A consumer therefore
    ///         never has to treat "the enumeration finished" as an ambiguous signal, and cannot
    ///         silently lose its subscription to a data path that quietly stopped (#485). See
    ///         <see cref="MTConnectStreamEndedException"/> (transient — reconnect) and
    ///         <see cref="MTConnectStreamNotSupportedException"/> (configuration — reconnecting will
    ///         reproduce it forever).
    ///     </para>
    ///     <para>
    ///         <b>Advancing the cursor.</b> After each chunk, the caller's next expected sequence is
    ///         that chunk's <see cref="MTConnectStreamsResult.NextSequence"/> — not the
    ///         <paramref name="from"/> this call was opened with. Feed that running value to
    ///         <see cref="IsSequenceGap"/> and, on reconnect, to a fresh <c>SampleAsync</c>.
    ///     </para>
    /// </remarks>
    /// <param name="from">The sequence number to resume streaming from.</param>
    /// <param name="ct">Cancellation token; cancelling is the contracted way to end the stream.</param>
    IAsyncEnumerable<MTConnectStreamsResult> SampleAsync(long from, CancellationToken ct);

    /// <summary>
    ///     Has the Agent's ring buffer overflowed past the sequence the caller expected next? True
    ///     when the chunk's oldest retained sequence is <i>strictly</i> newer than
    ///     <paramref name="expectedFrom"/>, meaning observations between the two were evicted before
    ///     the caller could read them and an incremental update would silently skip values.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <b><paramref name="expectedFrom"/> is a running cursor, not the original
    ///         <c>from</c>.</b> It equals the <c>from</c> passed to <see cref="SampleAsync"/> only
    ///         for the <i>first</i> chunk; from the second chunk on it is the <b>previous</b>
    ///         chunk's <see cref="MTConnectStreamsResult.NextSequence"/>. Comparing every chunk
    ///         against the original <c>from</c> instead reports a gap on every chunk once the
    ///         Agent's buffer has rolled past it — an endless <c>/current</c> re-baseline storm
    ///         against a perfectly healthy stream.
    ///     </para>
    ///     <para>
    ///         Equality is not a gap: <c>firstSequence == expectedFrom</c> is the ordinary
    ///         contiguous case where the Agent simply had nothing older to send.
    ///     </para>
    ///     <para>
    ///         <b>This check alone does NOT cover every ring-buffer overflow.</b> It can only judge
    ///         a chunk the Agent was willing to send — and cppagent answers a <c>from</c> that has
    ///         already fallen out of its buffer with an <c>MTConnectError</c> / <c>OUT_OF_RANGE</c>
    ///         document served under <b>HTTP 200</b>, which this client surfaces as an
    ///         <see cref="InvalidDataException"/> out of <see cref="SampleAsync"/>, never as a chunk.
    ///         A pump that re-baselines only on <c>IsSequenceGap</c> will therefore fault instead of
    ///         recovering exactly when it has fallen furthest behind. Treat an <c>OUT_OF_RANGE</c>
    ///         parse failure as a re-baseline trigger (re-prime from <see cref="CurrentAsync"/>,
    ///         resume from that snapshot's <c>NextSequence</c>) just like a detected gap.
    ///     </para>
    /// </remarks>
    /// <param name="expectedFrom">The sequence the caller expected this chunk to start at.</param>
    /// <param name="chunk">The chunk the Agent answered with.</param>
    /// <returns>
    ///     <c>true</c> when observations were lost and the caller must re-baseline via
    ///     <see cref="CurrentAsync"/> rather than trusting an incremental update.
    /// </returns>
    static bool IsSequenceGap(long expectedFrom, MTConnectStreamsResult chunk)
    {
        ArgumentNullException.ThrowIfNull(chunk);

        return chunk.FirstSequence > expectedFrom;
    }
}
