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

/// <summary>
///     Modbus RTU framing: builds a request ADU (<c>[unitId] + PDU + CRC</c>) and deframes a
///     response back to its bare PDU. Byte-stream-agnostic — it operates on a <see cref="Stream"/>
///     and knows nothing about sockets or serial ports, so a future serial transport can reuse it.
/// </summary>
/// <remarks>
///     <para>
///         The single genuinely-new correctness risk of the RTU path: an RTU frame carries <b>no
///         length field</b> (unlike TCP's MBAP header), so the response size must be derived from
///         the function code. After reading <c>addr(1)</c> + <c>fc(1)</c>, the remaining byte count
///         is one of three shapes:
///     </para>
///     <list type="table">
///         <listheader><term>Shape</term><description>Trailing bytes after <c>addr,fc</c></description></listheader>
///         <item>
///             <term>Exception (<c>fc &amp; 0x80</c>)</term>
///             <description><c>excCode(1)</c> + <c>CRC(2)</c></description>
///         </item>
///         <item>
///             <term>Read (FC 01/02/03/04)</term>
///             <description><c>byteCount(1)</c> then <c>byteCount</c> data bytes + <c>CRC(2)</c></description>
///         </item>
///         <item>
///             <term>Write echo (FC 05/06/15/16)</term>
///             <description>fixed <c>4</c> bytes + <c>CRC(2)</c></description>
///         </item>
///     </list>
///     <para>
///         The trailing CRC is validated over <c>[addr, ...pdu]</c> (everything except the two CRC
///         bytes) <b>before</b> the frame is interpreted — so a corrupt exception frame surfaces as a
///         desync, never as a bogus <see cref="ModbusException"/>. A CRC mismatch or a short read
///         (truncation / stream closed mid-frame) throws <see cref="ModbusTransportDesyncException"/>;
///         a CRC-valid exception PDU throws <see cref="ModbusException"/> carrying the original
///         function code (<c>fc &amp; 0x7F</c>) and exception code.
///     </para>
///     <para>
///         Once the CRC passes, the response's address byte is validated against the addressed unit.
///         On an RS-485 multi-drop bus a CRC-valid reply from the <b>wrong</b> slave would otherwise be
///         silently accepted as the addressed unit's data; such a frame is a unit-mismatch
///         <see cref="ModbusTransportDesyncException"/>. This ordering is deliberate — a corrupt frame
///         stays a CRC/desync failure, and only a coherent-but-mis-addressed frame becomes a
///         unit-mismatch desync.
///     </para>
/// </remarks>
public static class ModbusRtuFraming
{
    /// <summary>
    ///     Builds an RTU request ADU: the unit id, the PDU, and the trailing CRC-16/MODBUS
    ///     (appended low byte first).
    /// </summary>
    /// <param name="unitId">The RTU slave/unit address.</param>
    /// <param name="pdu">The bare PDU (function code + data).</param>
    /// <returns>A new array: <c>[unitId, ...pdu, crcLo, crcHi]</c>.</returns>
    public static byte[] BuildAdu(byte unitId, ReadOnlySpan<byte> pdu)
    {
        var frame = new byte[1 + pdu.Length];
        frame[0] = unitId;
        pdu.CopyTo(frame.AsSpan(1));
        return ModbusCrc.Append(frame);
    }

    /// <summary>
    ///     Reads a single RTU response frame from <paramref name="stream"/> and returns its bare PDU
    ///     (<c>[fc, ...data]</c>), with the leading unit-id and trailing CRC stripped.
    /// </summary>
    /// <param name="stream">The byte stream to read the response from.</param>
    /// <param name="expectedUnit">
    ///     The unit id the request was addressed to. The response's address byte is validated against
    ///     it after the CRC passes; a mismatch is a unit-mismatch desync.
    /// </param>
    /// <param name="ct">A token to cancel the read.</param>
    /// <returns>The bare response PDU (function code followed by its data).</returns>
    /// <exception cref="ModbusTransportDesyncException">
    ///     The frame was truncated (stream closed mid-frame), its trailing CRC did not validate, or its
    ///     address byte did not match <paramref name="expectedUnit"/>.
    /// </exception>
    /// <exception cref="ModbusException">
    ///     The frame was a CRC-valid Modbus exception PDU (high bit set on the function code).
    /// </exception>
    public static async Task<byte[]> ReadResponsePduAsync(
        Stream stream, byte expectedUnit, CancellationToken ct)
    {
        // Header: addr(1) + fc(1). The function code selects how many more bytes to read.
        var header = new byte[2];
        await ReadExactlyAsync(stream, header, ct).ConfigureAwait(false);
        var fc = header[1];

        int trailing; // bytes after addr+fc, up to and including the 2 CRC bytes.
        if ((fc & 0x80) != 0)
        {
            // Exception frame: excCode(1) + CRC(2).
            trailing = 1 + 2;
        }
        else if (fc is 0x01 or 0x02 or 0x03 or 0x04)
        {
            // Read response: byteCount(1) then byteCount data bytes + CRC(2).
            var bc = new byte[1];
            await ReadExactlyAsync(stream, bc, ct).ConfigureAwait(false);
            var byteCount = bc[0];
            var rest = new byte[byteCount + 2];
            await ReadExactlyAsync(stream, rest, ct).ConfigureAwait(false);
            return ValidateAndStrip(expectedUnit, header, bc, rest);
        }
        else
        {
            // Fixed 4-byte echo: correct for the write FCs this driver emits (05/06/0F/10). A future
            // variable-length response FC outside 01-04 (e.g. FC23) would be mis-sized here and
            // surface as a desync — revisit the FC-shape table if the driver starts emitting one.
            trailing = 4 + 2;
        }

        var tail = new byte[trailing];
        await ReadExactlyAsync(stream, tail, ct).ConfigureAwait(false);
        return ValidateAndStrip(expectedUnit, header, ReadOnlySpan<byte>.Empty, tail);
    }

