using RobotNet.Script;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;

namespace RobotNet.ScriptManager.Connections;

public class CcLinkIeBasicClient(IeBasicClientOptions options) : ICcLinkIeBasicClient
{
    private readonly SemaphoreSlim _sendLock = new(1, 1);
    private UdpClient? _udp;
    private TcpClient? _tcp;
    private NetworkStream? _tcpStream;
    private IPEndPoint? _remoteEp;
    private CancellationTokenSource? _pollCts;

    private IeBasicStatus _status = new()
    {
        LinkUp = false,
        ConsecutiveErrors = 0,
        LastEndCode = null,
        LastErrorText = null,
        AvgRtt = null,
        MaxRtt = null
    };

    private volatile bool _connected;
    private long _rttCount;
    private long _rttSumTicks;

    public bool IsConnected => _connected;
    public int RetryCount { get => options.RetryCount; set => options = options with { RetryCount = value }; }

    public event EventHandler<PollUpdatedEventArgs>? Polled;

    // ====== Public API ======

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        if (options.FrameFormat != SlmpFrameFormat.Format3E)
            throw new NotSupportedException("Triển khai tham chiếu này hiện hỗ trợ 3E binary frame.");

        if (options.Transport == IeBasicTransport.Udp)
        {
            _udp?.Dispose();
            _udp = new UdpClient();
            _udp.Client.ReceiveTimeout = (int)options.Timeout.TotalMilliseconds;
            _udp.Client.SendTimeout = (int)options.Timeout.TotalMilliseconds;

            var ip = await ResolveHostAsync(options.Host, ct);
            _remoteEp = new IPEndPoint(ip, options.Port);
            _udp.Connect(_remoteEp);
        }
        else
        {
            _tcp?.Close();
            _tcp = new TcpClient
            {
                NoDelay = true,
                ReceiveTimeout = (int)options.Timeout.TotalMilliseconds,
                SendTimeout = (int)options.Timeout.TotalMilliseconds
            };

            var ip = await ResolveHostAsync(options.Host, ct);
            await _tcp.ConnectAsync(ip, options.Port, ct);
            _tcpStream = _tcp.GetStream();
        }

