using System.Diagnostics;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Modbus FC03 probe for the <see cref="ModbusDriverOptions"/>-shaped driver config.
///     Opens a TCP connection to the configured endpoint and sends a one-shot FC03
///     (Read Holding Registers, qty 1 @ address 0) handshake. A normal FC03 response
///     or a Modbus exception PDU both confirm a live Modbus device (green + latency);
///     TCP failure surfaces the SocketError; a Modbus-level handshake failure after
///     TCP succeeds surfaces a targeted message; timeout surfaces "timed out after Ns."
/// </summary>
public sealed class ModbusDriverProbe : IDriverProbe
{
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

    // FC03 Read Holding Registers: function=0x03, addr-hi=0, addr-lo=0, qty-hi=0, qty-lo=1
    private static readonly byte[] Fc03Pdu = [0x03, 0x00, 0x00, 0x00, 0x01];

    /// <inheritdoc />
    public string DriverType => "Modbus";

    /// <summary>The parsed probe target — host/port/unitId + the effective connection timeout + the wire
    /// transport mode, all read from the SAME factory DTO shape (<c>ModbusDriverConfigDto</c>) the driver
    /// factory parses, so <c>timeoutMs</c> binds identically to the factory (R2-11, 05/CONV-2; the
    /// OpcUaClient parity rule) and an <c>RtuOverTcp</c>-authored config probes over RTU framing —
    /// matching how the driver will actually run.</summary>
    internal readonly record struct ProbeTarget(string Host, int Port, byte UnitId, TimeSpan Timeout, ModbusTransportMode Transport);

    /// <summary>Parse the driver config JSON into a <see cref="ProbeTarget"/> using the factory DTO shape.
    /// Returns a null target + an error message when the JSON is invalid or has no host/port. The effective
    /// timeout mirrors the factory (<c>TimeSpan.FromMilliseconds(dto.TimeoutMs ?? …)</c>); when the config
    /// omits <c>timeoutMs</c> the caller's <paramref name="fallbackTimeout"/> is used. <c>Transport</c>
    /// mirrors the factory's null-defaults-to-Tcp convention (<see cref="ModbusDriverFactoryExtensions"/>);
    /// an unrecognized value is reported as a probe-target error rather than silently falling back.</summary>
    /// <param name="configJson">The driver config JSON (factory DTO shape).</param>
    /// <param name="fallbackTimeout">The timeout used when the config omits <c>timeoutMs</c>.</param>
    /// <returns>The parsed target, or a null target with an error string.</returns>
    internal static (ProbeTarget? Target, string? Error) TryParseProbeTarget(string configJson, TimeSpan fallbackTimeout)
    {
        ModbusDriverFactoryExtensions.ModbusDriverConfigDto? dto;
        try { dto = JsonSerializer.Deserialize<ModbusDriverFactoryExtensions.ModbusDriverConfigDto>(configJson, _opts); }
        catch (Exception ex) { return (null, $"Config JSON is invalid: {ex.Message}"); }
        if (dto is null) return (null, "Config JSON deserialized to null.");

        var port = dto.Port ?? 502;
        if (string.IsNullOrWhiteSpace(dto.Host) || port <= 0)
            return (null, "Config has no host/port to probe.");

        var transportMode = ModbusTransportMode.Tcp;
        if (dto.Transport is not null && !Enum.TryParse(dto.Transport, ignoreCase: true, out transportMode))
        {
            return (null, $"Config has unknown Transport '{dto.Transport}'. " +
                $"Expected one of {string.Join(", ", Enum.GetNames<ModbusTransportMode>())}");
        }

        var timeout = dto.TimeoutMs is { } ms && ms > 0 ? TimeSpan.FromMilliseconds(ms) : fallbackTimeout;
        return (new ProbeTarget(dto.Host!, port, dto.UnitId ?? 1, timeout, transportMode), null);
    }

    /// <inheritdoc />
    public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
    {
        var (target, error) = TryParseProbeTarget(configJson, timeout);
        if (target is not { } t) return new(false, error!, null);

        var host = t.Host;
        var port = t.Port;
        var unitId = t.UnitId;
        // Honour the config's timeoutMs (factory parity) — the caller's timeout is the fallback.
        timeout = t.Timeout;

        var sw = Stopwatch.StartNew();
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(timeout);

        // Phase 1 — TCP connect, over whichever transport the config's Transport mode selects
        // (ModbusTransportFactory is the single mapping point — same switch the live driver uses).
        // autoReconnect=false: this is a one-shot probe, no retry loops.
        var transport = ModbusTransportFactory.Create(new ModbusDriverOptions
        {
            Host = host,
            Port = port,
            Timeout = timeout,
            Transport = t.Transport,
            AutoReconnect = false,
        });
        await using (transport.ConfigureAwait(false))
        {
            try
            {
                await transport.ConnectAsync(cts.Token).ConfigureAwait(false);
            }
            catch (SocketException ex)
            {
                return new(false, $"Connect failed: {ex.SocketErrorCode}", null);
            }
            catch (OperationCanceledException)
            {
                return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
            }
            catch (Exception ex)
            {
                return new(false, ex.Message, null);
            }

            // Phase 2 — FC03 handshake. TCP is up; now prove a Modbus device is answering.
            try
            {
                await transport.SendAsync(unitId, Fc03Pdu, cts.Token).ConfigureAwait(false);
                sw.Stop();
                return new(true, "Modbus FC03 OK", sw.Elapsed);
            }
            catch (ModbusException)
            {
                // Device replied with an exception PDU — it IS a real Modbus device.
                sw.Stop();
                return new(true, "Modbus FC03 OK (device returned exception PDU)", sw.Elapsed);
            }
            catch (OperationCanceledException)
            {
                return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
            }
            catch (Exception ex)
            {
                sw.Stop();
                return new(false, $"Reachable at {host}:{port} but Modbus FC03 handshake failed: {ex.Message}", null);
            }
        }
    }
}
