using System.Diagnostics;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Serialization;
using Grpc.Core;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;

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

/// <summary>
///     Probe for the <see cref="GalaxyDriverOptions"/>-shaped driver config. Parses the
///     <c>Gateway.Endpoint</c> gRPC endpoint (e.g. <c>http://host:5120</c> or <c>host:5120</c>),
///     does a fast TCP-connect preflight to fail closed ports quickly, then issues a lightweight,
///     read-only gRPC ping — the <c>GalaxyRepository.TestConnection</c> unary RPC — against the
///     mxaccessgw to confirm the remote is actually a live gateway speaking gRPC. The channel is
///     built the same way the driver builds it (cleartext for <c>http://</c>, TLS for
///     <c>https://</c>), and the result is classified by the gRPC <see cref="StatusCode"/>:
///     <list type="bullet">
///       <item><c>OK</c> → reachable, gateway confirmed.</item>
///       <item><c>Unauthenticated</c> / <c>PermissionDenied</c> → ALSO reachable: an auth
///         rejection proves a live gateway gRPC server answered. The probe deliberately does
///         <b>not</b> resolve secrets — it sends whatever API-key string is in the config
///         (possibly an unresolved <c>env:</c>/<c>file:</c> ref or empty), so authentication is
///         expected to be rejected and that rejection is the positive signal.</item>
///       <item><c>Unavailable</c> / transport error / <c>DeadlineExceeded</c> → handshake
///         failed (the port answered TCP but did not complete a gRPC handshake).</item>
///     </list>
///     The ping is strictly read-only and never mutates gateway state.
/// </summary>
public sealed class GalaxyDriverProbe : IDriverProbe
{
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        Converters = { new JsonStringEnumConverter() },
    };

    /// <inheritdoc />
    // Matches DriverInstance.DriverType strings set by the AdminUI's GalaxyDriverPage.
    public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName;

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

        // --- 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, 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);
        }

        // --- gRPC ping: confirm the port is a live mxaccessgw speaking gRPC ---
        // TestConnection is a read-only metadata RPC (it asks the gateway whether it can reach
        // the Galaxy Repository SQL Server). We don't care about its boolean result — only that
        // the gateway answered the gRPC call. A successful return ⇒ OK; an auth rejection still
        // proves a live gateway; a transport failure ⇒ the port isn't a gateway. We never resolve
        // the API-key secret ref here (the host owns secret resolution): whatever string is in the
        // config is sent as-is, so an unresolved/empty key surfaces as an auth rejection = reachable.
        GalaxyRepositoryClient? client = null;
        try
        {
            client = GalaxyRepositoryClient.Create(BuildProbeClientOptions(opts.Gateway, timeout));

            using var deadlineCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            deadlineCts.CancelAfter(timeout);

            await client.TestConnectionAsync(deadlineCts.Token).ConfigureAwait(false);
            sw.Stop();

            // No RpcException ⇒ the gateway answered the gRPC call successfully.
            var (ok, message) = ClassifyRpc(StatusCode.OK, host, port);
            return new(ok, message, sw.Elapsed);
        }
        catch (RpcException ex)
        {
            sw.Stop();
            var (ok, message) = ClassifyRpc(ex.StatusCode, host, port);
            return new(ok, message, ok ? sw.Elapsed : null);
        }
        catch (Exception ex) when (ex is MxGatewayAuthenticationException or MxGatewayAuthorizationException)
        {
            // The gateway authenticated/authorized our call and rejected the (unresolved /
            // placeholder) key — the mxaccessgw client surfaces this as a typed exception, NOT a
            // raw RpcException. It still PROVES a live gateway gRPC server answered, so auth
            // rejection counts as reachable (the probe never resolves the real secret).
            sw.Stop();
            return new(true, "gateway reachable & speaking gRPC (auth not checked)", sw.Elapsed);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // The caller cancelled (their own timeout / shutdown) — surface a timeout message.
            return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
        }
        catch (OperationCanceledException)
        {
            // Our own per-call deadline fired before the gateway answered — treat as a failed
            // handshake (the port answered TCP but didn't complete a gRPC handshake in time).
            var (_, message) = ClassifyRpc(StatusCode.DeadlineExceeded, host, port);
            return new(false, message, null);
        }
        catch (Exception ex)
        {
            // Transport / channel construction failures (e.g. an https endpoint mismatch, a TLS
            // negotiation failure, or a non-gRPC server) — reachable on TCP but not a gateway.
            return new(false,
                $"Reachable at {host}:{port} but gateway gRPC handshake failed: {ex.Message}",
                null);
        }
        finally
        {
            if (client is not null)
                await client.DisposeAsync().ConfigureAwait(false);
        }
    }

    /// <summary>
    ///     Maps a gRPC <see cref="StatusCode"/> from the ping RPC to a probe outcome. Factored out
    ///     as a pure helper so the classification is unit-testable without a live gateway. An
    ///     auth rejection (<see cref="StatusCode.Unauthenticated"/> /
    ///     <see cref="StatusCode.PermissionDenied"/>) counts as reachable because it proves a live
    ///     gateway gRPC server answered.
    /// </summary>
    /// <param name="code">The gRPC status code returned by the ping RPC.</param>
    /// <param name="host">The probed host, used to compose the failure message.</param>
    /// <param name="port">The probed port, used to compose the failure message.</param>
    /// <returns>Whether the gateway should be considered reachable, plus a human-readable message.</returns>
    internal static (bool ok, string message) ClassifyRpc(StatusCode code, string host, int port) => code switch
    {
        StatusCode.OK => (true, "gateway gRPC OK"),
        StatusCode.Unauthenticated or StatusCode.PermissionDenied =>
            (true, "gateway reachable & speaking gRPC (auth not checked)"),
        _ => (false, $"Reachable at {host}:{port} but gateway gRPC handshake failed: {code}"),
    };

    /// <summary>
    ///     Builds the gRPC client options for the probe ping from the gateway config WITHOUT
    ///     resolving the API-key secret ref (the host owns secret resolution; the probe sends the
    ///     raw config string). Mirrors the driver's channel build — <c>UseTls</c> selects TLS vs
    ///     cleartext, and the CA path is honoured for TLS — but caps the call/connect timeouts to
    ///     the probe budget and uses a single attempt so an unreachable host fails fast.
    /// </summary>
    private static MxGatewayClientOptions BuildProbeClientOptions(GalaxyGatewayOptions gw, TimeSpan timeout)
    {
        // The gw client's Validate() rejects an empty API key. The config's secret ref may be an
        // unresolved env:/file: string (non-empty → passes) or, in degenerate configs, empty. Send
        // a non-empty placeholder only when the config carries no key string at all, so the gateway
        // can still answer (and reject) the call. We never resolve env:/file: refs to real secrets.
        var apiKey = string.IsNullOrWhiteSpace(gw.ApiKeySecretRef) ? "probe" : gw.ApiKeySecretRef;

        // Keep both timeouts inside the probe budget so the ping fails fast on an unreachable host.
        var budget = timeout > TimeSpan.Zero ? timeout : TimeSpan.FromSeconds(1);

        return new MxGatewayClientOptions
        {
            Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
            ApiKey = apiKey,
            UseTls = gw.UseTls,
            CaCertificatePath = gw.CaCertificatePath,
            ConnectTimeout = budget,
            DefaultCallTimeout = budget,
            // Leave Retry at the client default (as GalaxyDriver does) — an explicit
            // MaxAttempts=1 maps to 0 Polly retries, which Polly rejects as an invalid
            // RetryStrategyOptions. Fast-fail is already guaranteed: the TCP preflight rejects
            // unreachable hosts before the gRPC call, and the linked deadline caps the call to
            // the probe budget regardless of retries.
        };
    }

    private static (string host, int port) ExtractTarget(GalaxyDriverOptions opts)
    {
        var endpoint = opts.Gateway.Endpoint;
        if (string.IsNullOrWhiteSpace(endpoint)) return (string.Empty, 0);

        // Try absolute URI first (e.g. "http://hostname:5120" or "https://hostname:5120").
        if (Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
        {
            var host = uri.Host;
            // Uri.Port is -1 when not specified; default mxaccessgw port is 5001.
            var port = uri.Port > 0 ? uri.Port : 5001;
            return (host, port);
        }

        // Fallback: treat as "host:port" (no scheme).
        var colonIdx = endpoint.LastIndexOf(':');
        if (colonIdx > 0 && int.TryParse(endpoint[(colonIdx + 1)..], out var rawPort) && rawPort > 0)
            return (endpoint[..colonIdx], rawPort);

        // No port found — return the whole string as host with default port.
        return (endpoint, 5001);
    }
}
