using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Net.WebSockets;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using Nexus.Api.Observability;

namespace Nexus.Api.Services;

/// <summary>
/// Maintains Nexus' single trusted backend connection to the OpenClaw Gateway.
/// The connector implements the protocol-v4 challenge/connect handshake, keeps
/// request/response correlation inside the backend, and never exposes Gateway
/// credentials to the browser.
/// </summary>
public sealed class GatewayConnector : BackgroundService, IGatewayConnector
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

    private readonly IConfiguration _configuration;
    private readonly ILogger<GatewayConnector> _logger;
    private readonly GatewayConnectorOptions _options;
    private readonly IOpenClawDeviceIdentityStore _deviceIdentityStore;
    private readonly object _stateLock = new();
    private readonly SemaphoreSlim _sendLock = new(1, 1);
    private readonly SemaphoreSlim _reconnectSignal = new(0, 1);
    private readonly ConcurrentDictionary<string, TaskCompletionSource<JsonNode?>> _pending = new(StringComparer.Ordinal);
    private readonly ConcurrentQueue<GatewayEventEnvelope> _recentEvents = new();

    private ClientWebSocket? _socket;
    private GatewayConnectionState _state = GatewayConnectionState.Initializing;
    private string? _gatewayVersion;
    private string? _requiredVersion;
    private DateTimeOffset? _lastConnectedAt;
    private DateTimeOffset? _lastEventAt;
    private int _reconnectAttempts;
    private int? _protocolVersion;
    private string? _statusMessage;
    private string? _deviceId;
    private bool _deviceTokenConfigured;
    private bool _pairingRequired;
    private string? _pairingRequestId;
    private HashSet<string> _advertisedMethods = new(StringComparer.Ordinal);
    private HashSet<string> _advertisedEvents = new(StringComparer.Ordinal);
    private HashSet<string> _grantedScopes = new(StringComparer.Ordinal);
    private HashSet<string> _requestedScopes = new(StringComparer.Ordinal);
    private Uri? _endpointOverride;
    private string? _tlsFingerprintOverride;
    private string? _transientBootstrapToken;

    public GatewayConnector(
        IConfiguration configuration,
        IOptions<GatewayConnectorOptions> options,
        ILogger<GatewayConnector> logger)
        : this(
            configuration,
            options,
            logger,
            new OpenClawDeviceIdentityStore(
                options,
                Microsoft.Extensions.Logging.Abstractions.NullLogger<OpenClawDeviceIdentityStore>.Instance))
    {
    }

    public GatewayConnector(
        IConfiguration configuration,
        IOptions<GatewayConnectorOptions> options,
        ILogger<GatewayConnector> logger,
        IOpenClawDeviceIdentityStore deviceIdentityStore)
    {
        _configuration = configuration;
        _logger = logger;
        _options = options.Value;
        _deviceIdentityStore = deviceIdentityStore;
        _requestedScopes = _options.Scopes
            .Where(scope => !string.IsNullOrWhiteSpace(scope))
            .Select(scope => scope.Trim())
            .ToHashSet(StringComparer.Ordinal);
        var pinnedVersion = _configuration["Integrations:OpenClaw:RequiredVersion"];
        _requiredVersion = string.IsNullOrWhiteSpace(pinnedVersion)
            ? OpenClawGatewayProtocol.DefaultRequiredGatewayVersion
            : pinnedVersion.Trim();
    }

    public GatewayConnectionState ConnectionState
    {
        get { lock (_stateLock) return _state; }
    }

    public string? GatewayVersion
    {
        get { lock (_stateLock) return _gatewayVersion; }
    }

    public string? RequiredVersion
    {
        get { lock (_stateLock) return _requiredVersion; }
    }

    public DateTimeOffset? LastConnectedAt
    {
        get { lock (_stateLock) return _lastConnectedAt; }
    }

    public int ReconnectAttempts
    {
        get { lock (_stateLock) return _reconnectAttempts; }
    }

    public string? StatusMessage
    {
        get { lock (_stateLock) return _statusMessage; }
    }

    public string? DeviceId
    {
        get { lock (_stateLock) return _deviceId; }
    }

    public bool DeviceTokenConfigured
    {
        get { lock (_stateLock) return _deviceTokenConfigured; }
    }

    public bool PairingRequired
    {
        get { lock (_stateLock) return _pairingRequired; }
    }

    public string? PairingRequestId
    {
        get { lock (_stateLock) return _pairingRequestId; }
    }

    public int? ProtocolVersion
    {
        get { lock (_stateLock) return _protocolVersion; }
    }

    public IReadOnlySet<string> AdvertisedMethods
    {
        get { lock (_stateLock) return new HashSet<string>(_advertisedMethods, StringComparer.Ordinal); }
    }

    public IReadOnlySet<string> AdvertisedEvents
    {
        get { lock (_stateLock) return new HashSet<string>(_advertisedEvents, StringComparer.Ordinal); }
    }

    public IReadOnlySet<string> GrantedScopes
    {
        get { lock (_stateLock) return new HashSet<string>(_grantedScopes, StringComparer.Ordinal); }
    }

    public string? ActiveEndpoint
    {
        get
        {
            lock (_stateLock)
                return (_endpointOverride ?? BuildConfiguredWebSocketUri()).AbsoluteUri;
        }
    }

    public string? ActiveTlsFingerprint
    {
        get
        {
            lock (_stateLock)
                return _tlsFingerprintOverride ?? NormalizeFingerprint(_options.TlsFingerprint);
        }
    }

    public DateTimeOffset? LastEventAt
    {
        get { lock (_stateLock) return _lastEventAt; }
    }

    public bool Supports(string method)
    {
        if (string.IsNullOrWhiteSpace(method))
            return false;

        lock (_stateLock)
            return _advertisedMethods.Contains(method);
    }

    public IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100)
    {
        var bounded = Math.Clamp(limit, 1, _options.EventBufferCapacity);
        return _recentEvents
            .ToArray()
            .Reverse()
            .Take(bounded)
            .Select(item => item with { Payload = item.Payload?.DeepClone() })
            .ToList();
    }

    public async Task RequestOperatorScopesAsync(
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(scopes);
        var requested = scopes
            .Where(scope => !string.IsNullOrWhiteSpace(scope))
            .Select(scope => scope.Trim())
            .Distinct(StringComparer.Ordinal)
            .Order(StringComparer.Ordinal)
            .ToArray();
        if (requested.Length == 0 ||
            !requested.Contains("operator.read", StringComparer.Ordinal) ||
            requested.Any(scope => scope is not (
                "operator.read" or
                "operator.admin" or
                "operator.approvals")))
        {
            throw new ArgumentException(
                "Only an explicit OpenClaw operator scope set containing operator.read can be requested.",
                nameof(scopes));
        }

        ClientWebSocket? socket;
        lock (_stateLock)
        {
            _requestedScopes = requested.ToHashSet(StringComparer.Ordinal);
            if (requested.All(_grantedScopes.Contains))
                return;

            socket = _socket;
            _pairingRequired = false;
            _pairingRequestId = null;
            _statusMessage = "A new OpenClaw operator scope set was requested; reconnecting for pairing.";
        }

        if (socket is not { State: WebSocketState.Open })
        {
            SignalReconnect();
            return;
        }

        await _sendLock.WaitAsync(cancellationToken);
        try
        {
            if (socket.State == WebSocketState.Open)
            {
                await socket.CloseOutputAsync(
                    WebSocketCloseStatus.NormalClosure,
                    "operator scope change",
                    cancellationToken);
            }
        }
        catch (WebSocketException)
        {
            // The receive loop will reconnect and apply the requested scopes.
        }
        catch (ObjectDisposedException)
        {
            // The receive loop already observed the disconnect.
        }
        finally
        {
            _sendLock.Release();
        }
        SignalReconnect();
    }

    public async Task ConfigureEndpointAsync(
        string endpoint,
        string? tlsFingerprint,
        string? bootstrapToken,
        CancellationToken cancellationToken = default)
    {
        if (!Uri.TryCreate(endpoint?.Trim(), UriKind.Absolute, out var target) ||
            target.Scheme is not ("ws" or "wss") ||
            string.IsNullOrWhiteSpace(target.Host) ||
            !string.IsNullOrEmpty(target.UserInfo) ||
            !string.IsNullOrEmpty(target.Query) ||
            !string.IsNullOrEmpty(target.Fragment))
        {
            throw new ArgumentException("A normalized ws:// or wss:// OpenClaw endpoint is required.", nameof(endpoint));
        }

        var normalizedFingerprint = NormalizeFingerprint(tlsFingerprint);
        if (!string.IsNullOrWhiteSpace(tlsFingerprint) && normalizedFingerprint.Length != 64)
            throw new ArgumentException("TLS fingerprint must be a SHA-256 hexadecimal value.", nameof(tlsFingerprint));
        if (bootstrapToken is { Length: > 4096 } || bootstrapToken?.Any(char.IsControl) == true)
            throw new ArgumentException("Bootstrap token is invalid.", nameof(bootstrapToken));

        ClientWebSocket? socket;
        lock (_stateLock)
        {
            var endpointChanged = _endpointOverride is null ||
                !string.Equals(_endpointOverride.AbsoluteUri, target.AbsoluteUri, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(
                    _tlsFingerprintOverride ?? string.Empty,
                    normalizedFingerprint,
                    StringComparison.Ordinal);
            _endpointOverride = target;
            _tlsFingerprintOverride = normalizedFingerprint;
            if (endpointChanged)
                _transientBootstrapToken = null;
            if (!string.IsNullOrWhiteSpace(bootstrapToken))
                _transientBootstrapToken = bootstrapToken;
            _requestedScopes = new HashSet<string>(["operator.read"], StringComparer.Ordinal);
            _grantedScopes.Clear();
            _pairingRequired = false;
            _pairingRequestId = null;
            _reconnectAttempts = 0;
            _state = GatewayConnectionState.Reconnecting;
            _statusMessage = $"Connecting to the validated OpenClaw endpoint {target.Host}.";
            socket = _socket;
        }

        if (socket is { State: WebSocketState.Open })
        {
            await _sendLock.WaitAsync(cancellationToken);
            try
            {
                if (socket.State == WebSocketState.Open)
                {
                    await socket.CloseOutputAsync(
                        WebSocketCloseStatus.NormalClosure,
                        "setup endpoint change",
                        cancellationToken);
                }
            }
            catch (WebSocketException)
            {
                // The receive loop already observed the endpoint transition.
            }
            catch (ObjectDisposedException)
            {
                // The receive loop already disposed the socket.
            }
            finally
            {
                _sendLock.Release();
            }
        }

        SignalReconnect();
    }

    public async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        ClientWebSocket? socket;
        lock (_stateLock)
        {
            socket = _socket;
            _deviceTokenConfigured = false;
            _pairingRequired = false;
            _pairingRequestId = null;
            _grantedScopes.Clear();
            _statusMessage = "The adopted OpenClaw connection was detached locally.";
        }

        if (socket is not { State: WebSocketState.Open })
            return;

        await _sendLock.WaitAsync(cancellationToken);
        try
        {
            if (socket.State == WebSocketState.Open)
            {
                await socket.CloseOutputAsync(
                    WebSocketCloseStatus.NormalClosure,
                    "connection detached",
                    cancellationToken);
            }
        }
        catch (WebSocketException)
        {
            // The receive loop already observed the disconnect.
        }
        catch (ObjectDisposedException)
        {
            // The receive loop already disposed the socket.
        }
        finally
        {
            _sendLock.Release();
        }
    }

    public async Task<JsonNode?> InvokeAsync(
        string method,
        object? parameters = null,
        TimeSpan? timeout = null,
        CancellationToken cancellationToken = default,
        OpenClawInvocationContext? invocationContext = null)
    {
        if (string.IsNullOrWhiteSpace(method))
            throw new ArgumentException("Gateway method is required.", nameof(method));

        using var activity = NexusTelemetry.ActivitySource.StartActivity(
            "nexus.openclaw.gateway.rpc",
            ActivityKind.Client);
        activity?.SetTag("rpc.system", "openclaw");
        activity?.SetTag("rpc.method", method);
        var stopwatch = Stopwatch.StartNew();
        var outcome = "error";
        try
        {
            ClientWebSocket socket;
            lock (_stateLock)
            {
                if (_state != GatewayConnectionState.Connected ||
                    _socket is not { State: WebSocketState.Open } currentSocket)
                {
                    throw new OpenClawGatewayRpcException(
                        "GATEWAY_DISCONNECTED",
                        "OpenClaw Gateway is not connected.",
                        retryable: true);
                }

                if (_advertisedMethods.Count > 0 && !_advertisedMethods.Contains(method))
                {
                    throw new OpenClawGatewayRpcException(
                        "METHOD_UNAVAILABLE",
                        $"OpenClaw Gateway does not advertise '{method}'.");
                }

                socket = currentSocket;
            }

            var requestId = BuildGatewayRequestId(invocationContext?.CorrelationId);
            var completion = new TaskCompletionSource<JsonNode?>(
                TaskCreationOptions.RunContinuationsAsynchronously);
            if (!_pending.TryAdd(requestId, completion))
                throw new InvalidOperationException("Could not allocate a Gateway request id.");

            var parametersNode = parameters switch
            {
                null => new JsonObject(),
                JsonNode node => node.DeepClone(),
                _ => JsonSerializer.SerializeToNode(parameters, JsonOptions) ?? new JsonObject()
            };

            var request = OpenClawGatewayProtocol.BuildRpcRequest(
                requestId,
                method,
                parametersNode,
                invocationContext);

            try
            {
                await SendFrameAsync(socket, request, cancellationToken);
                var response = await completion.Task.WaitAsync(
                    timeout ?? TimeSpan.FromSeconds(_options.RpcTimeoutSeconds),
                    cancellationToken);
                outcome = "success";
                return response;
            }
            catch (TimeoutException)
            {
                outcome = "timeout";
                throw new OpenClawGatewayRpcException(
                    "GATEWAY_TIMEOUT",
                    $"OpenClaw Gateway method '{method}' timed out.",
                    retryable: true);
            }
            finally
            {
                _pending.TryRemove(requestId, out _);
            }
        }
        catch (OperationCanceledException)
        {
            outcome = "cancelled";
            throw;
        }
        catch (OpenClawGatewayRpcException)
        {
            if (outcome == "error")
                outcome = "gateway_error";
            throw;
        }
        finally
        {
            stopwatch.Stop();
            activity?.SetTag("rpc.outcome", outcome);
            NexusTelemetry.GatewayRpcDuration.Record(
                stopwatch.Elapsed.TotalMilliseconds,
                new KeyValuePair<string, object?>("outcome", outcome));
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ConnectAndReceiveAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (OpenClawGatewayRpcException exception)
            {
                CapturePairingFailure(exception);
                _logger.LogWarning(
                    "OpenClaw Gateway handshake failed with {Code}: {Message}",
                    exception.Code,
                    exception.Message);
                IncrementReconnectAttempts();
                SetState(GatewayConnectionState.Disconnected, BuildSafeErrorMessage(exception));
            }
            catch (Exception exception)
            {
                _logger.LogWarning(exception, "OpenClaw Gateway connection failed");
                IncrementReconnectAttempts();
                SetState(GatewayConnectionState.Disconnected, exception.Message);
            }
            finally
            {
                lock (_stateLock)
                    _socket = null;
                FailPending("OpenClaw Gateway connection closed.");
            }

            if (stoppingToken.IsCancellationRequested)
                break;

            if (ConnectionState == GatewayConnectionState.Failed)
            {
                try
                {
                    await _reconnectSignal.WaitAsync(stoppingToken);
                    continue;
                }
                catch (OperationCanceledException)
                {
                    break;
                }
            }

            if (_options.ReconnectMaxAttempts > 0 && ReconnectAttempts >= _options.ReconnectMaxAttempts)
            {
                SetState(
                    GatewayConnectionState.Failed,
                    $"Reconnect limit reached after {ReconnectAttempts} attempts.");
                continue;
            }

            var delay = ComputeBackoff();
            SetState(GatewayConnectionState.Reconnecting, $"Reconnect scheduled in {Math.Ceiling(delay.TotalSeconds)} seconds.");
            try
            {
                await _reconnectSignal.WaitAsync(delay, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break;
            }
        }
    }

    private async Task ConnectAndReceiveAsync(CancellationToken stoppingToken)
    {
        OpenClawGatewayProtocol.ValidateExternalClientIdentity(_options);

        using var socket = new ClientWebSocket();
        socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
        socket.Options.UseDefaultCredentials = false;

        var endpoint = BuildWebSocketUri();
        ConfigureTransportTrust(socket, endpoint);
        var gatewayBinding = BuildGatewayBinding(endpoint, GetActiveTlsFingerprint());
        var requiresDeviceIdentity = OpenClawGatewayProtocol.RequiresDeviceIdentity(endpoint);
        SetState(GatewayConnectionState.Reconnecting, $"Connecting to {endpoint.Host}.");
        await socket.ConnectAsync(endpoint, stoppingToken);

        using var handshakeTimeout = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
        handshakeTimeout.CancelAfter(TimeSpan.FromSeconds(_options.HandshakeTimeoutSeconds));

        var challenge = await ReceiveChallengeAsync(socket, handshakeTimeout.Token);
        if (string.IsNullOrWhiteSpace(challenge))
            throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "OpenClaw Gateway did not send connect.challenge.");

        var connectId = Guid.NewGuid().ToString("N");
        var (configuredToken, configuredPassword) = ResolveCredentials();
        string[] scopes;
        lock (_stateLock)
            scopes = _requestedScopes.Order(StringComparer.Ordinal).ToArray();
        string? token = configuredToken;
        string? password = configuredPassword;
        string? deviceToken = null;
        OpenClawDeviceIdentity? identity = null;
        OpenClawGatewayDeviceProof? deviceProof = null;

        if (requiresDeviceIdentity)
        {
            identity = await _deviceIdentityStore.LoadOrCreateAsync(handshakeTimeout.Token);
            var storedToken = await _deviceIdentityStore.LoadTokenAsync(
                identity.DeviceId,
                "operator",
                gatewayBinding,
                handshakeTimeout.Token);
            if (storedToken is not null)
            {
                lock (_stateLock)
                    _deviceTokenConfigured = true;

                if (string.IsNullOrWhiteSpace(configuredToken) &&
                    string.IsNullOrWhiteSpace(configuredPassword))
                {
                    deviceToken = storedToken.Token;
                    token = storedToken.Token;
                    password = null;
                }
            }

            var signedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            var platform = GetPlatform();
            var signaturePayload = OpenClawGatewayProtocol.BuildDeviceAuthPayloadV3(
                identity.DeviceId,
                _options.ClientId,
                _options.ClientMode,
                "operator",
                scopes,
                signedAt,
                token,
                challenge,
                platform,
                _options.DeviceFamily);
            deviceProof = new OpenClawGatewayDeviceProof(
                identity.DeviceId,
                identity.PublicKey,
                identity.Sign(signaturePayload),
                signedAt,
                challenge);

            lock (_stateLock)
                _deviceId = identity.DeviceId;
        }

        var connectFrame = OpenClawGatewayProtocol.BuildConnectRequest(
            connectId,
            _options,
            token,
            password,
            GetClientVersion(),
            GetPlatform(),
            CultureInfo.CurrentUICulture.Name,
            deviceProof,
            scopes,
            _options.DeviceFamily,
            deviceToken);

        await SendFrameAsync(socket, connectFrame, handshakeTimeout.Token);
        var hello = await ReceiveHelloAsync(socket, connectId, handshakeTimeout.Token);

        if (identity is not null && !string.IsNullOrWhiteSpace(hello.DeviceToken))
        {
            await _deviceIdentityStore.StoreTokenAsync(
                identity.DeviceId,
                hello.Role ?? "operator",
                gatewayBinding,
                hello.DeviceToken,
                hello.Scopes,
                handshakeTimeout.Token);
            lock (_stateLock)
            {
                _deviceTokenConfigured = true;
                _transientBootstrapToken = null;
            }
        }

        if (hello.Protocol != _options.ProtocolVersion)
        {
            throw new OpenClawGatewayRpcException(
                "PROTOCOL_MISMATCH",
                $"Gateway negotiated protocol {hello.Protocol}; Nexus requires {_options.ProtocolVersion}.");
        }

        var (versionMatches, versionWarning) = EvaluateVersion(hello.ServerVersion);
        if (!versionMatches && _options.FailFastOnVersionMismatch)
        {
            SetState(
                GatewayConnectionState.Failed,
                versionWarning ?? "Gateway version mismatch.");
            await CloseBestEffortAsync(socket, "version mismatch");
            return;
        }

        lock (_stateLock)
        {
            _socket = socket;
            _state = GatewayConnectionState.Connected;
            _gatewayVersion = hello.ServerVersion;
            _protocolVersion = hello.Protocol;
            _advertisedMethods = new HashSet<string>(hello.Methods, StringComparer.Ordinal);
            _advertisedEvents = new HashSet<string>(hello.Events, StringComparer.Ordinal);
            _grantedScopes = new HashSet<string>(hello.Scopes, StringComparer.Ordinal);
            _lastConnectedAt = DateTimeOffset.UtcNow;
            _reconnectAttempts = 0;
            _statusMessage = versionWarning
                ?? $"Protocol v{hello.Protocol} ready; {_advertisedMethods.Count} methods advertised.";
            _pairingRequired = false;
            _pairingRequestId = null;
        }

        _logger.LogInformation(
            "OpenClaw Gateway connected using protocol {Protocol}; version {Version}; scopes {Scopes}",
            hello.Protocol,
            hello.ServerVersion ?? "unknown",
            string.Join(",", hello.Scopes));

        await ReceiveLoopAsync(socket, stoppingToken);
    }

    private async Task<string?> ReceiveChallengeAsync(ClientWebSocket socket, CancellationToken cancellationToken)
    {
        for (var attempt = 0; attempt < 5; attempt++)
        {
            var frame = await ReceiveFrameAsync(socket, cancellationToken);
            if (frame is null)
                return null;
            if (OpenClawGatewayProtocol.IsConnectChallenge(frame, out var nonce))
                return nonce;
        }

        return null;
    }

    private async Task<OpenClawGatewayHello> ReceiveHelloAsync(
        ClientWebSocket socket,
        string connectId,
        CancellationToken cancellationToken)
    {
        for (var attempt = 0; attempt < 10; attempt++)
        {
            var frame = await ReceiveFrameAsync(socket, cancellationToken);
            if (frame is null)
                throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "Gateway closed before hello-ok.");

            if (string.Equals(frame["type"]?.GetValue<string>(), "res", StringComparison.Ordinal) &&
                string.Equals(frame["id"]?.GetValue<string>(), connectId, StringComparison.Ordinal))
            {
                return OpenClawGatewayProtocol.ParseHello(frame, connectId);
            }
        }

        throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "Gateway did not return hello-ok.");
    }

    private async Task ReceiveLoopAsync(ClientWebSocket socket, CancellationToken stoppingToken)
    {
        try
        {
            while (socket.State == WebSocketState.Open && !stoppingToken.IsCancellationRequested)
            {
                var frame = await ReceiveFrameAsync(socket, stoppingToken);
                if (frame is null)
                    break;

                var type = frame["type"]?.GetValue<string>();
                if (string.Equals(type, "res", StringComparison.Ordinal))
                {
                    HandleResponse(frame);
                    continue;
                }

                if (string.Equals(type, "event", StringComparison.Ordinal))
                    HandleEvent(frame);
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Normal application shutdown.
        }
        catch (WebSocketException exception)
        {
            _logger.LogInformation("OpenClaw Gateway WebSocket closed: {Message}", exception.Message);
        }
        finally
        {
            await CloseBestEffortAsync(socket, "nexus connector shutdown");
            if (!stoppingToken.IsCancellationRequested)
            {
                IncrementReconnectAttempts();
                SetState(GatewayConnectionState.Disconnected, "Gateway connection closed.");
            }
        }
    }

    private void HandleResponse(JsonNode frame)
    {
        var requestId = frame["id"]?.GetValue<string>();
        if (string.IsNullOrWhiteSpace(requestId) || !_pending.TryRemove(requestId, out var completion))
            return;

        if (frame["ok"]?.GetValue<bool>() == true)
        {
            completion.TrySetResult(frame["payload"]?.DeepClone());
            return;
        }

        completion.TrySetException(OpenClawGatewayProtocol.CreateRpcException(frame["error"]));
    }

    private void HandleEvent(JsonNode frame)
    {
        var eventName = frame["event"]?.GetValue<string>();
        if (string.IsNullOrWhiteSpace(eventName))
            return;

        var receivedAt = DateTimeOffset.UtcNow;
        var envelope = new GatewayEventEnvelope(
            eventName,
            frame["payload"]?.DeepClone(),
            TryGetLong(frame["seq"]),
            TryGetLong(frame["stateVersion"]),
            receivedAt);

        _recentEvents.Enqueue(envelope);
        while (_recentEvents.Count > _options.EventBufferCapacity)
            _recentEvents.TryDequeue(out _);

        lock (_stateLock)
            _lastEventAt = receivedAt;
    }

    private async Task SendFrameAsync(
        ClientWebSocket socket,
        JsonNode frame,
        CancellationToken cancellationToken)
    {
        var bytes = Encoding.UTF8.GetBytes(frame.ToJsonString(JsonOptions));
        await _sendLock.WaitAsync(cancellationToken);
        try
        {
            await socket.SendAsync(
                new ArraySegment<byte>(bytes),
                WebSocketMessageType.Text,
                endOfMessage: true,
                cancellationToken);
        }
        finally
        {
            _sendLock.Release();
        }
    }

    private async Task<JsonNode?> ReceiveFrameAsync(
        ClientWebSocket socket,
        CancellationToken cancellationToken)
    {
        var buffer = new byte[16 * 1024];
        using var message = new MemoryStream();

        while (true)
        {
            var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
            if (result.MessageType == WebSocketMessageType.Close)
                return null;
            if (result.MessageType != WebSocketMessageType.Text)
                throw new OpenClawGatewayRpcException("UNSUPPORTED_FRAME", "Gateway returned a non-text frame.");

            await message.WriteAsync(buffer.AsMemory(0, result.Count), cancellationToken);
            if (message.Length > _options.MaxFrameBytes)
                throw new OpenClawGatewayRpcException("PAYLOAD_TOO_LARGE", "Gateway frame exceeded Nexus' configured receive limit.");

            if (result.EndOfMessage)
                break;
        }

        message.Position = 0;
        try
        {
            return await JsonNode.ParseAsync(message, cancellationToken: cancellationToken);
        }
        catch (JsonException exception)
        {
            throw new OpenClawGatewayRpcException("INVALID_FRAME", $"Gateway returned invalid JSON: {exception.Message}");
        }
    }

    private Uri BuildWebSocketUri()
    {
        lock (_stateLock)
            return _endpointOverride ?? BuildConfiguredWebSocketUri();
    }

    private Uri BuildConfiguredWebSocketUri()
    {
        var configuredBase = _configuration["Integrations:OpenClaw:BaseUrl"]
            ?? "http://127.0.0.1:18789";
        var builder = new UriBuilder(configuredBase)
        {
            Scheme = configuredBase.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws"
        };

        var configuredPath = string.IsNullOrWhiteSpace(_options.WebSocketPath)
            ? "/"
            : _options.WebSocketPath.Trim();
        builder.Path = configuredPath.StartsWith('/') ? configuredPath : "/" + configuredPath;
        builder.Query = string.Empty;
        builder.Fragment = string.Empty;
        return builder.Uri;
    }

    private void ConfigureTransportTrust(ClientWebSocket socket, Uri endpoint)
    {
        if (string.Equals(endpoint.Scheme, "ws", StringComparison.OrdinalIgnoreCase))
        {
            var trustedPlaintextHost = endpoint.IsLoopback ||
                string.Equals(endpoint.Host, "localhost", StringComparison.OrdinalIgnoreCase) ||
                _options.TrustedPlaintextHosts.Any(host =>
                    string.Equals(host?.Trim(), endpoint.Host, StringComparison.OrdinalIgnoreCase));
            if (!trustedPlaintextHost)
            {
                throw new OpenClawGatewayRpcException(
                    "INSECURE_GATEWAY_ENDPOINT",
                    "Plaintext OpenClaw WebSockets are allowed only for loopback or explicitly trusted internal hosts.");
            }

            return;
        }

        if (!string.Equals(endpoint.Scheme, "wss", StringComparison.OrdinalIgnoreCase))
        {
            throw new OpenClawGatewayRpcException(
                "INVALID_GATEWAY_ENDPOINT",
                "OpenClaw Gateway endpoint must use ws or wss.");
        }

        var expectedFingerprint = GetActiveTlsFingerprint();
        if (endpoint.IsLoopback && string.IsNullOrWhiteSpace(expectedFingerprint))
            return;
        if (string.IsNullOrWhiteSpace(expectedFingerprint))
        {
            throw new OpenClawGatewayRpcException(
                "TLS_PIN_REQUIRED",
                "Remote OpenClaw WSS endpoints require an explicitly confirmed SHA-256 TLS fingerprint.");
        }

        socket.Options.RemoteCertificateValidationCallback = (_, certificate, _, errors) =>
        {
            if (certificate is null || errors != System.Net.Security.SslPolicyErrors.None)
                return false;
            var actual = Convert.ToHexString(certificate.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256));
            return string.Equals(
                NormalizeFingerprint(actual),
                expectedFingerprint,
                StringComparison.Ordinal);
        };
    }

    private static string NormalizeFingerprint(string? value)
        => string.Concat((value ?? string.Empty)
            .Where(character => char.IsAsciiHexDigit(character)))
            .ToUpperInvariant();

    internal static string BuildGatewayBinding(Uri endpoint, string? tlsFingerprint)
    {
        ArgumentNullException.ThrowIfNull(endpoint);
        var normalizedEndpoint = endpoint.GetComponents(
            UriComponents.SchemeAndServer | UriComponents.Path,
            UriFormat.UriEscaped).TrimEnd('/').ToLowerInvariant();
        var material = $"{normalizedEndpoint}|{NormalizeFingerprint(tlsFingerprint)}";
        return OpenClawInvocationContextFactory.Hash(material);
    }

    private (string? Token, string? Password) ResolveCredentials()
    {
        var password = _configuration["Integrations:OpenClaw:Password"];
        var token = _configuration["Integrations:OpenClaw:Token"];
        lock (_stateLock)
        {
            if (!string.IsNullOrWhiteSpace(_transientBootstrapToken))
                token = _transientBootstrapToken;
        }
        return (
            string.IsNullOrWhiteSpace(token) ? null : token,
            string.IsNullOrWhiteSpace(password) ? null : password);
    }

    private string GetActiveTlsFingerprint()
    {
        lock (_stateLock)
            return _tlsFingerprintOverride ?? NormalizeFingerprint(_options.TlsFingerprint);
    }

    private void SignalReconnect()
    {
        try
        {
            if (_reconnectSignal.CurrentCount == 0)
                _reconnectSignal.Release();
        }
        catch (SemaphoreFullException)
        {
            // Another caller already woke the connector.
        }
    }

    private (bool Matches, string? Warning) EvaluateVersion(string? detectedVersion)
    {
        if (_requiredVersion is null)
            return (true, detectedVersion is null ? "Gateway version is unknown and unpinned." : null);

        if (detectedVersion is null)
        {
            var warning = $"Gateway did not report a version; Nexus requires '{_requiredVersion}'.";
            return (_options.FailFastOnMissingVersion ? false : true, warning);
        }

        if (!string.Equals(detectedVersion, _requiredVersion, StringComparison.OrdinalIgnoreCase))
            return (false, $"Gateway version drift: detected '{detectedVersion}', required '{_requiredVersion}'.");

        return (true, null);
    }

    private TimeSpan ComputeBackoff()
    {
        var attempts = Math.Max(0, ReconnectAttempts);
        var backoffMs = Math.Min(
            _options.ReconnectInitialDelayMs * Math.Pow(2, attempts),
            _options.ReconnectMaxDelayMs);
        var jitter = backoffMs * 0.25 * (Random.Shared.NextDouble() * 2 - 1);
        return TimeSpan.FromMilliseconds(Math.Max(100, backoffMs + jitter));
    }

    private void IncrementReconnectAttempts()
    {
        lock (_stateLock)
            _reconnectAttempts++;
    }

    private void SetState(GatewayConnectionState state, string? message = null)
    {
        lock (_stateLock)
        {
            _state = state;
            if (message is not null)
                _statusMessage = message;
        }
    }

    private void FailPending(string message)
    {
        foreach (var request in _pending.ToArray())
        {
            if (_pending.TryRemove(request.Key, out var completion))
            {
                completion.TrySetException(new OpenClawGatewayRpcException(
                    "GATEWAY_DISCONNECTED",
                    message,
                    retryable: true));
            }
        }
    }

    private static async Task CloseBestEffortAsync(ClientWebSocket socket, string description)
    {
        if (socket.State is not (WebSocketState.Open or WebSocketState.CloseReceived))
            return;

        try
        {
            await socket.CloseOutputAsync(
                WebSocketCloseStatus.NormalClosure,
                description,
                CancellationToken.None);
        }
        catch
        {
            // Connection is already gone.
        }
    }

    private static string BuildSafeErrorMessage(OpenClawGatewayRpcException exception)
    {
        var recommendation = exception.Details?["recommendedNextStep"]?.GetValue<string>();
        return string.IsNullOrWhiteSpace(recommendation)
            ? $"{exception.Code}: {exception.Message}"
            : $"{exception.Code}: {exception.Message} ({recommendation})";
    }

    private void CapturePairingFailure(OpenClawGatewayRpcException exception)
    {
        var pairingRequired = OpenClawGatewayProtocol.TryReadPairingRequest(
            exception,
            out var requestId);

        lock (_stateLock)
        {
            _pairingRequired = pairingRequired;
            _pairingRequestId = pairingRequired ? requestId : null;
        }
    }

    private static string BuildGatewayRequestId(string? correlationId)
    {
        var suffix = Guid.NewGuid().ToString("N");
        if (string.IsNullOrWhiteSpace(correlationId))
            return suffix;

        var safe = new string(correlationId
            .Where(character => char.IsAsciiLetterOrDigit(character) || character is '-' or '_' or '.')
            .Take(64)
            .ToArray());
        return string.IsNullOrWhiteSpace(safe)
            ? suffix
            : $"{safe}.{suffix}";
    }

    private static string GetClientVersion()
    {
        var version = Assembly.GetExecutingAssembly().GetName().Version;
        return version is null ? "0.1.0" : $"{version.Major}.{version.Minor}.{Math.Max(0, version.Build)}";
    }

    private static string GetPlatform()
    {
        if (OperatingSystem.IsWindows()) return "windows";
        if (OperatingSystem.IsMacOS()) return "macos";
        if (OperatingSystem.IsLinux()) return "linux";
        return "unknown";
    }

    private static long? TryGetLong(JsonNode? node)
    {
        if (node is null)
            return null;

        try
        {
            return node.GetValueKind() switch
            {
                JsonValueKind.Number => node.GetValue<long>(),
                JsonValueKind.String when long.TryParse(node.GetValue<string>(), out var value) => value,
                _ => null
            };
        }
        catch
        {
            return null;
        }
    }

}

