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

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

/// <summary>
///     Two-phase Test-Connect probe for the <see cref="AbLegacyDriverOptions"/>-shaped driver config.
///     Phase 1: bare TCP connect to the first device's gateway host + EtherNet/IP port to
///     quickly reject unreachable targets. Phase 2: initialises a libplctag <see cref="LibplctagLegacyTagRuntime"/>
///     against the same device to open a real PCCC-over-EIP session — confirming the remote
///     endpoint actually speaks PCCC, not just accepts TCP.
///     A device that accepts the TCP connection but is not a PCCC controller (wrong path,
///     non-PCCC server) returns <c>Ok = false</c> with a "handshake failed" message instead of
///     a false-positive green tick.
///     <para>
///     Status-code discrimination: libplctag statuses that indicate the controller answered
///     the PCCC session but could not resolve the probe tag
///     (<see cref="Status.ErrorNotFound"/>, <see cref="Status.ErrorNoMatch"/>,
///     <see cref="Status.ErrorBadDevice"/>) are treated as "reachable — Ok = true" because
///     a non-existent tag name proves PCCC connectivity. Statuses that indicate a transport or
///     session failure (<see cref="Status.ErrorTimeout"/>, <see cref="Status.ErrorBadConnection"/>,
///     <see cref="Status.ErrorBadGateway"/>, <see cref="Status.ErrorWinsock"/>,
///     <see cref="Status.ErrorOpen"/>, <see cref="Status.ErrorClose"/>,
///     <see cref="Status.ErrorRead"/>, <see cref="Status.ErrorWrite"/>,
///     <see cref="Status.ErrorBadReply"/>, <see cref="Status.ErrorRemoteErr"/>,
///     <see cref="Status.ErrorPartial"/>, <see cref="Status.ErrorAbort"/>) are treated as
///     "handshake failed — Ok = false". All other error statuses default to
///     "handshake failed" (conservative).
///     </para>
/// </summary>
public sealed class AbLegacyDriverProbe : IDriverProbe
{
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

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