        _connected = true;
        _status = _status with { LinkUp = true, ConsecutiveErrors = 0, LastErrorText = null, LastEndCode = null };
    }

    public Task DisconnectAsync(CancellationToken ct = default)
    {
        StopPollingInternal();

        _connected = false;

        try { _tcpStream?.Dispose(); } catch { }
        try { _tcp?.Close(); } catch { }
        try { _udp?.Dispose(); } catch { }

        _tcpStream = null;
        _tcp = null;
        _udp = null;

        _status = _status with { LinkUp = false };
        return Task.CompletedTask;
    }

    public async Task<bool[]> ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default)
    {
        // 3E binary: Command 0x0401, Subcommand 0x0001 (bit units)
        var payload = BuildBatchRead(device, startAddress, count, isBit: true);
        var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
        EnsureOk(resp);

        // Bit data: packed per bit (LSB-first) theo manual. Ở đây đơn giản dùng từng byte -> từng bit.
        return UnpackBits(resp.Payload.Span, count);
    }

    public async Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<bool> values, CancellationToken ct = default)
    {
        // 3E binary: Command 0x1401, Subcommand 0x0001 (bit units)
        var payload = BuildBatchWriteBits(device, startAddress, values.Span);
        var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
        EnsureOk(resp);
    }

    public async Task<ushort[]> ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default)
    {
        // 3E binary: Command 0x0401, Subcommand 0x0000 (word units)
        var payload = BuildBatchRead(device, startAddress, wordCount, isBit: false);
        var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
        EnsureOk(resp);

        // Word data: little-endian UInt16
        if (resp.Payload.Length < wordCount * 2)
            throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
        var span = resp.Payload.Span;
        var result = new ushort[wordCount];
        for (int i = 0; i < wordCount; i++)
            result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
        return result;
    }

    public async Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<ushort> words, CancellationToken ct = default)
    {
        // 3E binary: Command 0x1401, Subcommand 0x0000 (word units)
        var payload = BuildBatchWriteWords(device, startAddress, words.Span);
        var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
        EnsureOk(resp);
    }

    public async Task<uint[]> ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default)
    {
        if (SupportsDirectDWordRead(device))
        {
            // 3E binary: Command 0x0401, Subcommand 0x0002 (dword units)
            var payload = BuildBatchRead(device, startAddress, dwordCount, isBit: false, isDWord: true);
            var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
            EnsureOk(resp);

            if (resp.Payload.Length < dwordCount * 4)
                throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
            var span = resp.Payload.Span;
            var result = new uint[dwordCount];
            for (int i = 0; i < dwordCount; i++)
                result[i] = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(i * 4, 4));
            return result;
        }
        else
        {
            // Đọc dword như đọc 2 word/liên tiếp
            var words = await ReadWordsAsync(device, startAddress, dwordCount * 2, ct);
            var result = new uint[dwordCount];

            if (options.LittleEndianWordOrder)
            {
                for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
                    result[i] = (uint)(words[j] | ((uint)words[j + 1] << 16));
            }
            else
            {
                for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
                    result[i] = (uint)(words[j + 1] | ((uint)words[j] << 16));
            }
            return result;
        }
    }

    public async Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<uint> dwords, CancellationToken ct = default)
    {
        var span = dwords.Span;
        var buf = new ushort[span.Length * 2];

        if (options.LittleEndianWordOrder)
        {
            for (int i = 0, j = 0; i < span.Length; i++, j += 2)
            {
                buf[j] = (ushort)(span[i] & 0xFFFF);
                buf[j + 1] = (ushort)((span[i] >> 16) & 0xFFFF);
            }
        }
        else
        {
            for (int i = 0, j = 0; i < span.Length; i++, j += 2)
            {
                buf[j] = (ushort)((span[i] >> 16) & 0xFFFF);
                buf[j + 1] = (ushort)(span[i] & 0xFFFF);
            }
        }
        await WriteWordsAsync(device, startAddress, buf, ct);
    }

    public async Task<ushort[]> ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default)
    {
        // 3E binary: Read Random (0x0403). Payload: điểm (addr+devcode)
        var payload = BuildRandomRead(points);
        var resp = await SendSlmpAsync(SlmpCommand.ReadRandom, payload, ct);
        EnsureOk(resp);

        // Trả về word liên tiếp
        var span = resp.Payload.Span;
        if (span.Length < points.Length * 2) throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
        var result = new ushort[points.Length];
        for (int i = 0; i < points.Length; i++)
            result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
        return result;
    }

    public async Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory<ushort> values, CancellationToken ct = default)
    {
        if (points.Length != values.Length)
            throw new ArgumentException("Số điểm và số giá trị không khớp.");

        var payload = BuildRandomWrite(points, values.Span);
        var resp = await SendSlmpAsync(SlmpCommand.WriteRandom, payload, ct);
        EnsureOk(resp);
    }

    public async Task<IeBasicStatus> GetStatusAsync(CancellationToken ct = default)
    {
        // Có thể gửi NodeMonitor để “làm nóng” trạng thái; ở đây trả về snapshot hiện có
        await Task.Yield();
        return _status;
    }

    public async Task<bool> PingAsync(CancellationToken ct = default)
    {
        try
        {
            // Node monitor: dùng CMD DeviceInfo/NodeMonitor rỗng để kiểm phản hồi
            var resp = await SendSlmpAsync(SlmpCommand.NodeMonitor, ReadOnlyMemory<byte>.Empty, ct);
            return resp.EndCode == SlmpEndCode.Completed;
        }
        catch
        {
            return false;
        }
    }

    public async Task<ModuleIdentity> IdentifyAsync(CancellationToken ct = default)
    {
        // Tối giản: gửi DeviceInfo và parse sơ bộ (tuỳ PLC mà payload khác nhau)
        var resp = await SendSlmpAsync(SlmpCommand.DeviceInfo, ReadOnlyMemory<byte>.Empty, ct);
        // Ở đây không chuẩn hoá do payload phụ thuộc model; trả về khung rỗng để người dùng tự giải.
        return new ModuleIdentity
        {
            Vendor = "Mitsubishi Electric",
            AdditionalInfo = $"Raw bytes: {resp.Payload.Length} (xem manual model để parse)"
        };
    }

    public async Task<SlmpResponse> SendSlmpAsync(SlmpCommand command, ReadOnlyMemory<byte> payload, CancellationToken ct = default)
    {
        if (!_connected) throw new InvalidOperationException("Chưa kết nối.");

        // Dựng 3E frame
        var req = Build3EFrame(options, (ushort)command, subcommand: CurrentSubcommandHint, payload);

        // Gửi/nhận với retry
        var sw = System.Diagnostics.Stopwatch.StartNew();
        for (int attempt = 0; attempt <= RetryCount; attempt++)
        {
            try
            {
                var rawResp = await SendAndReceiveAsync(req, ct);
                var resp = Parse3EResponse(rawResp);

                sw.Stop();
                TrackRtt(sw.Elapsed);

                _status = _status with
                {
                    LinkUp = true,
                    ConsecutiveErrors = 0,
                    LastEndCode = resp.EndCode,
                    LastErrorText = null,
                    AvgRtt = TimeSpan.FromTicks((_rttCount > 0) ? _rttSumTicks / _rttCount : 0),
                    MaxRtt = (_status.MaxRtt == null || sw.Elapsed > _status.MaxRtt) ? sw.Elapsed : _status.MaxRtt
                };
                return resp;
            }
            catch (Exception ex) when (attempt < RetryCount)
            {
                _status = _status with
                {
                    LinkUp = false,
                    ConsecutiveErrors = _status.ConsecutiveErrors + 1,
                    LastErrorText = ex.Message,
                    LastEndCode = SlmpEndCode.TransportError
                };
                // thử lại
                await Task.Delay(10, ct);
            }
        }

        sw.Stop();
        throw new TimeoutException("SLMP: Hết retry mà vẫn lỗi.");
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
        _sendLock.Dispose();
        GC.SuppressFinalize(this);
    }

    public void Dispose()
    {
        DisposeAsync().AsTask().GetAwaiter().GetResult();
        GC.SuppressFinalize(this);
    }

    public void StartPolling(PollOptions options, CancellationToken ct = default)
    {
        StopPollingInternal();
        _pollCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        var token = _pollCts.Token;

        _ = Task.Run(async () =>
        {
            // Strategy: gom vùng bit và word/dword riêng để tối ưu
            while (!token.IsCancellationRequested && _connected)
            {
                try
                {
                    // Đơn giản: chỉ đọc word; nếu có bit thì đọc riêng
                    var wordsAreas = options.Areas;
                    ushort[]? words = null;
                    bool[]? bits = null;

                    // (Minh hoạ) Đọc tất cả vùng word liên tiếp bằng cách gộp, bạn có thể tối ưu thêm theo thiết bị.
                    if (wordsAreas.Length > 0)
                    {
                        // Đây đọc lần lượt từng vùng cho đơn giản (dễ hiểu; bạn có thể hợp nhất để giảm gói)
                        var aggWords = new System.Collections.Generic.List<ushort>();
                        foreach (var (Device, Start, Count) in wordsAreas)
                        {
                            if (Device == DeviceCode.X || Device == DeviceCode.Y || Device == DeviceCode.M ||
                                Device == DeviceCode.L || Device == DeviceCode.S || Device == DeviceCode.B)
                            {
                                // Bit area
                                var b = await ReadBitsAsync(Device, Start, Count, token);
                                // Nối bit (minh hoạ): không ghép; đẩy riêng
                                bits = b;
                            }
                            else
                            {
                                var w = await ReadWordsAsync(Device, Start, Count, token);
                                aggWords.AddRange(w);
                            }
                        }
                        if (aggWords.Count > 0) words = [.. aggWords];
                    }

                    Polled?.Invoke(this, new PollUpdatedEventArgs
                    {
                        Timestamp = DateTimeOffset.UtcNow,
                        Bits = bits,
                        Words = words,
                        DWords = null
                    });

                    await Task.Delay(options.Interval, token);
                }
                catch (OperationCanceledException) { }
                catch
                {
                    // bỏ qua vòng lỗi và tiếp tục (tuỳ nhu cầu: bạn có thể tăng backoff)
                    await Task.Delay(TimeSpan.FromMilliseconds(50), token);
                }
            }
        }, token);
    }

    public Task StopPollingAsync(CancellationToken ct = default)
    {
        StopPollingInternal();
        return Task.CompletedTask;
    }

    // ====== Internal helpers ======

    private void StopPollingInternal()
    {
        try { _pollCts?.Cancel(); } catch { }
        try { _pollCts?.Dispose(); } catch { }
        _pollCts = null;
    }

    private static async Task<IPAddress> ResolveHostAsync(string host, CancellationToken ct)
    {
        if (IPAddress.TryParse(host, out var ip)) return ip;
        var entry = await Dns.GetHostEntryAsync(host, ct);
        foreach (var addr in entry.AddressList)
            if (addr.AddressFamily == AddressFamily.InterNetwork) return addr;
        throw new InvalidOperationException("Không tìm thấy IPv4 cho host.");
    }

    private void TrackRtt(TimeSpan elapsed)
    {
        var ticks = elapsed.Ticks;
        System.Threading.Interlocked.Add(ref _rttSumTicks, ticks);
        System.Threading.Interlocked.Increment(ref _rttCount);
    }

    private static void EnsureOk(SlmpResponse resp)
    {
        if (resp.EndCode != SlmpEndCode.Completed)
            throw new InvalidOperationException($"SLMP EndCode: 0x{(ushort)resp.EndCode:X4}");
    }

    // ====== SLMP 3E Binary Build/Parse ======

    // Gợi ý subcommand hiện hành (đặt trước khi Build3EFrame): 0x0000=word, 0x0001=bit
    private ushort CurrentSubcommandHint = 0x0000;

    private static byte[] Build3EFrame(IeBasicClientOptions opt, ushort command, ushort subcommand, ReadOnlyMemory<byte> userData)
    {
        // Tài liệu 3E binary (TCP): Subheader 0x5000; (UDP): 0x5400
        // Header:
        // [0-1]  Subheader (LE)
        // [2]    Network No
        // [3]    PC No (để 0x00)
        // [4-5]  Module I/O No (LE)
        // [6]    Multidrop No
        // [7-8]  Data Length (LE)  = 2(timer) + 2(cmd) + 2(subcmd) + payload.Length
        // [9-10] Timer (LE) đơn vị 250 ms hoặc 1? (tuỳ manual; thường 0x0010 ~ 4s). Ở đây lấy theo Timeout ~ ms/10.
        // [11-12] Command (LE)
        // [13-14] Subcommand (LE)
        // [15..] Payload

        var subheader = (opt.Transport == IeBasicTransport.Udp) ? (ushort)0x5400 : (ushort)0x5000;
        var dataLen = (ushort)(2 + 2 + 2 + userData.Length); // timer + cmd + subcmd + payload

        // Timer: convert từ Timeout ~ ms -> giá trị phù hợp. Ở đây minh hoạ: ms/10 (tuỳ manual).
        var timer = (ushort)Math.Clamp((int)(opt.Timeout.TotalMilliseconds / 10.0), 1, 0xFFFE);

        var buf = new byte[15 + userData.Length];

        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), subheader);
        buf[2] = opt.NetworkNo;
        buf[3] = 0x00; // PC No
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), opt.ModuleIoNo);
        buf[6] = opt.MultidropNo;
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(7, 2), dataLen);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(9, 2), timer);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(11, 2), command);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(13, 2), subcommand);
        userData.Span.CopyTo(buf.AsSpan(15));

        return buf;
    }

    private async Task<byte[]> SendAndReceiveAsync(byte[] request, CancellationToken ct)
    {
        await _sendLock.WaitAsync(ct).ConfigureAwait(false);

        try
        {
            if (options.Transport == IeBasicTransport.Udp)
            {
                if (_udp is null) throw new InvalidOperationException("UDP client null.");
                await _udp.SendAsync(request, request.Length);
                var recv = await _udp.ReceiveAsync(ct);
                return recv.Buffer;
            }
            else
            {
                if (_tcpStream is null) throw new InvalidOperationException("TCP stream null.");

                // Gửi request
                await _tcpStream.WriteAsync(request, ct);
                await _tcpStream.FlushAsync(ct);

                // Đọc header tối thiểu 15 bytes để biết data length
                var header = new byte[15];
                await ReadExactAsync(_tcpStream, header, ct);

                // Lấy data length
                var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(7, 2));
                var rest = new byte[dataLen]; // bao gồm timer/cmd/subcmd/endcode/payload (ở response thay timer bằng EndCode)
                await ReadExactAsync(_tcpStream, rest, ct);

                // Gộp lại thành frame đầy đủ cho parser
                var combined = new byte[header.Length + rest.Length];
                Buffer.BlockCopy(header, 0, combined, 0, header.Length);
                Buffer.BlockCopy(rest, 0, combined, header.Length, rest.Length);
                return combined;
            }
        }
        finally
        {
            _sendLock.Release();
        }
    }

    private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken ct)
    {
        int read = 0;
        while (read < buffer.Length)
        {
            var n = await stream.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct);
            if (n == 0) throw new SocketException((int)SocketError.ConnectionReset);
            read += n;
        }
    }

    private static SlmpResponse Parse3EResponse(byte[] frame)
    {
        // Response 3E:
        // [0-1] Subheader
        // [2]   NetworkNo
        // [3]   PCNo
        // [4-5] ModuleIoNo
        // [6]   MultidropNo
        // [7-8] Data Length (LE)
        // [9-10] End Code (LE)   <-- khác request (timer)
        // [11..] Payload
        if (frame.Length < 11)
            throw new InvalidOperationException("Frame quá ngắn.");

        var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(7, 2));
        if (frame.Length < 11 + dataLen)
            throw new InvalidOperationException("Frame không đủ chiều dài.");

        var endCode = (SlmpEndCode)BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(9, 2));
        var payload = new ReadOnlyMemory<byte>(frame, 11, dataLen - 2); // trừ endcode (2B)
        return new SlmpResponse { EndCode = endCode, Payload = payload, Timestamp = DateTimeOffset.UtcNow };
    }

    // ====== Payload builders (3E binary) ======

    private static byte DevCodeBinary(DeviceCode device) => device switch
    {
        // Mã phổ biến trong MC 3E binary (tham chiếu; kiểm tra manual cho chắc theo CPU):
        DeviceCode.X => 0x9C,
        DeviceCode.Y => 0x9D,
        DeviceCode.M => 0x90,
        DeviceCode.L => 0x92,
        DeviceCode.B => 0xA0,
        DeviceCode.S => 0x98,
        DeviceCode.D => 0xA8,
        DeviceCode.W => 0xB4,
        DeviceCode.R => 0xAF,
        DeviceCode.ZR => 0xB0,
        _ => 0x00
    };

    private ReadOnlyMemory<byte> BuildBatchWriteWords(DeviceCode device, int start, ReadOnlySpan<ushort> words)
    {
        CurrentSubcommandHint = 0x0000; // word units

        var buf = new byte[3 + 1 + 2 + words.Length * 2];
        buf[0] = (byte)(start & 0xFF);
        buf[1] = (byte)((start >> 8) & 0xFF);
        buf[2] = (byte)((start >> 16) & 0xFF);
        buf[3] = DevCodeBinary(device);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)words.Length);

        var off = 6;
        for (int i = 0; i < words.Length; i++, off += 2)
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off, 2), words[i]);

        return buf;
    }

    private ReadOnlyMemory<byte> BuildBatchWriteBits(DeviceCode device, int start, ReadOnlySpan<bool> bits)
    {
        CurrentSubcommandHint = 0x0001; // bit units

        // Bit packing theo MC (1 bit/byte hoặc 2 bit/byte tuỳ subcmd). Ở đây dùng 1 bit/bit (1 byte/bit) để đơn giản;
        // nhiều PLC chấp nhận. Nếu cần tối ưu, gói bit theo nibble.
        var buf = new byte[3 + 1 + 2 + bits.Length];
        buf[0] = (byte)(start & 0xFF);
        buf[1] = (byte)((start >> 8) & 0xFF);
        buf[2] = (byte)((start >> 16) & 0xFF);
        buf[3] = DevCodeBinary(device);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)bits.Length);

        for (int i = 0; i < bits.Length; i++)
            buf[6 + i] = (byte)(bits[i] ? 0x01 : 0x00);

        return buf;
    }

    private static ReadOnlyMemory<byte> BuildRandomRead((DeviceCode Device, int Address)[] points)
    {
        // Format tối giản: [count(2)] + N * (addr(3) + dev(1))
        var buf = new byte[2 + points.Length * 4];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
        var off = 2;
        foreach (var (Device, Address) in points)
        {
            buf[off + 0] = (byte)(Address & 0xFF);
            buf[off + 1] = (byte)((Address >> 8) & 0xFF);
            buf[off + 2] = (byte)((Address >> 16) & 0xFF);
            buf[off + 3] = DevCodeBinary(Device);
            off += 4;
        }
        return buf;
    }

    private static ReadOnlyMemory<byte> BuildRandomWrite((DeviceCode Device, int Address)[] points, ReadOnlySpan<ushort> values)
    {
        // [count(2)] + N*(addr(3)+dev(1)+value(2))
        var buf = new byte[2 + points.Length * (4 + 2)];
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
        var off = 2;
        for (int i = 0; i < points.Length; i++)
        {
            var (Device, Address) = points[i];
            buf[off + 0] = (byte)(Address & 0xFF);
            buf[off + 1] = (byte)((Address >> 8) & 0xFF);
            buf[off + 2] = (byte)((Address >> 16) & 0xFF);
            buf[off + 3] = DevCodeBinary(Device);
            BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off + 4, 2), values[i]);
            off += 6;
        }
        return buf;
    }

    private static bool[] UnpackBits(ReadOnlySpan<byte> data, int expectedCount)
    {
        // Đơn giản hoá: nếu payload trả về mỗi bit = 1 byte (0x00/0x01)
        // Nếu PLC trả về packed bits, bạn cần giải theo bit-level. Ở đây xử lý cả hai:
        if (data.Length == expectedCount)
        {
            var res = new bool[expectedCount];
            for (int i = 0; i < expectedCount; i++) res[i] = data[i] != 0;
            return res;
        }
        else
        {
            // Giải packed: LSB-first trong từng byte
            var res = new bool[expectedCount];
            int idx = 0;
            for (int b = 0; b < data.Length && idx < expectedCount; b++)
            {
                byte val = data[b];
                for (int bit = 0; bit < 8 && idx < expectedCount; bit++, idx++)
                    res[idx] = ((val >> bit) & 0x01) != 0;
            }
            return res;
        }
    }

    private ReadOnlyMemory<byte> BuildBatchRead(DeviceCode device, int start, int count, bool isBit, bool isDWord = false)
    {
        // Request data:
        // [0-2]  Address (3 bytes, little-endian 24-bit)
        // [3]    Device Code (1 byte)
        // [4-5]  Points/Count (LE, word/bit/dword tuỳ subcmd)
        // [6-7]  Reserved? (tuỳ lệnh) - KHÔNG cần cho 0x0401
        // Ở đây dùng định dạng tối giản: addr(3) + dev(1) + count(2) + subcmd đã set ở header

        if (isDWord)
            CurrentSubcommandHint = 0x0002; // dword units
        else
            CurrentSubcommandHint = isBit ? (ushort)0x0001 : (ushort)0x0000;

        var buf = new byte[3 + 1 + 2];
        // 24-bit little-endian
        buf[0] = (byte)(start & 0xFF);
        buf[1] = (byte)((start >> 8) & 0xFF);
        buf[2] = (byte)((start >> 16) & 0xFF);
        buf[3] = DevCodeBinary(device);
        BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)count);
        return buf;
    }

    // 5. Hàm kiểm tra device có hỗ trợ đọc dword trực tiếp không (tuỳ CPU, ví dụ D, W, R, ZR thường hỗ trợ)
    private static bool SupportsDirectDWordRead(DeviceCode device)
    {
        return device == DeviceCode.D || device == DeviceCode.W || device == DeviceCode.R || device == DeviceCode.ZR;
    }
}
