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

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

/// <summary>
///     Probe for the <see cref="OpcUaClientDriverOptions"/>-shaped driver config. Parses the
///     first endpoint URL (from <see cref="OpcUaClientDriverOptions.EndpointUrls"/> or the
///     convenience <see cref="OpcUaClientDriverOptions.EndpointUrl"/> fallback), performs a
///     TCP-connect preflight to detect closed ports quickly, then executes a real OPC UA
///     <c>GetEndpoints</c> discovery handshake (mirroring the driver's
///     <c>SelectMatchingEndpointAsync</c>) to confirm the remote process is actually an OPC UA
///     server. No session is opened and no authentication is required.
/// </summary>
public sealed class OpcUaClientDriverProbe : IDriverProbe
{
    // Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions so the probe and the
    // factory parse a given DriverConfig the same way. The JsonStringEnumConverter lets
    // enum-valued knobs be authored as their string names.
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

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

    /// <inheritdoc />
    public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
    {
        // G-2b note: the probe does an UNAUTHENTICATED GetEndpoints discovery preflight
        // (BuildMinimalAppConfig sets no user identity) and never reads opts.Password /
        // opts.UserCertificatePassword. Those credentials — including any secret: refs — are
        // therefore intentionally NOT resolved here; resolving them would be dead code. Secret
        // resolution happens lazily in OpcUaClientDriver's async session-open path, where the
        // credentials are actually consumed.
        OpcUaClientDriverOptions? opts;
        try { opts = JsonSerializer.Deserialize<OpcUaClientDriverOptions>(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, endpointUrl) = ExtractTarget(opts);
        if (string.IsNullOrWhiteSpace(host) || port <= 0)
            return new(false, "Config has no host/port to probe.", null);

        // Bound the whole probe (both phases) by the caller timeout, independent of `ct` — a
        // stalled OPC UA endpoint that accepts TCP but never answers GetEndpoints must not block
        // the probe when the caller passes CancellationToken.None.
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(timeout);

        // --- TCP preflight: fast-fail for closed ports / 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);
        }

        // --- OPC UA discovery handshake: confirm the remote is an OPC UA server ---
        // Mirrors OpcUaClientDriver.SelectMatchingEndpointAsync exactly:
        //   DiscoveryClient.CreateAsync(appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, ct)
        // GetEndpoints needs no session, no application certificate, and no authentication.
        try
        {
            var appConfig = BuildMinimalAppConfig();
            using var client = await DiscoveryClient.CreateAsync(
                appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, cts.Token).ConfigureAwait(false);
            var endpoints = await client.GetEndpointsAsync(null, cts.Token).ConfigureAwait(false);
            sw.Stop();

            if (endpoints is { Count: > 0 })
                return new(true, $"OPC UA: {endpoints.Count} endpoint(s)", sw.Elapsed);

            return new(false,
                $"Reachable at {host}:{port} but OPC UA handshake failed: server published 0 endpoints",
                null);
        }
        catch (OperationCanceledException)
        {
            return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
        }
        catch (Exception ex)
        {
            return new(false,
                $"Reachable at {host}:{port} but OPC UA handshake failed: {ex.Message}",
                null);
        }
    }

    /// <summary>
    ///     Builds a minimal <see cref="ApplicationConfiguration"/> suitable for unauthenticated
    ///     discovery (GetEndpoints). No PKI stores, no session certificate — discovery traffic
    ///     is plain-text and does not require a client certificate.
    /// </summary>
    private static ApplicationConfiguration BuildMinimalAppConfig() =>
        new()
        {
            ApplicationName = "OtOpcUa.Probe",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration
            {
                AutoAcceptUntrustedCertificates = true,
            },
            TransportQuotas = new TransportQuotas(),
            ClientConfiguration = new ClientConfiguration(),
            DisableHiResClock = true,
        };

    private static (string host, int port, string endpointUrl) ExtractTarget(OpcUaClientDriverOptions opts)
    {
        // EndpointUrls wins over the convenience EndpointUrl when both are set.
        var endpointUrl = opts.EndpointUrls.FirstOrDefault()
                          ?? (string.IsNullOrWhiteSpace(opts.EndpointUrl) ? null : opts.EndpointUrl);
        if (endpointUrl is null) return (string.Empty, 0, string.Empty);

        // Parse as a URI — opc.tcp://host:port is a valid URI.
        if (!Uri.TryCreate(endpointUrl, UriKind.Absolute, out var uri))
            return (string.Empty, 0, string.Empty);

        var host = uri.Host;
        var port = uri.IsDefaultPort ? 4840 : uri.Port;
        return (host, port, endpointUrl);
    }
}
