using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using MaxMind.GeoIP2;
using NBitcoin;
using Newtonsoft.Json;

namespace NostrServices.Services;

public class RelayScraperService : BackgroundService
{
    private readonly RedisStore _redisStore;
    private readonly IGeoIP2DatabaseReader _geoIp2Database;
    private readonly ILogger<RelayScraperService> _logger;
    private readonly HttpClient _client;

    public RelayScraperService(IGeoIP2DatabaseReader geoIp2Database, ILogger<RelayScraperService> logger, HttpClient client,
        RedisStore redisStore)
    {
        _geoIp2Database = geoIp2Database;
        _logger = logger;
        _client = client;
        _redisStore = redisStore;
        _client.Timeout = TimeSpan.FromSeconds(10);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var sw = Stopwatch.StartNew();
                await UpdateRelays(stoppingToken);
                sw.Stop();

                _logger.LogInformation("Caching relay locations took {n:#,##0.00} seconds", sw.Elapsed.TotalSeconds);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to cache relay-geo info {msg}", ex.Message);
            }

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

    private async Task UpdateRelays(CancellationToken cts)
    {
        const int batchSize = 20;

        var relays = await _redisStore.CountRelayUsers();
        foreach (var batch in relays.Select((v, i) => (Batch: i / batchSize, Value: v)).GroupBy(a => a.Batch))
        {
            await Task.WhenAll(batch.Select(a => ScrapeRelay(a.Value, cts)));
        }
    }

    private async Task ScrapeRelay(KeyValuePair<Uri, long> relay, CancellationToken cts)
    {
        try
        {
            _logger.LogInformation("Scraping relay: {url}", relay);
            if (relay.Key.AbsolutePath.Contains("%20wss") || relay.Key.AbsolutePath.StartsWith("/npub1"))
            {
                return;
            }

            // only store relays with info doc (lazy up status)
            var info = await GetRelayInfo(relay.Key);
            if (info == default) return;

            var ret = new RelayInfo()
            {
                Url = relay.Key,
                Users = relay.Value,
                IsPaid = info?.Limitations?.PaymentRequired,
                IsWriteRestricted = info?.Limitations?.RestrictedWrites,
                Description = info?.Description,
                LastSeen = DateTime.UtcNow.ToUnixTimestamp(),
                FirstSeen = DateTime.UtcNow.ToUnixTimestamp()
            };

            var ips = await Dns.GetHostAddressesAsync(relay.Key.DnsSafeHost, cts);
            foreach (var ip in ips)
            {
                if (!ip.IsRoutable(false)) continue;

                if (_geoIp2Database.TryCity(ip, out var city) && city!.Location.Longitude.HasValue && city.Location.Latitude.HasValue)
                {
                    ret.Positions.Add(new()
                    {
                        Lat = city.Location.Latitude.Value,
                        Lon = city.Location.Longitude.Value,
                        Country = city.Country.Name,
                        City = city.City.Name,
                        IpAddress = ip.ToString()
                    });
                }
            }

            await _redisStore.StoreRelay(ret);
            foreach (var pos in ret.Positions)
            {
                await _redisStore.StoreRelayPosition(ret.Url, pos.IpAddress, pos.Lat, pos.Lon);
            }
        }
        catch (Exception ex)
        {
            // ignored
            _logger.LogWarning(ex, "Failed to load relay geo info {relay} {message}", relay, ex.Message);
        }
    }

    private async Task<RelayDoc?> GetRelayInfo(Uri r)
    {
        try
        {
            var ub = new UriBuilder()
            {
                Scheme = r.Scheme == "wss" ? "https" : "http",
                Host = r.Host,
                Path = r.AbsolutePath,
                Port = r.Port,
            };

            var rsp = await _client.SendAsync(new(HttpMethod.Get, ub.Uri)
            {
                Headers =
                {
                    Accept = {new MediaTypeWithQualityHeaderValue("application/nostr+json")}
                }
            });

            if (rsp.IsSuccessStatusCode)
            {
                var json = await rsp.Content.ReadAsStringAsync();
                var doc = JsonConvert.DeserializeObject<RelayDoc>(json);
                return doc;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to load relay doc for {url} {message}", r, ex.Message);
        }

        return default;
    }
}

public class RelayDoc
{
    [JsonProperty("description")]
    public string? Description { get; init; }

    [JsonProperty("limitation")]
    public RelayDocLimitation? Limitations { get; init; }
}

public class RelayDocLimitation
{
    [JsonProperty("payment_required")]
    public bool? PaymentRequired { get; init; }

    [JsonProperty("restricted_writes")]
    public bool? RestrictedWrites { get; init; }
}
