using System.Diagnostics;
using System.Buffers.Binary;
using System.IO;
using System.Net.Sockets;
using Serilog;

namespace ConeCalorimeter.Services;

public sealed class TcpDeviceConnectionService : ITcpDeviceConnectionService
{
    private const byte ReadCoilsFunction = 0x01;
    private const byte ReadHoldingRegistersFunction = 0x03;
    private const byte WriteSingleCoilFunction = 0x05;
    private const byte WriteSingleRegisterFunction = 0x06;
    private const byte WriteMultipleRegistersFunction = 0x10;
    private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(3);
    private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan ReadWriteTimeout = TimeSpan.FromSeconds(2);

    private readonly object _syncRoot = new();
    private readonly TcpDeviceConnectionOptions _options;
    private CancellationTokenSource? _connectionLoopCancellation;
    private Task? _connectionLoopTask;
    private TcpClient? _client;
    private ushort _transactionId;
    private bool _isConnected;
    private string _statusText;

    public TcpDeviceConnectionService()
        : this(TcpDeviceConnectionOptions.Default)
    {
    }

    public TcpDeviceConnectionService(TcpDeviceConnectionOptions options)
    {
        _options = options;
        _statusText = $"未连接设备 {_options.Host}:{_options.Port}";
        Log.Information(
            "TCP device service created. Endpoint={Endpoint}, UnitId={UnitId}.",
            Endpoint,
            _options.UnitId);
    }

    public bool IsConnected
    {
        get
        {
            lock (_syncRoot)
            {
                return _isConnected;
            }
        }
    }

    public string Endpoint => $"{_options.Host}:{_options.Port}";

    public string StatusText
    {
        get
        {
            lock (_syncRoot)
            {
                return _statusText;
            }
        }
    }

    public event EventHandler? ConnectionStatusChanged;

    public Task StartAsync()
    {
        lock (_syncRoot)
        {
            if (_connectionLoopTask is not null)
            {
                return Task.CompletedTask;
            }

            _connectionLoopCancellation = new CancellationTokenSource();
            _connectionLoopTask = RunConnectionLoopAsync(_connectionLoopCancellation.Token);
        }

        return Task.CompletedTask;
    }

    public async Task StopAsync()
    {
        Task? loopTask;
        CancellationTokenSource? cancellation;

        lock (_syncRoot)
        {
            loopTask = _connectionLoopTask;
            cancellation = _connectionLoopCancellation;
            _connectionLoopTask = null;
            _connectionLoopCancellation = null;
        }

        if (cancellation is not null)
        {
            await cancellation.CancelAsync();
        }

        CloseCurrentClient();
        SetConnectionState(false, $"未连接设备 {_options.Host}:{_options.Port}");

        if (loopTask is not null)
        {
            try
            {
                await loopTask;
            }
            catch (OperationCanceledException)
            {
            }
        }

        cancellation?.Dispose();
        Debug.WriteLine("TCP device connection service stopped.");
        Log.Information("TCP device connection service stopped. Endpoint={Endpoint}.", Endpoint);
    }

    public async ValueTask DisposeAsync()
    {
        await StopAsync();
    }

