using System.Net.WebSockets;
using System.Reflection;
using Nostr.Client.Client;
using Nostr.Client.Communicator;
using Nostr.Client.Requests;
using Websocket.Client.Models;

namespace NostrServices.Services;

public class NostrListener : IDisposable
{
    private readonly NostrMultiWebsocketClient _client;
    private readonly ILogger<NostrListener> _logger;

    private readonly Dictionary<string, NostrFilter> _subscriptionToFilter = new();

    public NostrListener(NostrMultiWebsocketClient client, ILogger<NostrListener> logger)
    {
        _client = client;
        _logger = logger;
    }

    public NostrClientStreams Streams => _client.Streams;

    public void Dispose()
    {
        _client.Dispose();
    }

    public void RegisterFilter(string subscription, NostrFilter filter)
    {
        _subscriptionToFilter[subscription] = filter;
        _client.Send(new NostrRequest(subscription, filter));
    }

    private void Stop()
    {
        foreach (var nostrWebsocketClient in _client.Clients)
        {
            _ = nostrWebsocketClient.Communicator.Stop(WebSocketCloseStatus.NormalClosure, string.Empty);
        }
    }

    private void AddCommunicators(List<Uri> relays)
    {
        var missing = relays.Where(a => _client.FindClient(ClientName(a)) == null);
        var newComm = missing.Select(CreateCommunicator);
        foreach (var comm in newComm)
        {
            _client.RegisterCommunicator(comm);
            comm.Start();
            var client = _client.FindClient(comm.Name);
            if (client != default)
            {
                foreach (var (k, v) in _subscriptionToFilter)
                {
                    client.Send(new NostrRequest(k, v));
                }
            }
        }
    }

    private INostrCommunicator CreateCommunicator(Uri uri)
    {
        var comm = new NostrWebsocketCommunicator(uri, () =>
        {
            var client = new ClientWebSocket();
            client.Options.SetRequestHeader("Origin", "https://nostr-api.v0l.io/swagger");
            client.Options.SetRequestHeader("User-Agent", $"v0l/NostrServices ({Assembly.GetExecutingAssembly().GetName().Version})");
            return client;
        });

        comm.Name = ClientName(uri);
        comm.ReconnectTimeout = null; //TimeSpan.FromSeconds(30);
        comm.ErrorReconnectTimeout = TimeSpan.FromSeconds(60);

        comm.ReconnectionHappened.Subscribe(info => OnCommunicatorReconnection(info, comm.Name));
        comm.DisconnectionHappened.Subscribe(info =>
            _logger.LogWarning("[{relay}] Disconnected, type: {type}, reason: {reason}", comm.Name, info.Type, info.CloseStatus));

        return comm;
    }

    private string ClientName(Uri u) => $"{u.Host}{u.AbsolutePath}";

    private void OnCommunicatorReconnection(ReconnectionInfo info, string communicatorName)
    {
        try
        {
            _logger.LogInformation("[{relay}] Reconnected, sending Nostr filters ({filterCount})", communicatorName,
                _subscriptionToFilter.Count);

            var client = _client.FindClient(communicatorName);
            if (client == null)
            {
                _logger.LogWarning("[{relay}] Cannot find client", communicatorName);
                return;
            }

            foreach (var (sub, filter) in _subscriptionToFilter)
            {
                client.Send(new NostrRequest(sub, filter));
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "[{relay}] Failed to process reconnection, error: {error}", communicatorName, e.Message);
        }
    }

    public class NostrListenerLifetime : BackgroundService
    {
        private readonly NostrListener _nostrListener;
        private readonly IServiceScopeFactory _scopeFactory;

        public NostrListenerLifetime(NostrListener nostrListener, IServiceScopeFactory scopeFactory)
        {
            _nostrListener = nostrListener;
            _scopeFactory = scopeFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                using var scope = _scopeFactory.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<RedisStore>();

                var relays = await db.CountRelayUsers();
                var topRelays = relays.OrderByDescending(a => a.Value).Take(20).Select(a => a.Key).ToList();
                _nostrListener.AddCommunicators(topRelays);

                await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
            }
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            await base.StopAsync(cancellationToken);
            _nostrListener.Stop();
        }
    }
}