public sealed class GatewayConnectorOptions
{
    public const string SectionName = "GatewayConnector";

    /// <summary>
    /// WebSocket path on the Gateway. OpenClaw serves the control plane at root by default.
    /// </summary>
    public string WebSocketPath { get; set; } = "/";

    public int ProtocolVersion { get; set; } = OpenClawGatewayProtocol.CurrentProtocol;
    public int HandshakeTimeoutSeconds { get; set; } = 15;
    public int RpcTimeoutSeconds { get; set; } = 30;
    public int MaxFrameBytes { get; set; } = 4 * 1024 * 1024;
    public int EventBufferCapacity { get; set; } = 500;
    public string DeviceStatePath { get; set; } = string.Empty;
    public string OperationAuditPath { get; set; } = string.Empty;
    public string DeviceFamily { get; set; } = "server";
    public string ClientId { get; set; } = "nexus";
    public string ClientMode { get; set; } = "backend";
    public string ClientDisplayName { get; set; } = "Nexus Mission Control";
    public string ClientInstanceId { get; set; } = string.Empty;
    public bool ExternalClientIdentitySupported { get; set; }
    public bool AllowReservedInternalClientIdentity { get; set; }
    public string TlsFingerprint { get; set; } = string.Empty;
    public string[] TrustedPlaintextHosts { get; set; } =
    [
        "openclaw-gateway",
        "host.docker.internal"
    ];
    public int ReconnectInitialDelayMs { get; set; } = 1000;
    public int ReconnectMaxDelayMs { get; set; } = 300_000;
    public int ReconnectMaxAttempts { get; set; }
    public bool FailFastOnVersionMismatch { get; set; } = true;
    public bool FailFastOnMissingVersion { get; set; }

    public string[] Scopes { get; set; } =
    [
        "operator.read"
    ];

    public string[] Capabilities { get; set; } =
    [
        "approvals",
        "exec-approvals",
        "session-scoped-events",
        "task-suggestions",
        "tool-events"
    ];
}
