using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;

namespace BTCPayServer.Services.PaymentRequests
{
    public class PaymentRequestRepository
    {
        private readonly ApplicationDbContextFactory _ContextFactory;
        private readonly InvoiceRepository _InvoiceRepository;

        public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
        {
            _ContextFactory = contextFactory;
            _InvoiceRepository = invoiceRepository;
        }


        public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
        {
            using (var context = _ContextFactory.CreateContext())
            {
                if (string.IsNullOrEmpty(entity.Id))
                {
                    await context.PaymentRequests.AddAsync(entity);
                }
                else
                {
                    context.PaymentRequests.Update(entity);
                }

                await context.SaveChangesAsync();
                return entity;
            }
        }

        public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrEmpty(id))
            {
                return null;
            }

            using (var context = _ContextFactory.CreateContext())
            {
                return await context.PaymentRequests.Include(x => x.StoreData)
                    .Where(data =>
                        string.IsNullOrEmpty(userId) ||
                        (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)))
                    .SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
            }
        }

        public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
        {
            if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
            {
                return false;
            }
            using (var context = _ContextFactory.CreateContext())
            {
                return await context.PaymentRequests.Include(x => x.StoreData)
                    .AnyAsync(data =>
                        data.Id == paymentRequestId &&
                        (data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
            }
        }
        
        public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
        {
            using (var context = _ContextFactory.CreateContext())
            {
                var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
                if (invoiceData == null)
                    return;
                invoiceData.Status = status;
                await context.SaveChangesAsync(cancellationToken);
            }
        }

        public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
        {
            using (var context = _ContextFactory.CreateContext())
            {
                var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
                if (!string.IsNullOrEmpty(query.StoreId))
                {
                    queryable = queryable.Where(data =>
                       data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
                }

                if (query.Status != null && query.Status.Any())
                {
                    queryable = queryable.Where(data =>
                        query.Status.Contains(data.Status));
                }

                if (!string.IsNullOrEmpty(query.UserId))
                {
                    queryable = queryable.Where(i =>
                        i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
                }

                var total = await queryable.CountAsync(cancellationToken);
                
                if (query.Skip.HasValue)
                {
                    queryable = queryable.Skip(query.Skip.Value);
                }

                if (query.Count.HasValue)
                {
                    queryable = queryable.Take(query.Count.Value);
                }

                return (total, await queryable.ToArrayAsync(cancellationToken));
            }
        }

        public async Task<bool> RemovePaymentRequest(string id, string userId)
        {
            using (var context = _ContextFactory.CreateContext())
            {
                var canDelete = !EnumerableExtensions.Any((await GetInvoicesForPaymentRequest(id)));
                if (!canDelete) return false;
                var pr = await FindPaymentRequest(id, userId);
                if (pr == null)
                {
                    return false;
                }

                context.PaymentRequests.Remove(pr);
                await context.SaveChangesAsync();

                return true;
            }
        }

        public async Task<InvoiceEntity[]> GetInvoicesForPaymentRequest(string paymentRequestId,
            InvoiceQuery invoiceQuery = null)
        {
            if (invoiceQuery == null)
            {
                invoiceQuery = new InvoiceQuery();
            }

            invoiceQuery.OrderId = new[] {GetOrderIdForPaymentRequest(paymentRequestId)};
            return await _InvoiceRepository.GetInvoices(invoiceQuery);
        }

        public static string GetOrderIdForPaymentRequest(string paymentRequestId)
        {
            return $"PAY_REQUEST_{paymentRequestId}";
        }

        public static string GetPaymentRequestIdFromOrderId(string invoiceOrderId)
        {
            if (string.IsNullOrEmpty(invoiceOrderId) ||
                !invoiceOrderId.StartsWith("PAY_REQUEST_", StringComparison.InvariantCulture))
            {
                return null;
            }

            return invoiceOrderId.Replace("PAY_REQUEST_", "", StringComparison.InvariantCulture);
        }

        public static string GetInternalTag(string id)
        {
            return $"PAYREQ#{id}";
        }
        public static string[] GetPaymentIdsFromInternalTags(InvoiceEntity invoiceEntity)
        {
            return invoiceEntity.GetInternalTags("PAYREQ#");
        }
    }

    public class PaymentRequestUpdated
    {
        public string PaymentRequestId { get; set; }
        public PaymentRequestData Data { get; set; }
    }

    public class PaymentRequestQuery
    {
        public string StoreId { get; set; }
        
        public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
        public string UserId { get; set; }
        public int? Skip { get; set; }
        public int? Count { get; set; }
    }

    public class PaymentRequestData
    {
        public string Id { get; set; }
        public string StoreDataId { get; set; }

        public StoreData StoreData { get; set; }

        public PaymentRequestStatus Status { get; set; }

        public byte[] Blob { get; set; }

        public PaymentRequestBlob GetBlob()
        {
            var result = Blob == null
                ? new PaymentRequestBlob()
                : JObject.Parse(ZipUtils.Unzip(Blob)).ToObject<PaymentRequestBlob>();
            return result;
        }

        public bool SetBlob(PaymentRequestBlob blob)
        {
            var original = new Serializer(Network.Main).ToString(GetBlob());
            var newBlob = new Serializer(Network.Main).ToString(blob);
            if (original == newBlob)
                return false;
            Blob = ZipUtils.Zip(newBlob);
            return true;
        }

        public class PaymentRequestBlob
        {
            public decimal Amount { get; set; }
            public string Currency { get; set; }

            public DateTime? ExpiryDate { get; set; }

            public string Title { get; set; }
            public string Description { get; set; }
            public string Email { get; set; }

            public string EmbeddedCSS { get; set; }
            public string CustomCSSLink { get; set; }
            public bool AllowCustomPaymentAmounts { get; set; }
        }

        public enum PaymentRequestStatus
        {
            Pending = 0,
            Completed = 1,
            Expired = 2
        }
    }
}
