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

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

/// <summary>
///     Two-phase Test-Connect probe for the <see cref="FocasDriverOptions"/>-shaped driver config.
///     Phase 1: bare TCP connect to the first device's FOCAS Ethernet address + port to quickly
///     reject unreachable targets (preserves the original "Connect failed" / "timed out"
///     messages). Phase 2: a real FOCAS session via the managed <see cref="FocasWireClient"/> — the
///     two-socket initiate handshake plus one sample read (<c>cnc_statinfo</c>). A handshake +
///     read that succeeds confirms the remote endpoint is a real FOCAS CNC, not just a TCP
///     listener.
///     <para>
///     <b>Why a wire-client probe (not FWLIB).</b> The pure-managed wire client is the driver's
///     only read backend (the FWLIB / out-of-process paths were retired in the Wire migration), so
///     the probe must exercise the same path the driver actually uses. The previous probe issued
///     the <c>cnc_allclibhndl3</c> FWLIB P/Invoke and, on any host without the native library (the
///     normal case — macOS dev boxes, Linux CI, and the Windows hosts that run the managed client),
///     degraded to <c>Ok=true</c> "TCP reachability only". That made every bare TCP listener look
///     HEALTHY — exactly how a Makino 31i-B looked "healthy" while no FOCAS data flowed. The wire
///     probe reports HEALTHY only on a genuine FOCAS session + read. See
///     <c>docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md</c> (Phase 8).
///     </para>
///     <para>
///     The wire client honours the linked CTS (<c>ct</c> + <c>CancelAfter(timeout)</c>) and its
///     reads are abort-bounded (see <see cref="FocasWireProtocol"/>), so the probe always returns
///     within the timeout budget even against a host that accepts TCP then stalls.
///     </para>
/// </summary>
public sealed class FocasDriverProbe : IDriverProbe
{
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

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

    /// <inheritdoc />
    public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
    {
        FocasDriverOptions? opts;
        try { opts = JsonSerializer.Deserialize<FocasDriverOptions>(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);

        var sw = Stopwatch.StartNew();

        // Phase 1: bare TCP preflight — fast rejection for unreachable hosts. Messages unchanged.
        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: real FOCAS session via the managed wire client — initiate handshake + one
        // sample read. Bounded by a linked CTS = ct + CancelAfter(budget); the wire reads are
        // abort-bounded so a TCP-accept-then-stall host can't hold the probe past the budget.
        using var sessionCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        var budget = timeout > TimeSpan.Zero ? timeout : TimeSpan.FromSeconds(1);
        sessionCts.CancelAfter(budget);

        try
        {
            await using var wire = new FocasWireClient();
            await wire.ConnectAsync(host, port, budget, sessionCts.Token).ConfigureAwait(false);
            var status = await wire.ReadStatusAsync(sessionCts.Token, budget).ConfigureAwait(false);
            sw.Stop();

            return status.IsOk
                ? new(true, $"FOCAS session OK at {host}:{port} (cnc_statinfo)", sw.Elapsed)
                : new(false, $"Reachable at {host}:{port} but FOCAS read failed: EW_{status.Rc}", null);
        }
        catch (OperationCanceledException)
        {
            return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
        }
        catch (FocasWireException ex)
        {
            // TCP-reachable but the FOCAS initiate/read failed — a listener that is not a CNC.
            return new(false, $"Reachable at {host}:{port} but FOCAS session failed: {ex.Message}", null);
        }
    }

    private static (string host, int port) ExtractTarget(FocasDriverOptions opts)
    {
        // Parse the first device's focas:// address to extract host + port.
        var firstDevice = opts.Devices.FirstOrDefault();
        if (firstDevice is null) return (string.Empty, 0);

        var parsed = FocasHostAddress.TryParse(firstDevice.HostAddress);
        if (parsed is null) return (string.Empty, 0);

        return (parsed.Host, parsed.Port);
    }
}