    /// <summary>
    ///     Assembles the full frame from its pieces, validates the trailing CRC over everything
    ///     except the CRC bytes, then validates the response address against
    ///     <paramref name="expectedUnit"/> and strips <c>addr</c> + <c>CRC</c> to return the bare PDU.
    ///     A CRC-valid exception PDU throws <see cref="ModbusException"/>.
    /// </summary>
    private static byte[] ValidateAndStrip(
        byte expectedUnit, ReadOnlySpan<byte> header, ReadOnlySpan<byte> middle, ReadOnlySpan<byte> tail)
    {
        // Reassemble the whole frame: header(addr,fc) + middle(optional byteCount) + tail.
        var frame = new byte[header.Length + middle.Length + tail.Length];
        header.CopyTo(frame);
        middle.CopyTo(frame.AsSpan(header.Length));
        tail.CopyTo(frame.AsSpan(header.Length + middle.Length));

        // CRC covers everything except the trailing 2 CRC bytes.
        var body = frame.AsSpan(0, frame.Length - 2);
        var expected = ModbusCrc.Compute(body);
        var actual = (ushort)(frame[^2] | (frame[^1] << 8));
        if (actual != expected)
            throw new ModbusTransportDesyncException(
                ModbusTransportDesyncException.DesyncReason.CrcMismatch,
                $"Modbus RTU CRC mismatch: computed {expected:X4} got {actual:X4}");

        // Unit-address validation runs only AFTER the CRC passes: a corrupt frame is a CRC/desync
        // failure, and only a coherent-but-mis-addressed frame (a CRC-valid reply from the wrong
        // RS-485 slave) is a unit-mismatch desync. Never silently accept another unit's data.
        var addr = frame[0];
        if (addr != expectedUnit)
            throw new ModbusTransportDesyncException(
                ModbusTransportDesyncException.DesyncReason.UnitMismatch,
                $"Modbus RTU unit mismatch: expected {expectedUnit} got {addr}");

        // Bare PDU = frame minus leading addr and trailing CRC.
        var pdu = body[1..].ToArray();

        // Exception PDU: high bit set on the function code. The CRC already validated, so this is a
        // coherent protocol-level error — surface the ORIGINAL function code (fc & 0x7F).
        if ((pdu[0] & 0x80) != 0)
        {
            var fc = (byte)(pdu[0] & 0x7F);
            var exCode = pdu[1];
            throw new ModbusException(fc, exCode, $"Modbus exception fc={fc} code={exCode}");
        }

        return pdu;
    }

    /// <summary>
    ///     Fills <paramref name="buf"/> completely from <paramref name="stream"/>, throwing a
    ///     truncation desync if the stream ends first. Mirrors
    ///     <c>ModbusTcpTransport.ReadExactlyAsync</c>, but normalises the end-of-stream to a
    ///     <see cref="ModbusTransportDesyncException"/> so a length-less RTU frame that arrives
    ///     short is classified as a desync rather than a bare <see cref="EndOfStreamException"/>.
    /// </summary>
    private static async Task ReadExactlyAsync(Stream stream, byte[] buf, CancellationToken ct)
    {
        var read = 0;
        while (read < buf.Length)
        {
            var n = await stream.ReadAsync(buf.AsMemory(read), ct).ConfigureAwait(false);
            if (n == 0)
                throw new ModbusTransportDesyncException(
                    ModbusTransportDesyncException.DesyncReason.TruncatedFrame,
                    $"Modbus RTU frame truncated: expected {buf.Length} bytes, got {read}");
            read += n;
        }
    }
}
