using NBitcoin;
using Nostr.Client.Identifiers;
using Nostr.Client.Json;
using Nostr.Client.Messages;
using Nostr.Client.Utils;
using NostrServices.Client;
using ProtoBuf;
using StackExchange.Redis;

namespace NostrServices.Services;

public class RedisStore
{
    private static readonly TimeSpan DefaultExpire = TimeSpan.FromDays(90);
    private readonly IDatabase _database;
    private readonly ConnectionMultiplexer _connection;
    private readonly HttpClient _client;

    public RedisStore(ConnectionMultiplexer connection, HttpClient client)
    {
        _connection = connection;
        _client = client;
        _database = connection.GetDatabase();
    }

    public async Task<bool> StoreEvent(CompactEvent ev, TimeSpan? expiry = null)
    {
        return true;
    }

    public async Task<CompactEvent?> GetEvent(NostrIdentifier id)
    {
        try
        {
            var ev = await _client.GetAsync($"https://nostr-rs.api.v0l.io/event/{id.ToBech32()}");
            if (ev.IsSuccessStatusCode)
            {
                var obj = NostrJson.Deserialize<NostrEvent>(await ev.Content.ReadAsStringAsync());
                if (obj != default)
                {
                    return CompactEvent.FromNostrEvent(obj);
                }
            }
        }
        catch
        {
            // ignored
        }

        return default;
    }

    public async Task<bool> StoreProfile(CompactProfile meta, TimeSpan? expiry = null)
    {
        return true;
    }

    public async Task<CompactProfile?> GetProfile(string id)
    {
        try
        {
            var ev = await _client.GetAsync($"https://nostr-rs.api.v0l.io/event/0/{id}");
            if (ev.IsSuccessStatusCode)
            {
                var evo = NostrJson.Deserialize<NostrEvent>(await ev.Content.ReadAsStringAsync());
                if (evo != default)
                {
                    return CompactProfile.FromNostrEvent(evo);
                }
            }
        }
        catch
        {
            // ignored
        }

        return default;
    }

    public async Task<HashSet<Uri>> GetKnownRelays()
    {
        var ret = new HashSet<Uri>();
        await foreach (var r in _database.SetScanAsync("relays"))
        {
            if (r.HasValue)
            {
                ret.Add(new Uri(r!));
            }
        }

        return ret;
    }

    public async Task<bool> StoreRelay(RelayInfo cri)
    {
        return await _database.SetAsync(RelayKey(cri.Url), cri, DefaultExpire);
    }

    public async Task<RelayInfo?> GetRelay(Uri u)
    {
        return await _database.GetAsync<RelayInfo>(RelayKey(u));
    }

    public async Task<bool> StoreUserRelays(string pubkey, UserRelaySettings relays)
    {
        var old = await GetUserRelays(pubkey);
        if ((old?.Created ?? 0) < relays.Created)
        {
            var removed = old?.Relays.Where(a => relays.Relays.All(b => b.Url != a.Url));
            var added = old == default
                ? relays.Relays
                : relays.Relays.Where(a => old.Relays.All(b => b.Url != a.Url)).ToList();
            if (removed != default)
            {
                await Task.WhenAll(removed.Select(a =>
                    _database.SetRemoveAsync(RelayUsersKey(a.Url), Convert.FromHexString(pubkey))));
            }

            await Task.WhenAll(added.Select(a =>
                _database.SetAddAsync(RelayUsersKey(a.Url), Convert.FromHexString(pubkey))));
            await _database.SetAddAsync("relays", added.Select(a => (RedisValue)a.Url.ToString()).ToArray());

            return await _database.SetAsync(UserRelaysKey(pubkey), relays, DefaultExpire);
        }

        return false;
    }

    public async Task<UserRelaySettings?> GetUserRelays(string pubkey)
    {
        return await _database.GetAsync<UserRelaySettings>(UserRelaysKey(pubkey));
    }

    public async Task<Dictionary<Uri, long>> CountRelayUsers()
    {
        var allRelays = await GetKnownRelays();
        var tasks = allRelays.Select(a => (Url: a, Task: _database.SetLengthAsync(RelayUsersKey(a)))).ToList();
        await Task.WhenAll(tasks.Select(a => a.Task));

        return tasks.ToDictionary(a => a.Url, b => b.Task.Result);
    }

    public async Task<long> CountUserEvents(string pubkey)
    {
        return await _database.SetLengthAsync(UserEventsKey(pubkey));
    }

    public async Task StoreRelayPosition(Uri u, string ipAddress, double lat, double lon)
    {
        await _database.GeoAddAsync(RelayPositionKey(), lon, lat, $"{u}\x1{ipAddress}");
    }

    public async Task<List<RelayDistance>> FindCloseRelays(double lat, double lon, int radius = 50_000, int count = 10)
    {
        var ret = new Dictionary<Uri, RelayDistance>();
        var geoRelays =
            await _database.GeoSearchAsync(RelayPositionKey(), lon, lat, new GeoSearchCircle(radius), count * 2);
        foreach (var gr in geoRelays)
        {
            if (ret.Count == count)
            {
                break;
            }

            var id = ((string)gr.Member!).Split('\x1');
            var u = new Uri(id[0]);
            var info = await GetRelay(u);
            if (info != default && !ret.ContainsKey(u))
            {
                ret.Add(u, new()
                {
                    Distance = gr.Distance.HasValue ? (long)gr.Distance.Value : 0,
                    Relay = info,
                    IpAddress = id[1]
                });
            }
        }

        return ret.Values.ToList();
    }

