using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace DragonISO.App.Services;

// GET /ws â€” upgrades to WebSocket and pushes participant-list snapshots
// at 4Hz with diffing (no push when nothing changed). Lets controllers
// stay live-synced without polling /participants.
//
// Lifecycle:
//   â€¢ Server's accept loop upgrades the request and hands the socket here.
//   â€¢ HandleWebSocketAsync owns the connection until the client closes.
//   â€¢ The Start() method wires a 4Hz DispatcherTimer that calls
//     PushSnapshotIfChangedAsync to fan out to every connected client.
public sealed partial class ControlSurfaceServer
{
    /// <summary>
    /// Owns a single client connection until it closes. Sends an immediate
    /// snapshot on connect (so the client doesn't have to wait up to 250ms
    /// for the next push tick), then sits in a receive loop draining any
    /// incoming text â€” we ignore clientâ†’server messages for v1 since all
    /// commands are REST. The receive loop is the canonical way to detect
    /// graceful close: when WebSocket.ReceiveAsync returns CloseReceived,
    /// we close back and remove the client.
    /// </summary>
    private async Task HandleWebSocketAsync(WebSocket ws)
    {
        var clientId = Guid.NewGuid();
        _clients[clientId] = ws;
        _logger?.LogInformation("WebSocket client {Id} connected.", clientId);

        try
        {
            // Initial snapshot â€” fetch synchronously on the UI thread so the
            // ObservableCollection isn't enumerated cross-thread.
            await SendAsync(ws, await GetSnapshotJsonAsync());

            var buf = new byte[1024];
            while (ws.State == WebSocketState.Open)
            {
                var result = await ws.ReceiveAsync(new ArraySegment<byte>(buf), CancellationToken.None);
                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None);
                    break;
                }
                // Ignore any client-sent messages for now; future bidirectional
                // commands could route through here.
            }
        }
        catch (WebSocketException) { /* client crashed; drop */ }
        catch (ObjectDisposedException) { /* Stop() aborted us */ }
        catch (OperationCanceledException) { /* server shutting down */ }
        finally
        {
            _clients.TryRemove(clientId, out _);
            // Don't double-dispose: Stop() already disposed the WebSocket if
            // it's tearing us down. Aborting an already-disposed socket is a
            // no-op throw which we catch + ignore.
            try { ws.Dispose(); } catch { /* defensive */ }
            _logger?.LogInformation("WebSocket client {Id} disconnected.", clientId);
        }
    }

    /// <summary>
    /// Dispatcher-tick handler. Reads the current participants snapshot,
    /// and if it differs from what we last pushed, broadcasts the new
    /// JSON to every connected client. Diffing on the JSON string is
    /// cheap and saves wire bytes when nothing's actually changing â€”
    /// typical operator workflow has long periods of no state churn
    /// between meetings.
    /// </summary>
    private async Task PushSnapshotIfChangedAsync()
    {
        if (_clients.IsEmpty) return;

        string snapshot;
        try { snapshot = await GetSnapshotJsonAsync(); }
        catch { return; }

        if (snapshot == _lastPushedSnapshot) return;
        _lastPushedSnapshot = snapshot;

        var bytes = Encoding.UTF8.GetBytes(snapshot);
        foreach (var (id, ws) in _clients.ToArray())
        {
            if (ws.State != WebSocketState.Open)
            {
                _clients.TryRemove(id, out _);
                continue;
            }
            try
            {
                await ws.SendAsync(
                    new ArraySegment<byte>(bytes),
                    WebSocketMessageType.Text,
                    endOfMessage: true,
                    CancellationToken.None);
            }
            catch
            {
                _clients.TryRemove(id, out _);
                try { ws.Dispose(); } catch { /* defensive */ }
            }
        }
    }

    private static async Task SendAsync(WebSocket ws, string text)
    {
        var bytes = Encoding.UTF8.GetBytes(text);
        await ws.SendAsync(
            new ArraySegment<byte>(bytes),
            WebSocketMessageType.Text,
            endOfMessage: true,
            CancellationToken.None);
    }

    /// <summary>
    /// Build the same payload as <c>GET /participants</c> but as a JSON
    /// string for direct WebSocket Send. Reads the ObservableCollection
    /// via the UI dispatcher because WPF's ObservableCollection isn't
    /// thread-safe to enumerate from a non-UI thread.
    /// </summary>
    private async Task<string> GetSnapshotJsonAsync()
    {
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        var participants = dispatcher is null
            ? Array.Empty<object>()
            : await dispatcher.InvokeAsync(() =>
                {
                    var vm = _viewModel();
                    if (vm is null) return Array.Empty<object>();
                    return vm.Participants.Select(p => (object)new
                    {
                        id = p.Id,
                        displayName = p.DisplayName,
                        isOnline = p.IsOnline,
                        isEnabled = p.IsEnabled,
                        customName = string.IsNullOrEmpty(p.CustomName) ? null : p.CustomName,
                        stateLabel = p.StateLabel,
                    }).ToArray();
                });
        return JsonSerializer.Serialize(new { type = "participants", participants }, JsonOpts);
    }
}
