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

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

/// <summary>
///     Test-Connect probe for the <see cref="S7DriverOptions"/>-shaped driver config.
///     Performs a two-phase check: (1) a bare TCP connect to verify the host is reachable,
///     then (2) a full ISO-on-TCP COTP CR/CC + S7 setup-communication handshake via
///     <see cref="Plc.OpenAsync"/> to confirm the remote endpoint actually speaks S7comm.
///     A device that accepts the TCP connection but is not an S7 PLC (wrong rack/slot,
///     non-S7 server) returns <c>Ok = false</c> with a "handshake failed" message instead
///     of a false-positive green tick.
///     Surfaces a green tick + latency on full success; red chip + detail on any failure;
///     "timed out" on the caller's cancellation.
/// </summary>
public sealed class S7DriverProbe : IDriverProbe
{
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

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

    /// <inheritdoc />
    public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
    {
        S7DriverOptions? opts;
        try { opts = JsonSerializer.Deserialize<S7DriverOptions>(configJson, _opts); }
        catch (Exception ex) { return new(false, $"Config JSON is invalid: {ex.Message}", null); }
        if (opts is null) return new(false, "Config JSON deserialized to null.", null);

        var (host, port) = ExtractTarget(opts);
        if (string.IsNullOrWhiteSpace(host) || port <= 0)
            return new(false, "Config has no host/port to probe.", null);

        // Phase 1: bare TCP preflight — fast rejection for unreachable hosts.
        var sw = Stopwatch.StartNew();
        try
        {
            using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await socket.ConnectAsync(host, port, ct);
        }
        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: S7 ISO-on-TCP handshake (COTP CR/CC + S7 setup-communication).
        // The Plc is opened and immediately closed — no reads or writes are performed.
        // Use a linked CTS so we can distinguish a real caller cancellation from
        // S7netplus's internal socket-read timeout (which also surfaces as OCE).
        var plc = new Plc(S7CpuTypeMap.ToS7Net(opts.CpuType), host, port, opts.Rack, opts.Slot);
        plc.ReadTimeout = (int)timeout.TotalMilliseconds;
        using var handshakeCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        handshakeCts.CancelAfter(timeout);
        try
        {
            await plc.OpenAsync(handshakeCts.Token);
            sw.Stop();

            if (plc.IsConnected)
                return new(true, $"S7 connected (CPU {opts.CpuType})", sw.Elapsed);

            return new(false, $"Reachable at {host}:{port} but S7 handshake failed: not connected", null);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Caller cancelled (e.g. user navigated away or server-side wall-clock guard fired).
            return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
        }
        catch (OperationCanceledException)
        {
            // Our own handshakeCts fired the timeout — the host is reachable but S7 is not responding.
            return new(false, $"Reachable at {host}:{port} but S7 handshake failed: timed out", null);
        }
        catch (Exception ex)
        {
            return new(false, $"Reachable at {host}:{port} but S7 handshake failed: {ex.Message}", null);
        }
        finally
        {
            plc.Close();
        }
    }

    private static (string host, int port) ExtractTarget(S7DriverOptions opts)
        => (opts.Host, opts.Port);
}