    public async IAsyncEnumerable<string> EnumerateProfiles()
    {
        var servers = _connection.GetServers();
        foreach (var server in servers)
        {
            await foreach (var k in server.KeysAsync())
            {
                var stringKey = (string)k!;
                if (stringKey.StartsWith("profile:") && stringKey.Length == 64 + 8)
                {
                    yield return stringKey[8..];
                }
            }
        }
    }

    public async Task<bool> StorePubkeyStats(List<PubKeyStat> stats)
    {
        return await _database.SetAsync(UserStatsKey(), stats);
    }

    public async Task<List<PubKeyStat>?> GetPubkeyStats()
    {
        return await _database.GetAsync<List<PubKeyStat>>(UserStatsKey());
    }

    private string EventKey(NostrIdentifier id)
    {
        if (id is NostrAddressIdentifier naddr)
        {
            return $"event:{naddr.Kind}:{naddr.Pubkey}:{naddr.Identifier}";
        }

        return $"event:{id.Special}";
    }

    private static string ProfileKey(string id) => $"profile:{id}";
    private static string RelayKey(Uri relay) => $"relay:${relay}";
    private static string RelayPositionKey() => $"relays:geo";
    private static string RelayUsersKey(Uri relay) => $"relay:{relay}:users";
    private static string UserRelaysKey(string pubkey) => $"profile:{pubkey}:relays";
    private static string UserEventsKey(string pubkey) => $"profile:{pubkey}:events";
    private static string UserStatsKey() => "profile-stats";
}

[ProtoContract]
public class CompactEventTag
{
    [ProtoMember(1)]
    public string Key { get; init; } = null!;

    [ProtoMember(2)]
    public List<string> Values { get; init; } = null!;
}

[ProtoContract]
public class CompactEvent
{
    [ProtoMember(1)]
    public byte[] Id { get; init; } = null!;

    [ProtoMember(2)]
    public byte[] PubKey { get; init; } = null!;

    [ProtoMember(3)]
    public long Created { get; init; }

    [ProtoMember(4)]
    public string Content { get; init; } = null!;

    [ProtoMember(5)]
    public List<CompactEventTag> Tags { get; init; } = new();

    [ProtoMember(6)]
    public byte[] Sig { get; init; } = null!;

    [ProtoMember(7)]
    public long Kind { get; init; }

    public static CompactEvent FromNostrEvent(NostrEvent ev)
    {
        return new CompactEvent
        {
            Id = Convert.FromHexString(ev.Id!),
            PubKey = Convert.FromHexString(ev.Pubkey!),
            Kind = (long)ev.Kind,
            Created = ev.CreatedAt!.Value.ToUnixTimestamp(),
            Content = ev.Content!,
            Tags = ev.Tags!.Select(a => new CompactEventTag
            {
                Key = a.TagIdentifier,
                Values = a.AdditionalData.ToList()
            }).ToList(),
            Sig = Convert.FromHexString(ev.Sig!)
        };
    }

    public NostrEvent ToNostrEvent()
    {
        return new NostrEvent()
        {
            Id = Id.ToHex(),
            Pubkey = PubKey.ToHex(),
            CreatedAt = DateTimeOffset.FromUnixTimeSeconds(Created).UtcDateTime,
            Content = Content,
            Tags = new(Tags.Select(a => new NostrEventTag(a.Key, a.Values.ToArray()))),
            Sig = Sig.ToHex()
        };
    }
}

[ProtoContract]
public class RelayDistance
{
    [ProtoMember(1)]
    public long Distance { get; init; }

    [ProtoMember(2)]
    public string IpAddress { get; init; } = null!;

    [ProtoMember(3)]
    public RelayInfo Relay { get; init; } = null!;
}

[ProtoContract]
public class RelayInfo
{
    [ProtoMember(1)]
    public Uri Url { get; init; } = null!;

    [ProtoMember(2)]
    public long FirstSeen { get; init; }

    [ProtoMember(3)]
    public long LastSeen { get; init; }

    [ProtoMember(4)]
    public long Users { get; init; }

    [ProtoMember(5)]
    public List<Position> Positions { get; init; } = new();

    [ProtoMember(6)]
    public bool? IsPaid { get; init; }

    [ProtoMember(7)]
    public bool? IsWriteRestricted { get; init; }

    [ProtoMember(8)]
    public string? Description { get; init; }
}

[ProtoContract]
public class Position
{
    [ProtoMember(1)]
    public double Lat { get; init; }

    [ProtoMember(2)]
    public double Lon { get; init; }

    [ProtoMember(3)]
    public string? Country { get; init; }

    [ProtoMember(4)]
    public string? City { get; init; }

    [ProtoMember(6)]
    public string IpAddress { get; init; } = null!;
}

[ProtoContract]
public class RelayEndpoint
{
    [ProtoMember(1)]
    public Uri Relay { get; init; } = null!;

    [ProtoMember(2)]
    public string IpAddress { get; init; } = null!;
}

[ProtoContract]
public class UserRelaySettings
{
    [ProtoMember(1)]
    public long Created { get; init; }

    [ProtoMember(2)]
    public List<RelaySetting> Relays { get; init; } = new();
}

[ProtoContract]
public class RelaySetting
{
    [ProtoMember(1)]
    public Uri Url { get; init; } = null!;

    [ProtoMember(2)]
    public bool Read { get; init; }

    [ProtoMember(3)]
    public bool Write { get; init; }
}