using System.Threading.Tasks.Dataflow;
using FASTER.core;
using Microsoft.Extensions.Caching.Memory;
using Nostr.Client.Json;
using Nostr.Client.Keys;
using Nostr.Client.Messages;
using NostrRelay;
using NostrServices.Client;
using EventSession =
    FASTER.core.ClientSession<byte[], byte[], byte[], byte[], FASTER.core.Empty, FASTER.core.SimpleFunctions<byte[], byte[]>>;


namespace PayForReactions;

public class ReactionQueue
{
    public readonly BufferBlock<NostrEvent> Queue = new();
}

public class ReactionQueueWorker : BackgroundService
{
    private readonly ILogger<ReactionQueueWorker> _logger;
    private readonly ReactionQueue _queue;
    private readonly IMemoryCache _cache;
    private readonly Config _config;
    private readonly Lnurl _lnurl;
    private readonly AlbyApi _albyApi;
    private readonly NostrServicesClient _nostrServices;
    private readonly EventSession _session;

    public ReactionQueueWorker(ILogger<ReactionQueueWorker> logger, ReactionQueue queue, IMemoryCache cache, Config config, Lnurl lnurl,
        AlbyApi albyApi, NostrServicesClient nostrServices, NostrStore store)
    {
        _logger = logger;
        _queue = queue;
        _cache = cache;
        _config = config;
        _lnurl = lnurl;
        _albyApi = albyApi;
        _nostrServices = nostrServices;
        _session = store.MainStore.For(new SimpleFunctions<byte[], byte[]>()).NewSession<SimpleFunctions<byte[], byte[]>>();
        ;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var target = NostrPublicKey.FromBech32(_config.Target).Hex;
            var ev = await _queue.Queue.ReceiveAsync(stoppingToken);
            if (ev.Pubkey == target)
            {
                var id = Convert.FromHexString(ev.Id!);
                var obj = NostrBuf.Encode(ev);
                (await _session.UpsertAsync(ref id, ref obj, token: stoppingToken)).Complete();
                _logger.LogInformation("Got targets event, saving {ev}", NostrJson.Serialize(ev));
                continue;
            }

            var amount = MapKindToAmount(ev.Kind);
            if (amount == default) continue;

            _logger.LogInformation("Got reaction {json}", NostrJson.Serialize(ev));
            var eTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "e");
            var pTag = ev.Tags?.FirstOrDefault(a => a.TagIdentifier == "p" && a.AdditionalData[0] == target);
            if (pTag == default || eTag == default)
            {
                _logger.LogInformation("blocked: must be a reaction to {target}", target);
                continue;
            }

            var idBytes = Convert.FromHexString(eTag.AdditionalData[0]);
            var refEventResult = (await _session.ReadAsync(ref idBytes)).Complete();
            if (refEventResult.status.NotFound)
            {
                _logger.LogInformation("blocked: parent event not found");
                continue;
            }

            var seenId = Convert.FromHexString(ev.Id!);
            var seenIdResult = (await _session.ReadAsync(ref seenId)).Complete();
            if (seenIdResult.status.Found)
            {
                _logger.LogInformation("blocked: already zapped this one, how dare you!");
                continue;
            }

            var refEvent = NostrBuf.Decode(refEventResult.output);
            if (refEvent == default)
            {
                _logger.LogInformation("blocked: parent event not found");
                continue;
            }

            if (refEvent.Pubkey != target)
            {
                _logger.LogInformation("blocked: parent event not posted by {target}", target);
                continue;
            }

            var sender = ev.Pubkey!;
            var senderProfile = await _nostrServices.Profile(NostrPublicKey.FromHex(sender).Bech32);
            if (senderProfile == default)
            {
                _logger.LogInformation("blocked: couldn't find your profile anon!");
                continue;
            }

            var parsedTarget = Lnurl.ParseLnUrl(senderProfile.Lud16 ?? "");
            if (parsedTarget == default)
            {
                _logger.LogInformation(
                    "blocked: so sad... couldn't send a zap because you don't have a lightning address in your profile {name}!",
                    senderProfile.Name);

                continue;
            }

            _logger.LogInformation("Starting payment for {name} - {addr}", senderProfile.Name, senderProfile.Lud16);
            var svc = await _lnurl.LoadAsync(parsedTarget.ToString());
            if (svc == default)
            {
                _logger.LogInformation("blocked: wallet is down, no zap for you!");
                continue;
            }

            var keyEventWalletSeen = $"zapaped:{refEvent.Id}:{parsedTarget}";
            var eventWalletZapTry = _cache.Get<int>(keyEventWalletSeen);
            if (eventWalletZapTry > 0)
            {
                _logger.LogInformation("blocked: hey i already zapped you! (count={count})", eventWalletZapTry);
                continue;
            }

            _cache.Set(keyEventWalletSeen, ++eventWalletZapTry);

            var key = NostrPrivateKey.FromBech32(_config.PrivateKey);
            var myPubkey = key.DerivePublicKey().Hex;
            var zap = new NostrEvent
            {
                Kind = NostrKind.ZapRequest,
                Content = "Thanks for your interaction!",
                CreatedAt = DateTime.UtcNow,
                Pubkey = myPubkey,
                Tags = new NostrEventTags(
                    new NostrEventTag("e", ev.Id!),
                    new NostrEventTag("p", sender),
                    new NostrEventTag("relays", "wss://relay.snort.social", "wss://nos.lol", "wss://relay.damus.io"),
                    new NostrEventTag("amount", (amount.Value * 1000).ToString())
                )
            };

            var zapSigned = zap.Sign(key);
            try
            {
                var invoice = await _lnurl.GetInvoiceAsync(svc, amount.Value, "Thanks for your interaction!", zapSigned);
                if (string.IsNullOrEmpty(invoice.Pr))
                {
                    _logger.LogInformation("blocked: failed to get invoice from {target}", parsedTarget);
                }

                _logger.LogInformation("Paying invoice {pr}", invoice.Pr);
                if (!await _albyApi.PayInvoice(invoice.Pr))
                {
                    _logger.LogInformation("blocked: failed to pay invoice!");
                }

                var seenEvent = NostrBuf.Encode(ev);
                (await _session.UpsertAsync(ref seenId, ref seenEvent, token: stoppingToken)).Complete();

                _logger.LogInformation("Zapped {name}!", senderProfile.Name);
            }
            catch (Exception e)
            {
                _logger.LogError(e.ToString());
            }
        }
    }


    private int? MapKindToAmount(NostrKind k)
    {
        switch (k)
        {
            case NostrKind.Reaction: return 50;
            case NostrKind.GenericRepost: return 100;
        }

        return default;
    }
}