    /// <inheritdoc />
    public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
    {
        AbLegacyDriverOptions? opts;
        try { opts = JsonSerializer.Deserialize<AbLegacyDriverOptions>(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 (parsed, firstDevice) = ExtractTarget(opts);
        if (parsed is null || string.IsNullOrWhiteSpace(parsed.Gateway) || parsed.Port <= 0)
            return new(false, "Config has no host/port to probe.", null);

        var host = parsed.Gateway;
        var port = parsed.Port;

        // Bound both phases by the caller timeout, independent of `ct`, so a device that accepts
        // TCP but stalls the PCCC session cannot hang the probe.
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(timeout);

        // 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, cts.Token);
        }
        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: PCCC session via libplctag LibplctagLegacyTagRuntime.InitializeAsync.
        // Open a real PCCC-over-EIP session to confirm the remote endpoint speaks PCCC, not just TCP.
        var profile = AbLegacyPlcFamilyProfile.ForFamily(firstDevice?.PlcFamily ?? AbLegacyPlcFamily.Slc500);

        // Use the configured probe address (defaults to "S:0" — status file, first word).
        // S:0 is present on all PCCC PLCs that support the status file, so a "not found" response
        // still proves PCCC connectivity; a successful read is the happy path.
        var tagName = opts.Probe.ProbeAddress
            ?? FirstProbeableTagAddress(opts, firstDevice)
            ?? "S:0";

        var p = new AbLegacyTagCreateParams(
            Gateway: parsed.Gateway,
            Port: parsed.Port,
            CipPath: parsed.CipPath,
            LibplctagPlcAttribute: profile.LibplctagPlcAttribute,
            TagName: tagName,
            Timeout: timeout);

        var rt = new LibplctagLegacyTagRuntime(p);
        try
        {
            await rt.InitializeAsync(cts.Token);
            sw.Stop();

            // InitializeAsync completed without throwing — either the tag was found (Ok) or
            // libplctag returned a non-exception status. Check via GetStatus().
            var statusCode = (Status)rt.GetStatus();
            if (IsReachableStatus(statusCode))
            {
                var msg = statusCode == Status.Ok
                    ? "PCCC session OK"
                    : "PCCC session OK (controller reachable; probe tag not found)";
                return new(true, msg, sw.Elapsed);
            }

            // Non-Ok but non-exception: a session/transport error surfaced in the status word.
            return new(false, $"Reachable at {host}:{port} but PCCC handshake failed: {statusCode}", null);
        }
        catch (OperationCanceledException)
        {
            return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
        }
        catch (LibPlcTagException ex)
        {
            // LibPlcTagException carries the Status; classify as reachable vs transport failure.
            if (IsReachableException(ex))
            {
                sw.Stop();
                return new(true, "PCCC session OK (controller reachable; probe tag not found)", sw.Elapsed);
            }

            return new(false, $"Reachable at {host}:{port} but PCCC handshake failed: {ex.Message}", null);
        }
        catch (Exception ex)
        {
            return new(false, $"Reachable at {host}:{port} but PCCC handshake failed: {ex.Message}", null);
        }
        finally
        {
            rt.Dispose();
        }
    }

    /// <summary>
    ///     Returns <c>true</c> for libplctag statuses that indicate the PCCC controller answered
    ///     but the probe tag name was not found or did not match — sufficient to confirm PCCC
    ///     reachability. Returns <c>true</c> for <see cref="Status.Ok"/> (tag was found).
    /// </summary>
    private static bool IsReachableStatus(Status s) => s is
        Status.Ok or
        Status.ErrorNotFound or
        Status.ErrorNoMatch or
        Status.ErrorBadDevice;

    /// <summary>
    ///     Inspects a <see cref="LibPlcTagException"/> message to decide whether the error is a
    ///     "controller answered PCCC but tag not found" condition (reachable) versus a session /
    ///     transport failure (unreachable). Falls back to checking the exception message for
    ///     known tag-not-found phrases used by the libplctag native library.
    /// </summary>
    private static bool IsReachableException(LibPlcTagException ex)
    {
        // libplctag wraps the Status in the message as "STATUS_NOT_FOUND", "STATUS_NO_MATCH", etc.
        var msg = ex.Message ?? string.Empty;
        return msg.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("NO_MATCH", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("BAD_DEVICE", StringComparison.OrdinalIgnoreCase);
    }

    private static (AbLegacyHostAddress? parsed, AbLegacyDeviceOptions? device) ExtractTarget(AbLegacyDriverOptions opts)
    {
        // Parse the first device's ab:// host address to extract the gateway IP + EIP port.
        var firstDevice = opts.Devices.FirstOrDefault();
        if (firstDevice is null) return (null, null);

        var parsed = AbLegacyHostAddress.TryParse(firstDevice.HostAddress);
        return (parsed, firstDevice);
    }

    /// <summary>
    ///     Pick a PCCC address to probe from the authored raw tags: prefer a tag owned by the device
    ///     being probed, else any authored tag. v3 tags carry no typed address — the RawTagEntry's
    ///     <c>TagConfig</c> blob is mapped through <see cref="AbLegacyTagDefinitionFactory.FromTagConfig"/>
    ///     to recover the PCCC address. Returns <see langword="null"/> when no tag maps, so the caller
    ///     falls back to the status file (<c>S:0</c>).
    /// </summary>
    /// <param name="opts">The deserialised driver options.</param>
    /// <param name="firstDevice">The device being probed (matched by DeviceName / HostAddress).</param>
    /// <returns>A probeable PCCC address, or <see langword="null"/>.</returns>
    private static string? FirstProbeableTagAddress(AbLegacyDriverOptions opts, AbLegacyDeviceOptions? firstDevice)
    {
        static string? Address(RawTagEntry e) =>
            AbLegacyTagDefinitionFactory.FromTagConfig(e.TagConfig, e.RawPath, out var d) ? d.Address : null;

        foreach (var e in opts.RawTags)
            if ((string.Equals(e.DeviceName, firstDevice?.DeviceName, StringComparison.OrdinalIgnoreCase)
                 || string.Equals(e.DeviceName, firstDevice?.HostAddress, StringComparison.OrdinalIgnoreCase))
                && Address(e) is { } matched)
                return matched;
        foreach (var e in opts.RawTags)
            if (Address(e) is { } any)
                return any;
        return null;
    }
}