    public bool TryReadFloat(ushort registerAddress, out double value)
    {
        value = double.NaN;

        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                value = ReadFloat(_client, registerAddress);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP float register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
                    Endpoint,
                    registerAddress);
                SetConnectionState(false, $"读取寄存器 {registerAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryReadFloatValues(ushort registerAddress, out ModbusFloatReadResult result)
    {
        result = default;

        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                result = ReadFloatValues(_client, registerAddress);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device register {registerAddress} float values read failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP float raw register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
                    Endpoint,
                    registerAddress);
                SetConnectionState(false, $"读取寄存器 {registerAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryReadInt16(ushort registerAddress, out int value)
    {
        value = 0;

        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                value = ReadInt16(_client, registerAddress);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device register {registerAddress} read failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP int16 register read failed. Endpoint={Endpoint}, Register=D{RegisterAddress}.",
                    Endpoint,
                    registerAddress);
                SetConnectionState(false, $"读取寄存器 {registerAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryWriteInt16(ushort registerAddress, short value)
    {
        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                WriteInt16(_client, registerAddress, value);
                Log.Information(
                    "TCP int16 register write succeeded. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
                    Endpoint,
                    registerAddress,
                    value);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device register {registerAddress} write failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP int16 register write failed. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}.",
                    Endpoint,
                    registerAddress,
                    value);
                SetConnectionState(false, $"写入寄存器 {registerAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryWriteFloat(ushort registerAddress, float value)
    {
        return TryWriteFloat(registerAddress, value, ModbusFloatByteOrder.Abcd);
    }

    public bool TryWriteFloat(ushort registerAddress, float value, ModbusFloatByteOrder byteOrder)
    {
        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                WriteFloat(_client, registerAddress, value, byteOrder);
                Log.Information(
                    "TCP float register write succeeded. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}, ByteOrder={ByteOrder}.",
                    Endpoint,
                    registerAddress,
                    value,
                    byteOrder);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device register {registerAddress} float write failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP float register write failed. Endpoint={Endpoint}, Register=D{RegisterAddress}, Value={Value}, ByteOrder={ByteOrder}.",
                    Endpoint,
                    registerAddress,
                    value,
                    byteOrder);
                SetConnectionState(false, $"写入寄存器 {registerAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryReadCoil(ushort coilAddress, out bool value)
    {
        value = false;

        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                value = ReadCoil(_client, coilAddress);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device coil {coilAddress} read failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP coil read failed. Endpoint={Endpoint}, Coil=M{CoilAddress}.",
                    Endpoint,
                    coilAddress);
                SetConnectionState(false, $"读取线圈 {coilAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    public bool TryWriteCoil(ushort coilAddress, bool value)
    {
        lock (_syncRoot)
        {
            if (_client is null || !IsTcpClientConnected(_client))
            {
                CloseCurrentClientCore();
                return false;
            }

            try
            {
                WriteCoil(_client, coilAddress, value);
                Log.Information(
                    "TCP coil write succeeded. Endpoint={Endpoint}, Coil=M{CoilAddress}, Value={Value}.",
                    Endpoint,
                    coilAddress,
                    value);
                return true;
            }
            catch (Exception ex) when (ex is IOException or SocketException or InvalidDataException or ObjectDisposedException)
            {
                Debug.WriteLine($"TCP device coil {coilAddress} write failed: {ex.Message}");
                Log.Warning(
                    ex,
                    "TCP coil write failed. Endpoint={Endpoint}, Coil=M{CoilAddress}, Value={Value}.",
                    Endpoint,
                    coilAddress,
                    value);
                SetConnectionState(false, $"写入线圈 {coilAddress} 失败：{ex.Message}");
                CloseCurrentClientCore();
                return false;
            }
        }
    }

    private async Task RunConnectionLoopAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await ConnectAsync(cancellationToken);
                await MonitorConnectionAsync(cancellationToken);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"TCP device connection failed: {ex.Message}");
                Log.Warning(ex, "TCP device connection loop failed. Endpoint={Endpoint}.", Endpoint);
                SetConnectionState(false, $"连接设备失败：{ex.Message}");
            }
            finally
            {
                CloseCurrentClient();
            }

            try
            {
                await Task.Delay(RetryDelay, cancellationToken);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                break;
            }
        }
    }

    private async Task ConnectAsync(CancellationToken cancellationToken)
    {
        SetConnectionState(false, $"正在连接设备 {_options.Host}:{_options.Port}...");
        Debug.WriteLine($"Connecting TCP device at {_options.Host}:{_options.Port}.");
        Log.Information("Connecting TCP device. Endpoint={Endpoint}, UnitId={UnitId}.", Endpoint, _options.UnitId);

        var client = new TcpClient();
        var connectTask = client.ConnectAsync(_options.Host, _options.Port, cancellationToken).AsTask();
        var completedTask = await Task.WhenAny(connectTask, Task.Delay(ConnectTimeout, cancellationToken));

        if (!ReferenceEquals(completedTask, connectTask))
        {
            client.Dispose();
            throw new TimeoutException($"TCP device connection timed out after {ConnectTimeout.TotalSeconds:0} seconds.");
        }

        await connectTask;
        client.ReceiveTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
        client.SendTimeout = (int)ReadWriteTimeout.TotalMilliseconds;

        lock (_syncRoot)
        {
            _client?.Dispose();
            _client = client;
        }

        SetConnectionState(true, $"已连接设备 {_options.Host}:{_options.Port}");
        Debug.WriteLine($"TCP device connected at {_options.Host}:{_options.Port}.");
        Log.Information("TCP device connected. Endpoint={Endpoint}.", Endpoint);
    }

    private async Task MonitorConnectionAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            TcpClient? client;
            lock (_syncRoot)
            {
                client = _client;
            }

            if (client is null || !IsTcpClientConnected(client))
            {
                Debug.WriteLine("TCP device connection lost.");
                Log.Warning("TCP device connection lost. Endpoint={Endpoint}.", Endpoint);
                SetConnectionState(false, "设备连接已断开");
                return;
            }

            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
        }
    }

    private static bool IsTcpClientConnected(TcpClient client)
    {
        try
        {
            var socket = client.Client;
            return socket.Connected && !(socket.Poll(0, SelectMode.SelectRead) && socket.Available == 0);
        }
        catch (SocketException)
        {
            return false;
        }
        catch (ObjectDisposedException)
        {
            return false;
        }
    }

    private void CloseCurrentClient()
    {
        lock (_syncRoot)
        {
            CloseCurrentClientCore();
        }
    }

    private void CloseCurrentClientCore()
    {
        var client = _client;
        _client = null;
        _isConnected = false;
        client?.Dispose();
    }

    private double ReadFloat(TcpClient client, ushort registerAddress)
    {
        return ReadFloatValues(client, registerAddress).Abcd;
    }

    private ModbusFloatReadResult ReadFloatValues(TcpClient client, ushort registerAddress)
    {
        var pdu = ReadHoldingRegisters(client, registerAddress, 2);

        if (pdu.Length != 6 || pdu[1] != 4)
        {
            throw new InvalidDataException("Invalid Modbus TCP float response.");
        }

        return new ModbusFloatReadResult(pdu[2], pdu[3], pdu[4], pdu[5]);
    }

    private int ReadInt16(TcpClient client, ushort registerAddress)
    {
        var pdu = ReadHoldingRegisters(client, registerAddress, 1);

        if (pdu.Length != 4 || pdu[1] != 2)
        {
            throw new InvalidDataException("Invalid Modbus TCP int16 response.");
        }

        return BinaryPrimitives.ReadInt16BigEndian(pdu[2..4]);
    }

    private void WriteInt16(TcpClient client, ushort registerAddress, short value)
    {
        Span<byte> payload = stackalloc byte[4];
        BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
        BinaryPrimitives.WriteInt16BigEndian(payload[2..4], value);

        var pdu = SendModbusRequest(client, WriteSingleRegisterFunction, payload);

        if (pdu.Length != 5
            || BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
            || BinaryPrimitives.ReadInt16BigEndian(pdu[3..5]) != value)
        {
            throw new InvalidDataException("Invalid Modbus TCP register write response.");
        }
    }

    private void WriteFloat(TcpClient client, ushort registerAddress, float value, ModbusFloatByteOrder byteOrder)
    {
        Span<byte> payload = stackalloc byte[9];
        BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
        BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 2);
        payload[4] = 4;
        WriteFloatPayloadBytes(payload[5..9], value, byteOrder);

        var pdu = SendModbusRequest(client, WriteMultipleRegistersFunction, payload);

        if (pdu.Length != 5
            || BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != registerAddress
            || BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != 2)
        {
            throw new InvalidDataException("Invalid Modbus TCP float write response.");
        }
    }

    private static void WriteFloatPayloadBytes(Span<byte> destination, float value, ModbusFloatByteOrder byteOrder)
    {
        Span<byte> raw = stackalloc byte[4];
        BinaryPrimitives.WriteInt32BigEndian(raw, BitConverter.SingleToInt32Bits(value));

        switch (byteOrder)
        {
            case ModbusFloatByteOrder.Abcd:
                raw.CopyTo(destination);
                break;
            case ModbusFloatByteOrder.Cdab:
                destination[0] = raw[2];
                destination[1] = raw[3];
                destination[2] = raw[0];
                destination[3] = raw[1];
                break;
            case ModbusFloatByteOrder.Badc:
                destination[0] = raw[1];
                destination[1] = raw[0];
                destination[2] = raw[3];
                destination[3] = raw[2];
                break;
            case ModbusFloatByteOrder.Dcba:
                destination[0] = raw[3];
                destination[1] = raw[2];
                destination[2] = raw[1];
                destination[3] = raw[0];
                break;
            default:
                throw new InvalidDataException($"Unsupported Modbus float byte order: {byteOrder}.");
        }
    }

    private bool ReadCoil(TcpClient client, ushort coilAddress)
    {
        Span<byte> payload = stackalloc byte[4];
        BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
        BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], 1);

        var pdu = SendModbusRequest(client, ReadCoilsFunction, payload);

        if (pdu.Length != 3 || pdu[1] != 1)
        {
            throw new InvalidDataException("Invalid Modbus TCP coil response.");
        }

        return (pdu[2] & 0x01) == 0x01;
    }

    private void WriteCoil(TcpClient client, ushort coilAddress, bool value)
    {
        Span<byte> payload = stackalloc byte[4];
        BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], coilAddress);
        BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], value ? (ushort)0xFF00 : (ushort)0x0000);

        var pdu = SendModbusRequest(client, WriteSingleCoilFunction, payload);

        if (pdu.Length != 5
            || BinaryPrimitives.ReadUInt16BigEndian(pdu[1..3]) != coilAddress
            || BinaryPrimitives.ReadUInt16BigEndian(pdu[3..5]) != (value ? (ushort)0xFF00 : (ushort)0x0000))
        {
            throw new InvalidDataException("Invalid Modbus TCP coil write response.");
        }
    }

    private byte[] ReadHoldingRegisters(TcpClient client, ushort registerAddress, ushort registerCount)
    {
        Span<byte> payload = stackalloc byte[4];
        BinaryPrimitives.WriteUInt16BigEndian(payload[0..2], registerAddress);
        BinaryPrimitives.WriteUInt16BigEndian(payload[2..4], registerCount);

        return SendModbusRequest(client, ReadHoldingRegistersFunction, payload);
    }

    private byte[] SendModbusRequest(TcpClient client, byte functionCode, ReadOnlySpan<byte> payload)
    {
        var transactionId = ++_transactionId;
        var pduLength = 1 + payload.Length;
        var request = new byte[7 + pduLength];

        BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(0, 2), transactionId);
        BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(2, 2), 0);
        BinaryPrimitives.WriteUInt16BigEndian(request.AsSpan(4, 2), (ushort)(1 + pduLength));
        request[6] = _options.UnitId;
        request[7] = functionCode;
        payload.CopyTo(request.AsSpan(8));

        var stream = client.GetStream();
        stream.Write(request);

        Span<byte> header = stackalloc byte[7];
        ReadExactly(stream, header);

        var responseTransactionId = BinaryPrimitives.ReadUInt16BigEndian(header[0..2]);
        var protocolId = BinaryPrimitives.ReadUInt16BigEndian(header[2..4]);
        var length = BinaryPrimitives.ReadUInt16BigEndian(header[4..6]);
        var unitId = header[6];

        if (responseTransactionId != transactionId || protocolId != 0 || unitId != _options.UnitId || length < 2)
        {
            throw new InvalidDataException("Invalid Modbus TCP response header.");
        }

        var pdu = new byte[length - 1];
        ReadExactly(stream, pdu);

        if (pdu[0] == (functionCode | 0x80))
        {
            throw new InvalidDataException($"Modbus exception code {pdu[1]}.");
        }

        if (pdu[0] != functionCode)
        {
            throw new InvalidDataException("Unexpected Modbus TCP function code.");
        }

        return pdu;
    }

    private static void ReadExactly(NetworkStream stream, Span<byte> buffer)
    {
        var totalRead = 0;
        while (totalRead < buffer.Length)
        {
            var read = stream.Read(buffer[totalRead..]);
            if (read == 0)
            {
                throw new IOException("TCP device closed the connection.");
            }

            totalRead += read;
        }
    }

    private void SetConnectionState(bool isConnected, string statusText)
    {
        var changed = false;
        lock (_syncRoot)
        {
            if (_isConnected != isConnected || _statusText != statusText)
            {
                _isConnected = isConnected;
                _statusText = statusText;
                changed = true;
            }
        }

        if (changed)
        {
            Log.Information(
                "TCP device state changed. Endpoint={Endpoint}, IsConnected={IsConnected}, Status={StatusText}.",
                Endpoint,
                isConnected,
                statusText);
            ConnectionStatusChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}
