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

/// <summary>
///     Base type for every way an Agent <c>/sample</c> long poll can stop delivering chunks for a
///     reason that is <b>not</b> the caller cancelling it.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why these are exceptions rather than a normal end of enumeration.</b>
///         <see cref="IMTConnectAgentClient.SampleAsync"/> is contracted to yield indefinitely until
///         its token is cancelled, so a pump written to that contract has no reason to handle normal
///         completion — it would either drop the subscription silently or hot-loop reconnecting on
///         an enumerator that ends immediately. Returning normally would make "the data path
///         stopped" indistinguishable from "the data path is idle", which is the
///         quiet-successful-looking-termination shape that produced this repo's #485 defect. Every
///         non-cancellation end is therefore loud, typed, and carries the Agent URI.
///     </para>
///     <para>
///         Catch this base type to handle "the stream is over" uniformly; catch the two derived
///         types to distinguish a <i>transient</i> end (the Agent closed a connection — reconnect)
///         from a <i>configuration</i> error (the Agent will never stream to this request — retrying
///         is pointless until an operator changes something).
///     </para>
/// </remarks>
public abstract class MTConnectStreamException : Exception
{
    /// <summary>Creates the exception with no message.</summary>
    protected MTConnectStreamException()
    {
    }

    /// <summary>Creates the exception with a message.</summary>
    /// <param name="message">The operator-facing description.</param>
    protected MTConnectStreamException(string message)
        : base(message)
    {
    }

    /// <summary>Creates the exception with a message and an inner cause.</summary>
    /// <param name="message">The operator-facing description.</param>
    /// <param name="innerException">The underlying cause.</param>
    protected MTConnectStreamException(string message, Exception innerException)
        : base(message, innerException)
    {
    }
}

/// <summary>How an Agent <c>/sample</c> stream ended.</summary>
public enum MTConnectStreamEndReason
{
    /// <summary>
    ///     The transport closed mid-stream — the connection dropped, the Agent restarted, or a proxy
    ///     timed the connection out. Ordinary and transient: reconnect from the last
    ///     <see cref="MTConnectStreamsResult.NextSequence"/>.
    /// </summary>
    ConnectionClosed = 0,

    /// <summary>
    ///     The Agent wrote the closing <c>--boundary--</c> delimiter, ending the multipart response
    ///     deliberately. Usually means the request was bounded (an Agent honouring a <c>count</c>
    ///     that exhausted, or an administrative stop) rather than a fault. Also reconnectable, but
    ///     worth distinguishing in logs: a stream that keeps closing cleanly points at the request's
    ///     parameters, not at the network.
    /// </summary>
    ClosingBoundary = 1
}

/// <summary>
///     The Agent's <c>/sample</c> stream ended without the caller cancelling it. See
///     <see cref="MTConnectStreamException"/> for why this is not a normal end of enumeration.
/// </summary>
public sealed class MTConnectStreamEndedException : MTConnectStreamException
{
    /// <summary>Creates the exception for a stream that ended for <paramref name="reason"/>.</summary>
    /// <param name="reason">How the stream ended.</param>
    /// <param name="agentUri">The <c>/sample</c> request URI whose stream ended.</param>
    /// <param name="chunksDelivered">How many chunks were yielded before the end.</param>
    public MTConnectStreamEndedException(MTConnectStreamEndReason reason, string agentUri, long chunksDelivered)
        : base($"MTConnect /sample stream from '{agentUri}' ended after {chunksDelivered} chunk(s) without the caller cancelling it ({Describe(reason)}). The stream is contracted to run until cancelled, so this is reported rather than returned; resume from the last chunk's nextSequence.")
    {
        Reason = reason;
        AgentUri = agentUri;
        ChunksDelivered = chunksDelivered;
    }

    /// <summary>Creates the exception with a message.</summary>
    /// <param name="message">The operator-facing description.</param>
    public MTConnectStreamEndedException(string message)
        : base(message)
    {
        AgentUri = string.Empty;
    }

    /// <summary>Creates the exception with a message and an inner cause.</summary>
    /// <param name="message">The operator-facing description.</param>
    /// <param name="innerException">The underlying cause.</param>
    public MTConnectStreamEndedException(string message, Exception innerException)
        : base(message, innerException)
    {
        AgentUri = string.Empty;
    }

    /// <summary>Creates the exception with no message.</summary>
    public MTConnectStreamEndedException()
    {
        AgentUri = string.Empty;
    }

    /// <summary>How the stream ended.</summary>
    public MTConnectStreamEndReason Reason { get; }

    /// <summary>The <c>/sample</c> request URI whose stream ended.</summary>
    public string AgentUri { get; }

    /// <summary>How many chunks were yielded before the stream ended.</summary>
    public long ChunksDelivered { get; }

    private static string Describe(MTConnectStreamEndReason reason) =>
        reason switch
        {
            MTConnectStreamEndReason.ClosingBoundary => "the Agent wrote the closing multipart boundary",
            _ => "the connection closed mid-stream"
        };
}

/// <summary>
///     The Agent answered a <c>/sample</c> request with something that cannot be consumed as a
///     stream at all — a single non-multipart document, or a <c>multipart/*</c> response with no
///     <c>boundary</c> parameter to frame it by.
/// </summary>
/// <remarks>
///     This is a <b>configuration</b> error, not a transient one, and is deliberately typed apart
///     from <see cref="MTConnectStreamEndedException"/>: reconnecting will produce the identical
///     response forever. The usual cause is an endpoint that is not an MTConnect Agent (a reverse
///     proxy, a load balancer's health page, or an Agent fronted by something that buffers and
///     collapses <c>multipart/x-mixed-replace</c>). Reported instead of yielding the one document
///     the Agent did return, because a single snapshot followed by a healthy-looking finished
///     stream is exactly how a dead data path disguises itself as a working one.
/// </remarks>
public sealed class MTConnectStreamNotSupportedException : MTConnectStreamException
{
    /// <summary>Creates the exception with a message.</summary>
    /// <param name="message">The operator-facing description.</param>
    public MTConnectStreamNotSupportedException(string message)
        : base(message)
    {
    }

    /// <summary>Creates the exception with a message and an inner cause.</summary>
    /// <param name="message">The operator-facing description.</param>
    /// <param name="innerException">The underlying cause.</param>
    public MTConnectStreamNotSupportedException(string message, Exception innerException)
        : base(message, innerException)
    {
    }

    /// <summary>Creates the exception with no message.</summary>
    public MTConnectStreamNotSupportedException()
    {
    }
}
