using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Microsoft.Extensions.Logging;
using StaticStore = Bit.Core.Models.StaticStore;
using TaxRate = Bit.Core.Entities.TaxRate;

namespace Bit.Core.Services
{
    public class StripePaymentService : IPaymentService
    {
        private const string PremiumPlanId = "premium-annually";
        private const string PremiumPlanAppleIapId = "premium-annually-appleiap";
        private const decimal PremiumPlanAppleIapPrice = 14.99M;
        private const string StoragePlanId = "storage-gb-annually";

        private readonly ITransactionRepository _transactionRepository;
        private readonly IUserRepository _userRepository;
        private readonly IAppleIapService _appleIapService;
        private readonly ILogger<StripePaymentService> _logger;
        private readonly Braintree.IBraintreeGateway _btGateway;
        private readonly ITaxRateRepository _taxRateRepository;
        private readonly IStripeAdapter _stripeAdapter;

        public StripePaymentService(
            ITransactionRepository transactionRepository,
            IUserRepository userRepository,
            IAppleIapService appleIapService,
            ILogger<StripePaymentService> logger,
            ITaxRateRepository taxRateRepository,
            IStripeAdapter stripeAdapter,
            Braintree.IBraintreeGateway braintreeGateway)
        {
            _transactionRepository = transactionRepository;
            _userRepository = userRepository;
            _appleIapService = appleIapService;
            _logger = logger;
            _taxRateRepository = taxRateRepository;
            _stripeAdapter = stripeAdapter;
            _btGateway = braintreeGateway;
        }

        public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
            string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
            int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
        {
            Braintree.Customer braintreeCustomer = null;
            string stipeCustomerSourceToken = null;
            string stipeCustomerPaymentMethodId = null;
            var stripeCustomerMetadata = new Dictionary<string, string>();
            var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
                paymentMethodType == PaymentMethodType.BankAccount;

            if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
            {
                if (paymentToken.StartsWith("pm_"))
                {
                    stipeCustomerPaymentMethodId = paymentToken;
                }
                else
                {
                    stipeCustomerSourceToken = paymentToken;
                }
            }
            else if (paymentMethodType == PaymentMethodType.PayPal)
            {
                var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
                var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
                {
                    PaymentMethodNonce = paymentToken,
                    Email = org.BillingEmail,
                    Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix,
                    CustomFields = new Dictionary<string, string>
                    {
                        [org.BraintreeIdField()] = org.Id.ToString()
                    }
                });

                if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
                {
                    throw new GatewayException("Failed to create PayPal customer record.");
                }

                braintreeCustomer = customerResult.Target;
                stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
            }
            else
            {
                throw new GatewayException("Payment method is not supported at this time.");
            }

            if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
            {
                var taxRateSearch = new TaxRate
                {
                    Country = taxInfo.BillingAddressCountry,
                    PostalCode = taxInfo.BillingAddressPostalCode
                };
                var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);

                // should only be one tax rate per country/zip combo
                var taxRate = taxRates.FirstOrDefault();
                if (taxRate != null)
                {
                    taxInfo.StripeTaxRateId = taxRate.Id;
                }
            }

            var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);

            Stripe.Customer customer = null;
            Stripe.Subscription subscription;
            try
            {
                customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
                {
                    Description = org.BusinessName,
                    Email = org.BillingEmail,
                    Source = stipeCustomerSourceToken,
                    PaymentMethod = stipeCustomerPaymentMethodId,
                    Metadata = stripeCustomerMetadata,
                    InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
                    {
                        DefaultPaymentMethod = stipeCustomerPaymentMethodId
                    },
                    Address = new Stripe.AddressOptions
                    {
                        Country = taxInfo.BillingAddressCountry,
                        PostalCode = taxInfo.BillingAddressPostalCode,
                        // Line1 is required in Stripe's API, suggestion in Docs is to use Business Name intead.
                        Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
                        Line2 = taxInfo.BillingAddressLine2,
                        City = taxInfo.BillingAddressCity,
                        State = taxInfo.BillingAddressState,
                    },
                    TaxIdData = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions>
                    {
                        new Stripe.CustomerTaxIdDataOptions
                        {
                            Type = taxInfo.TaxIdType,
                            Value = taxInfo.TaxIdNumber,
                        },
                    },
                });
                subCreateOptions.AddExpand("latest_invoice.payment_intent");
                subCreateOptions.Customer = customer.Id;
                subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
                if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
                {
                    if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
                    {
                        await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
                        throw new GatewayException("Payment method was declined.");
                    }
                }
            }
            catch (Exception e)
            {
                if (customer != null)
                {
                    await _stripeAdapter.CustomerDeleteAsync(customer.Id);
                }
                if (braintreeCustomer != null)
                {
                    await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
                }
                throw;
            }

            org.Gateway = GatewayType.Stripe;
            org.GatewayCustomerId = customer.Id;
            org.GatewaySubscriptionId = subscription.Id;

            if (subscription.Status == "incomplete" &&
                subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
            {
                org.Enabled = false;
                return subscription.LatestInvoice.PaymentIntent.ClientSecret;
            }
            else
            {
                org.Enabled = true;
                org.ExpirationDate = subscription.CurrentPeriodEnd;
                return null;
            }
        }

        private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship)
        {
            var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType);
            var sponsoredPlan = sponsorship != null ?
                Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) :
                null;
            var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship);

            await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow);

            var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
            org.ExpirationDate = sub.CurrentPeriodEnd;

        }

        public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) =>
            ChangeOrganizationSponsorship(org, sponsorship, true);

        public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
            ChangeOrganizationSponsorship(org, sponsorship, false);

        public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
            short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
        {
            if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
            {
                throw new BadRequestException("Organization already has a subscription.");
            }

            var customerOptions = new Stripe.CustomerGetOptions();
            customerOptions.AddExpand("default_source");
            customerOptions.AddExpand("invoice_settings.default_payment_method");
            var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
            if (customer == null)
            {
                throw new GatewayException("Could not find customer payment profile.");
            }

            if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
            {
                var taxRateSearch = new TaxRate
                {
                    Country = taxInfo.BillingAddressCountry,
                    PostalCode = taxInfo.BillingAddressPostalCode
                };
                var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);

                // should only be one tax rate per country/zip combo
                var taxRate = taxRates.FirstOrDefault();
                if (taxRate != null)
                {
                    taxInfo.StripeTaxRateId = taxRate.Id;
                }
            }

            var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
            var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);

            var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
                stripePaymentMethod, paymentMethodType, subCreateOptions, null);
            org.GatewaySubscriptionId = subscription.Id;

            if (subscription.Status == "incomplete" &&
                subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
            {
                org.Enabled = false;
                return subscription.LatestInvoice.PaymentIntent.ClientSecret;
            }
            else
            {
                org.Enabled = true;
                org.ExpirationDate = subscription.CurrentPeriodEnd;
                return null;
            }
        }

        private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
                Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
        {
            var stripePaymentMethod = false;
            var paymentMethodType = PaymentMethodType.Credit;
            var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
            if (hasBtCustomerId)
            {
                paymentMethodType = PaymentMethodType.PayPal;
            }
            else
            {
                if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card")
                {
                    paymentMethodType = PaymentMethodType.Card;
                    stripePaymentMethod = true;
                }
                else if (customer.DefaultSource != null)
                {
                    if (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.SourceCard)
                    {
                        paymentMethodType = PaymentMethodType.Card;
                        stripePaymentMethod = true;
                    }
                    else if (customer.DefaultSource is Stripe.BankAccount || customer.DefaultSource is Stripe.SourceAchDebit)
                    {
                        paymentMethodType = PaymentMethodType.BankAccount;
                        stripePaymentMethod = true;
                    }
                }
                else
                {
                    var paymentMethod = GetLatestCardPaymentMethod(customer.Id);
                    if (paymentMethod != null)
                    {
                        paymentMethodType = PaymentMethodType.Card;
                        stripePaymentMethod = true;
                        subCreateOptions.DefaultPaymentMethod = paymentMethod.Id;
                    }
                }
            }
            return (stripePaymentMethod, paymentMethodType);
        }

        public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
            string paymentToken, short additionalStorageGb, TaxInfo taxInfo)
        {
            if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
            {
                throw new BadRequestException("Payment token is required.");
            }
            if (paymentMethodType == PaymentMethodType.Credit &&
                (user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId)))
            {
                throw new BadRequestException("Your account does not have any credit available.");
            }
            if (paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.GoogleInApp)
            {
                throw new GatewayException("Payment method is not supported at this time.");
            }
            if ((paymentMethodType == PaymentMethodType.GoogleInApp ||
                paymentMethodType == PaymentMethodType.AppleInApp) && additionalStorageGb > 0)
            {
                throw new BadRequestException("You cannot add storage with this payment method.");
            }

            var createdStripeCustomer = false;
            Stripe.Customer customer = null;
            Braintree.Customer braintreeCustomer = null;
            var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
                paymentMethodType == PaymentMethodType.BankAccount || paymentMethodType == PaymentMethodType.Credit;

            string stipeCustomerPaymentMethodId = null;
            string stipeCustomerSourceToken = null;
            if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
            {
                if (paymentToken.StartsWith("pm_"))
                {
                    stipeCustomerPaymentMethodId = paymentToken;
                }
                else
                {
                    stipeCustomerSourceToken = paymentToken;
                }
            }

            if (user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId))
            {
                if (!string.IsNullOrWhiteSpace(paymentToken))
                {
                    try
                    {
                        await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, true, taxInfo);
                    }
                    catch (Exception e)
                    {
                        var message = e.Message.ToLowerInvariant();
                        if (message.Contains("apple") || message.Contains("in-app"))
                        {
                            throw;
                        }
                    }
                }
                try
                {
                    customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId);
                }
                catch { }
            }

            if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
            {
                var stripeCustomerMetadata = new Dictionary<string, string>();
                if (paymentMethodType == PaymentMethodType.PayPal)
                {
                    var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
                    var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
                    {
                        PaymentMethodNonce = paymentToken,
                        Email = user.Email,
                        Id = user.BraintreeCustomerIdPrefix() + user.Id.ToString("N").ToLower() + randomSuffix,
                        CustomFields = new Dictionary<string, string>
                        {
                            [user.BraintreeIdField()] = user.Id.ToString()
                        }
                    });

                    if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
                    {
                        throw new GatewayException("Failed to create PayPal customer record.");
                    }

                    braintreeCustomer = customerResult.Target;
                    stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
                }
                else if (paymentMethodType == PaymentMethodType.AppleInApp)
                {
                    var verifiedReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken);
                    if (verifiedReceiptStatus == null)
                    {
                        throw new GatewayException("Cannot verify apple in-app purchase.");
                    }
                    var receiptOriginalTransactionId = verifiedReceiptStatus.GetOriginalTransactionId();
                    await VerifyAppleReceiptNotInUseAsync(receiptOriginalTransactionId, user);
                    await _appleIapService.SaveReceiptAsync(verifiedReceiptStatus, user.Id);
                    stripeCustomerMetadata.Add("appleReceipt", receiptOriginalTransactionId);
                }
                else if (!stripePaymentMethod)
                {
                    throw new GatewayException("Payment method is not supported at this time.");
                }

                customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
                {
                    Description = user.Name,
                    Email = user.Email,
                    Metadata = stripeCustomerMetadata,
                    PaymentMethod = stipeCustomerPaymentMethodId,
                    Source = stipeCustomerSourceToken,
                    InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
                    {
                        DefaultPaymentMethod = stipeCustomerPaymentMethodId
                    },
                    Address = new Stripe.AddressOptions
                    {
                        Line1 = string.Empty,
                        Country = taxInfo.BillingAddressCountry,
                        PostalCode = taxInfo.BillingAddressPostalCode,
                    },
                });
                createdStripeCustomer = true;
            }

            if (customer == null)
            {
                throw new GatewayException("Could not set up customer payment profile.");
            }

            var subCreateOptions = new Stripe.SubscriptionCreateOptions
            {
                Customer = customer.Id,
                Items = new List<Stripe.SubscriptionItemOptions>(),
                Metadata = new Dictionary<string, string>
                {
                    [user.GatewayIdField()] = user.Id.ToString()
                }
            };

            subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
            {
                Plan = paymentMethodType == PaymentMethodType.AppleInApp ? PremiumPlanAppleIapId : PremiumPlanId,
                Quantity = 1,
            });

            if (!string.IsNullOrWhiteSpace(taxInfo?.BillingAddressCountry)
                    && !string.IsNullOrWhiteSpace(taxInfo?.BillingAddressPostalCode))
            {
                var taxRates = await _taxRateRepository.GetByLocationAsync(
                    new TaxRate()
                    {
                        Country = taxInfo.BillingAddressCountry,
                        PostalCode = taxInfo.BillingAddressPostalCode
                    }
                );
                var taxRate = taxRates.FirstOrDefault();
                if (taxRate != null)
                {
                    subCreateOptions.DefaultTaxRates = new List<string>(1)
                    {
                        taxRate.Id
                    };
                }
            }

            if (additionalStorageGb > 0)
            {
                subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
                {
                    Plan = StoragePlanId,
                    Quantity = additionalStorageGb
                });
            }

            var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
                stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);

            user.Gateway = GatewayType.Stripe;
            user.GatewayCustomerId = customer.Id;
            user.GatewaySubscriptionId = subscription.Id;

            if (subscription.Status == "incomplete" &&
                subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
            {
                return subscription.LatestInvoice.PaymentIntent.ClientSecret;
            }
            else
            {
                user.Premium = true;
                user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
                return null;
            }
        }

        private async Task<Stripe.Subscription> ChargeForNewSubscriptionAsync(ISubscriber subcriber, Stripe.Customer customer,
            bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
            Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
        {
            var addedCreditToStripeCustomer = false;
            Braintree.Transaction braintreeTransaction = null;
            Transaction appleTransaction = null;

            var subInvoiceMetadata = new Dictionary<string, string>();
            Stripe.Subscription subscription = null;
            try
            {
                if (!stripePaymentMethod)
                {
                    var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
                    {
                        Customer = customer.Id,
                        SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
                        SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
                    });

                    if (previewInvoice.AmountDue > 0)
                    {
                        var appleReceiptOrigTransactionId = customer.Metadata != null &&
                            customer.Metadata.ContainsKey("appleReceipt") ? customer.Metadata["appleReceipt"] : null;
                        var braintreeCustomerId = customer.Metadata != null &&
                            customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null;
                        if (!string.IsNullOrWhiteSpace(appleReceiptOrigTransactionId))
                        {
                            if (!subcriber.IsUser())
                            {
                                throw new GatewayException("In-app purchase is only allowed for users.");
                            }

                            var appleReceipt = await _appleIapService.GetReceiptAsync(
                                appleReceiptOrigTransactionId);
                            var verifiedAppleReceipt = await _appleIapService.GetVerifiedReceiptStatusAsync(
                                appleReceipt.Item1);
                            if (verifiedAppleReceipt == null)
                            {
                                throw new GatewayException("Failed to get Apple in-app purchase receipt data.");
                            }
                            subInvoiceMetadata.Add("appleReceipt", verifiedAppleReceipt.GetOriginalTransactionId());
                            var lastTransactionId = verifiedAppleReceipt.GetLastTransactionId();
                            subInvoiceMetadata.Add("appleReceiptTransactionId", lastTransactionId);
                            var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(
                                GatewayType.AppStore, lastTransactionId);
                            if (existingTransaction == null)
                            {
                                appleTransaction = verifiedAppleReceipt.BuildTransactionFromLastTransaction(
                                    PremiumPlanAppleIapPrice, subcriber.Id);
                                appleTransaction.Type = TransactionType.Charge;
                                await _transactionRepository.CreateAsync(appleTransaction);
                            }
                        }
                        else if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
                        {
                            var btInvoiceAmount = (previewInvoice.AmountDue / 100M);
                            var transactionResult = await _btGateway.Transaction.SaleAsync(
                                new Braintree.TransactionRequest
                                {
                                    Amount = btInvoiceAmount,
                                    CustomerId = braintreeCustomerId,
                                    Options = new Braintree.TransactionOptionsRequest
                                    {
                                        SubmitForSettlement = true,
                                        PayPal = new Braintree.TransactionOptionsPayPalRequest
                                        {
                                            CustomField = $"{subcriber.BraintreeIdField()}:{subcriber.Id}"
                                        }
                                    },
                                    CustomFields = new Dictionary<string, string>
                                    {
                                        [subcriber.BraintreeIdField()] = subcriber.Id.ToString()
                                    }
                                });

                            if (!transactionResult.IsSuccess())
                            {
                                throw new GatewayException("Failed to charge PayPal customer.");
                            }

                            braintreeTransaction = transactionResult.Target;
                            subInvoiceMetadata.Add("btTransactionId", braintreeTransaction.Id);
                            subInvoiceMetadata.Add("btPayPalTransactionId",
                                braintreeTransaction.PayPalDetails.AuthorizationId);
                        }
                        else
                        {
                            throw new GatewayException("No payment was able to be collected.");
                        }

                        await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
                        {
                            Balance = customer.Balance - previewInvoice.AmountDue
                        });
                        addedCreditToStripeCustomer = true;
                    }
                }
                else if (paymentMethodType == PaymentMethodType.Credit)
                {
                    var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
                    {
                        Customer = customer.Id,
                        SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
                        SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
                    });
                    if (previewInvoice.AmountDue > 0)
                    {
                        throw new GatewayException("Your account does not have enough credit available.");
                    }
                }

                subCreateOptions.OffSession = true;
                subCreateOptions.AddExpand("latest_invoice.payment_intent");
                subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
                if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
                {
                    if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
                    {
                        await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
                        throw new GatewayException("Payment method was declined.");
                    }
                }

                if (!stripePaymentMethod && subInvoiceMetadata.Any())
                {
                    var invoices = await _stripeAdapter.InvoiceListAsync(new Stripe.InvoiceListOptions
                    {
                        Subscription = subscription.Id
                    });

                    var invoice = invoices?.FirstOrDefault();
                    if (invoice == null)
                    {
                        throw new GatewayException("Invoice not found.");
                    }

                    await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
                    {
                        Metadata = subInvoiceMetadata
                    });
                }

                return subscription;
            }
            catch (Exception e)
            {
                if (customer != null)
                {
                    if (createdStripeCustomer)
                    {
                        await _stripeAdapter.CustomerDeleteAsync(customer.Id);
                    }
                    else if (addedCreditToStripeCustomer || customer.Balance < 0)
                    {
                        await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
                        {
                            Balance = customer.Balance
                        });
                    }
                }
                if (braintreeTransaction != null)
                {
                    await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
                }
                if (braintreeCustomer != null)
                {
                    await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
                }
                if (appleTransaction != null)
                {
                    await _transactionRepository.DeleteAsync(appleTransaction);
                }

                if (e is Stripe.StripeException strEx &&
                    (strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
                {
                    throw new GatewayException("Bank account is not yet verified.");
                }

                throw;
            }
        }

        private List<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
            List<Stripe.SubscriptionItemOptions> subItemOptions)
        {
            return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions
            {
                Plan = si.Plan,
                Quantity = si.Quantity
            }).ToList();
        }

        private async Task<string> FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber,
            SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
        {
            var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
            if (sub == null)
            {
                throw new GatewayException("Subscription not found.");
            }

            prorationDate ??= DateTime.UtcNow;
            var collectionMethod = sub.CollectionMethod;
            var daysUntilDue = sub.DaysUntilDue;
            var chargeNow = collectionMethod == "charge_automatically";
            var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);

            var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
            {
                Items = updatedItemOptions,
                ProrationBehavior = "always_invoice",
                DaysUntilDue = daysUntilDue ?? 1,
                CollectionMethod = "send_invoice",
                ProrationDate = prorationDate,
            };

            if (!subscriptionUpdate.UpdateNeeded(sub))
            {
                // No need to update subscription, quantity matches
                return null;
            }

            var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId);

            if (!string.IsNullOrWhiteSpace(customer?.Address?.Country)
                    && !string.IsNullOrWhiteSpace(customer?.Address?.PostalCode))
            {
                var taxRates = await _taxRateRepository.GetByLocationAsync(
                    new TaxRate()
                    {
                        Country = customer.Address.Country,
                        PostalCode = customer.Address.PostalCode
                    }
                );
                var taxRate = taxRates.FirstOrDefault();
                if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
                {
                    subUpdateOptions.DefaultTaxRates = new List<string>(1)
                    {
                        taxRate.Id
                    };
                }
            }

            var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);

            var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
            if (invoice == null)
            {
                throw new BadRequestException("Unable to locate draft invoice for subscription update.");
            }

            string paymentIntentClientSecret = null;
            if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))
            {
                try
                {
                    if (chargeNow)
                    {
                        paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
                            storableSubscriber, invoice);
                    }
                    else
                    {
                        invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
                        {
                            AutoAdvance = false,
                        });
                        await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
                        paymentIntentClientSecret = null;
                    }
                }
                catch
                {
                    // Need to revert the subscription
                    await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
                    {
                        Items = subscriptionUpdate.RevertItemsOptions(sub),
                        // This proration behavior prevents a false "credit" from
                        //  being applied forward to the next month's invoice
                        ProrationBehavior = "none",
                        CollectionMethod = collectionMethod,
                        DaysUntilDue = daysUntilDue,
                    });
                    throw;
                }
            }
            else if (!invoice.Paid)
            {
                // Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h
                invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId);
                paymentIntentClientSecret = null;
            }

            // Change back the subscription collection method and/or days until due
            if (collectionMethod != "send_invoice" || daysUntilDue == null)
            {
                await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
                {
                    CollectionMethod = collectionMethod,
                    DaysUntilDue = daysUntilDue,
                });
            }

            return paymentIntentClientSecret;
        }

        public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
        {
            return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
        }

        public Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage,
            string storagePlanId, DateTime? prorationDate = null)
        {
            return FinalizeSubscriptionChangeAsync(storableSubscriber, new StorageSubscriptionUpdate(storagePlanId, additionalStorage), prorationDate);
        }

        public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)
        {
            if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
            {
                await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
                    new Stripe.SubscriptionCancelOptions());
            }

            if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                return;
            }

            var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
            if (customer == null)
            {
                return;
            }

            if (customer.Metadata.ContainsKey("btCustomerId"))
            {
                var transactionRequest = new Braintree.TransactionSearchRequest()
                    .CustomerId.Is(customer.Metadata["btCustomerId"]);
                var transactions = _btGateway.Transaction.Search(transactionRequest);

                if ((transactions?.MaximumCount ?? 0) > 0)
                {
                    var txs = transactions.Cast<Braintree.Transaction>().Where(c => c.RefundedTransactionId == null);
                    foreach (var transaction in txs)
                    {
                        await _btGateway.Transaction.RefundAsync(transaction.Id);
                    }
                }

                await _btGateway.Customer.DeleteAsync(customer.Metadata["btCustomerId"]);
            }
            else
            {
                var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions
                {
                    Customer = subscriber.GatewayCustomerId
                });

                if (charges?.Data != null)
                {
                    foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))
                    {
                        await _stripeAdapter.RefundCreateAsync(new Stripe.RefundCreateOptions { Charge = charge.Id });
                    }
                }
            }

            await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
        }

        public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
        {
            var customerOptions = new Stripe.CustomerGetOptions();
            customerOptions.AddExpand("default_source");
            customerOptions.AddExpand("invoice_settings.default_payment_method");
            var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
            var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt");
            if (usingInAppPaymentMethod)
            {
                throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " +
                    "Contact support.");
            }

            string paymentIntentClientSecret = null;

            // Invoice them and pay now instead of waiting until Stripe does this automatically.

            string cardPaymentMethodId = null;
            if (!customer.Metadata.ContainsKey("btCustomerId"))
            {
                var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
                var hasDefaultValidSource = customer.DefaultSource != null &&
                    (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount);
                if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)
                {
                    cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
                    if (cardPaymentMethodId == null)
                    {
                        // We're going to delete this draft invoice, it can't be paid
                        try
                        {
                            await _stripeAdapter.InvoiceDeleteAsync(invoice.Id);
                        }
                        catch
                        {
                            await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
                            {
                                AutoAdvance = false
                            });
                            await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id);
                        }
                        throw new BadRequestException("No payment method is available.");
                    }
                }
            }

            Braintree.Transaction braintreeTransaction = null;
            try
            {
                // Finalize the invoice (from Draft) w/o auto-advance so we
                //  can attempt payment manually.
                invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
                {
                    AutoAdvance = false,
                });
                var invoicePayOptions = new Stripe.InvoicePayOptions
                {
                    PaymentMethod = cardPaymentMethodId,
                };
                if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
                {
                    invoicePayOptions.PaidOutOfBand = true;
                    var btInvoiceAmount = (invoice.AmountDue / 100M);
                    var transactionResult = await _btGateway.Transaction.SaleAsync(
                        new Braintree.TransactionRequest
                        {
                            Amount = btInvoiceAmount,
                            CustomerId = customer.Metadata["btCustomerId"],
                            Options = new Braintree.TransactionOptionsRequest
                            {
                                SubmitForSettlement = true,
                                PayPal = new Braintree.TransactionOptionsPayPalRequest
                                {
                                    CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}"
                                }
                            },
                            CustomFields = new Dictionary<string, string>
                            {
                                [subscriber.BraintreeIdField()] = subscriber.Id.ToString()
                            }
                        });

                    if (!transactionResult.IsSuccess())
                    {
                        throw new GatewayException("Failed to charge PayPal customer.");
                    }

                    braintreeTransaction = transactionResult.Target;
                    invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
                    {
                        Metadata = new Dictionary<string, string>
                        {
                            ["btTransactionId"] = braintreeTransaction.Id,
                            ["btPayPalTransactionId"] =
                                braintreeTransaction.PayPalDetails.AuthorizationId
                        },
                    });
                    invoicePayOptions.PaidOutOfBand = true;
                }

                try
                {
                    invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
                }
                catch (Stripe.StripeException e)
                {
                    if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
                        e.StripeError?.Code == "invoice_payment_intent_requires_action")
                    {
                        // SCA required, get intent client secret
                        var invoiceGetOptions = new Stripe.InvoiceGetOptions();
                        invoiceGetOptions.AddExpand("payment_intent");
                        invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
                        paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
                    }
                    else
                    {
                        throw new GatewayException("Unable to pay invoice.");
                    }
                }
            }
            catch (Exception e)
            {
                if (braintreeTransaction != null)
                {
                    await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
                }
                if (invoice != null)
                {
                    if (invoice.Status == "paid")
                    {
                        // It's apparently paid, so we need to return w/o throwing an exception
                        return paymentIntentClientSecret;
                    }

                    invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());

                    // HACK: Workaround for customer balance credit
                    if (invoice.StartingBalance < 0)
                    {
                        // Customer had a balance applied to this invoice. Since we can't fully trust Stripe to
                        //  credit it back to the customer (even though their docs claim they will), we need to
                        //  check that balance against the current customer balance and determine if it needs to be re-applied
                        customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);

                        // Assumption: Customer balance should now be $0, otherwise payment would not have failed.
                        if (customer.Balance == 0)
                        {
                            await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
                            {
                                Balance = invoice.StartingBalance
                            });
                        }
                    }
                }

                if (e is Stripe.StripeException strEx &&
                    (strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
                {
                    throw new GatewayException("Bank account is not yet verified.");
                }

                // Let the caller perform any subscription change cleanup
                throw;
            }
            return paymentIntentClientSecret;
        }

        public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false,
            bool skipInAppPurchaseCheck = false)
        {
            if (subscriber == null)
            {
                throw new ArgumentNullException(nameof(subscriber));
            }

            if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
            {
                throw new GatewayException("No subscription.");
            }

            if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId) && !skipInAppPurchaseCheck)
            {
                var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
                if (customer.Metadata.ContainsKey("appleReceipt"))
                {
                    throw new BadRequestException("You are required to manage your subscription from the app store.");
                }
            }

            var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
            if (sub == null)
            {
                throw new GatewayException("Subscription was not found.");
            }

            if (sub.CanceledAt.HasValue || sub.Status == "canceled" || sub.Status == "unpaid" ||
                sub.Status == "incomplete_expired")
            {
                // Already canceled
                return;
            }

            try
            {
                var canceledSub = endOfPeriod ?
                    await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
                        new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
                    await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions());
                if (!canceledSub.CanceledAt.HasValue)
                {
                    throw new GatewayException("Unable to cancel subscription.");
                }
            }
            catch (Stripe.StripeException e)
            {
                if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
                {
                    throw;
                }
            }
        }

        public async Task ReinstateSubscriptionAsync(ISubscriber subscriber)
        {
            if (subscriber == null)
            {
                throw new ArgumentNullException(nameof(subscriber));
            }

            if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
            {
                throw new GatewayException("No subscription.");
            }

            var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
            if (sub == null)
            {
                throw new GatewayException("Subscription was not found.");
            }

            if ((sub.Status != "active" && sub.Status != "trialing" && !sub.Status.StartsWith("incomplete")) ||
                !sub.CanceledAt.HasValue)
            {
                throw new GatewayException("Subscription is not marked for cancellation.");
            }

            var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
                new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
            if (updatedSub.CanceledAt.HasValue)
            {
                throw new GatewayException("Unable to reinstate subscription.");
            }
        }

        public async Task<bool> UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType,
            string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null)
        {
            if (subscriber == null)
            {
                throw new ArgumentNullException(nameof(subscriber));
            }

            if (subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Stripe)
            {
                throw new GatewayException("Switching from one payment type to another is not supported. " +
                    "Contact us for assistance.");
            }

            var createdCustomer = false;
            AppleReceiptStatus appleReceiptStatus = null;
            Braintree.Customer braintreeCustomer = null;
            string stipeCustomerSourceToken = null;
            string stipeCustomerPaymentMethodId = null;
            var stripeCustomerMetadata = new Dictionary<string, string>();
            var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
                paymentMethodType == PaymentMethodType.BankAccount;
            var inAppPurchase = paymentMethodType == PaymentMethodType.AppleInApp ||
                paymentMethodType == PaymentMethodType.GoogleInApp;

            Stripe.Customer customer = null;

            if (!allowInAppPurchases && inAppPurchase)
            {
                throw new GatewayException("In-app purchase payment method is not allowed.");
            }

            if (!subscriber.IsUser() && inAppPurchase)
            {
                throw new GatewayException("In-app purchase payment method is only allowed for users.");
            }

            if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
                if (customer.Metadata?.Any() ?? false)
                {
                    stripeCustomerMetadata = customer.Metadata;
                }
            }

            if (inAppPurchase && customer != null && customer.Balance != 0)
            {
                throw new GatewayException("Customer balance cannot exist when using in-app purchases.");
            }

            if (!inAppPurchase && customer != null && stripeCustomerMetadata.ContainsKey("appleReceipt"))
            {
                throw new GatewayException("Cannot change from in-app payment method. Contact support.");
            }

            var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId");
            if (stripePaymentMethod)
            {
                if (paymentToken.StartsWith("pm_"))
                {
                    stipeCustomerPaymentMethodId = paymentToken;
                }
                else
                {
                    stipeCustomerSourceToken = paymentToken;
                }
            }
            else if (paymentMethodType == PaymentMethodType.PayPal)
            {
                if (hadBtCustomer)
                {
                    var pmResult = await _btGateway.PaymentMethod.CreateAsync(new Braintree.PaymentMethodRequest
                    {
                        CustomerId = stripeCustomerMetadata["btCustomerId"],
                        PaymentMethodNonce = paymentToken
                    });

                    if (pmResult.IsSuccess())
                    {
                        var customerResult = await _btGateway.Customer.UpdateAsync(
                            stripeCustomerMetadata["btCustomerId"], new Braintree.CustomerRequest
                            {
                                DefaultPaymentMethodToken = pmResult.Target.Token
                            });

                        if (customerResult.IsSuccess() && customerResult.Target.PaymentMethods.Length > 0)
                        {
                            braintreeCustomer = customerResult.Target;
                        }
                        else
                        {
                            await _btGateway.PaymentMethod.DeleteAsync(pmResult.Target.Token);
                            hadBtCustomer = false;
                        }
                    }
                    else
                    {
                        hadBtCustomer = false;
                    }
                }

                if (!hadBtCustomer)
                {
                    var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
                    {
                        PaymentMethodNonce = paymentToken,
                        Email = subscriber.BillingEmailAddress(),
                        Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() +
                            Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false),
                        CustomFields = new Dictionary<string, string>
                        {
                            [subscriber.BraintreeIdField()] = subscriber.Id.ToString()
                        }
                    });

                    if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
                    {
                        throw new GatewayException("Failed to create PayPal customer record.");
                    }

                    braintreeCustomer = customerResult.Target;
                }
            }
            else if (paymentMethodType == PaymentMethodType.AppleInApp)
            {
                appleReceiptStatus = await _appleIapService.GetVerifiedReceiptStatusAsync(paymentToken);
                if (appleReceiptStatus == null)
                {
                    throw new GatewayException("Cannot verify Apple in-app purchase.");
                }
                await VerifyAppleReceiptNotInUseAsync(appleReceiptStatus.GetOriginalTransactionId(), subscriber);
            }
            else
            {
                throw new GatewayException("Payment method is not supported at this time.");
            }

            if (stripeCustomerMetadata.ContainsKey("btCustomerId"))
            {
                if (braintreeCustomer?.Id != stripeCustomerMetadata["btCustomerId"])
                {
                    var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow);
                    stripeCustomerMetadata.Add($"btCustomerId_{nowSec}", stripeCustomerMetadata["btCustomerId"]);
                }
                stripeCustomerMetadata["btCustomerId"] = braintreeCustomer?.Id;
            }
            else if (!string.IsNullOrWhiteSpace(braintreeCustomer?.Id))
            {
                stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
            }

            if (appleReceiptStatus != null)
            {
                var originalTransactionId = appleReceiptStatus.GetOriginalTransactionId();
                if (stripeCustomerMetadata.ContainsKey("appleReceipt"))
                {
                    if (originalTransactionId != stripeCustomerMetadata["appleReceipt"])
                    {
                        var nowSec = Utilities.CoreHelpers.ToEpocSeconds(DateTime.UtcNow);
                        stripeCustomerMetadata.Add($"appleReceipt_{nowSec}", stripeCustomerMetadata["appleReceipt"]);
                    }
                    stripeCustomerMetadata["appleReceipt"] = originalTransactionId;
                }
                else
                {
                    stripeCustomerMetadata.Add("appleReceipt", originalTransactionId);
                }
                await _appleIapService.SaveReceiptAsync(appleReceiptStatus, subscriber.Id);
            }

            try
            {
                if (customer == null)
                {
                    customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
                    {
                        Description = subscriber.BillingName(),
                        Email = subscriber.BillingEmailAddress(),
                        Metadata = stripeCustomerMetadata,
                        Source = stipeCustomerSourceToken,
                        PaymentMethod = stipeCustomerPaymentMethodId,
                        InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
                        {
                            DefaultPaymentMethod = stipeCustomerPaymentMethodId
                        },
                        Address = taxInfo == null ? null : new Stripe.AddressOptions
                        {
                            Country = taxInfo.BillingAddressCountry,
                            PostalCode = taxInfo.BillingAddressPostalCode,
                            Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
                            Line2 = taxInfo.BillingAddressLine2,
                            City = taxInfo.BillingAddressCity,
                            State = taxInfo.BillingAddressState,
                        },
                    });

                    subscriber.Gateway = GatewayType.Stripe;
                    subscriber.GatewayCustomerId = customer.Id;
                    createdCustomer = true;
                }

                if (!createdCustomer)
                {
                    string defaultSourceId = null;
                    string defaultPaymentMethodId = null;
                    if (stripePaymentMethod)
                    {
                        if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
                        {
                            var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new Stripe.BankAccountCreateOptions
                            {
                                Source = paymentToken
                            });
                            defaultSourceId = bankAccount.Id;
                        }
                        else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
                        {
                            await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
                                new Stripe.PaymentMethodAttachOptions { Customer = customer.Id });
                            defaultPaymentMethodId = stipeCustomerPaymentMethodId;
                        }
                    }

                    foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
                    {
                        if (source is Stripe.BankAccount)
                        {
                            await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
                        }
                        else if (source is Stripe.Card)
                        {
                            await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
                        }
                    }

                    var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions
                    {
                        Customer = customer.Id,
                        Type = "card"
                    });
                    foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
                    {
                        await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new Stripe.PaymentMethodDetachOptions());
                    }

                    customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
                    {
                        Metadata = stripeCustomerMetadata,
                        DefaultSource = defaultSourceId,
                        InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
                        {
                            DefaultPaymentMethod = defaultPaymentMethodId
                        },
                        Address = taxInfo == null ? null : new Stripe.AddressOptions
                        {
                            Country = taxInfo.BillingAddressCountry,
                            PostalCode = taxInfo.BillingAddressPostalCode,
                            Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
                            Line2 = taxInfo.BillingAddressLine2,
                            City = taxInfo.BillingAddressCity,
                            State = taxInfo.BillingAddressState,
                        },
                    });
                }
            }
            catch
            {
                if (braintreeCustomer != null && !hadBtCustomer)
                {
                    await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
                }
                throw;
            }

            return createdCustomer;
        }

        public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
        {
            Stripe.Customer customer = null;
            var customerExists = subscriber.Gateway == GatewayType.Stripe &&
                !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
            if (customerExists)
            {
                customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
            }
            else
            {
                customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
                {
                    Email = subscriber.BillingEmailAddress(),
                    Description = subscriber.BillingName(),
                });
                subscriber.Gateway = GatewayType.Stripe;
                subscriber.GatewayCustomerId = customer.Id;
            }
            await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
            {
                Balance = customer.Balance - (long)(creditAmount * 100)
            });
            return !customerExists;
        }

        public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
        {
            var billingInfo = new BillingInfo();

            ICollection<Transaction> transactions = null;
            if (subscriber is User)
            {
                transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id);
            }
            else if (subscriber is Organization)
            {
                transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id);
            }
            if (transactions != null)
            {
                billingInfo.Transactions = transactions?.OrderByDescending(i => i.CreationDate)
                    .Select(t => new BillingInfo.BillingTransaction(t));
            }

            if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                Stripe.Customer customer = null;
                try
                {
                    var customerOptions = new Stripe.CustomerGetOptions();
                    customerOptions.AddExpand("default_source");
                    customerOptions.AddExpand("invoice_settings.default_payment_method");
                    customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
                }
                catch (Stripe.StripeException) { }
                if (customer != null)
                {
                    billingInfo.Balance = customer.Balance / 100M;

                    if (customer.Metadata?.ContainsKey("appleReceipt") ?? false)
                    {
                        billingInfo.PaymentSource = new BillingInfo.BillingSource
                        {
                            Type = PaymentMethodType.AppleInApp
                        };
                    }
                    else if (customer.Metadata?.ContainsKey("btCustomerId") ?? false)
                    {
                        try
                        {
                            var braintreeCustomer = await _btGateway.Customer.FindAsync(
                                customer.Metadata["btCustomerId"]);
                            if (braintreeCustomer?.DefaultPaymentMethod != null)
                            {
                                billingInfo.PaymentSource = new BillingInfo.BillingSource(
                                    braintreeCustomer.DefaultPaymentMethod);
                            }
                        }
                        catch (Braintree.Exceptions.NotFoundException) { }
                    }
                    else if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card")
                    {
                        billingInfo.PaymentSource = new BillingInfo.BillingSource(
                            customer.InvoiceSettings.DefaultPaymentMethod);
                    }
                    else if (customer.DefaultSource != null &&
                        (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount))
                    {
                        billingInfo.PaymentSource = new BillingInfo.BillingSource(customer.DefaultSource);
                    }
                    if (billingInfo.PaymentSource == null)
                    {
                        var paymentMethod = GetLatestCardPaymentMethod(customer.Id);
                        if (paymentMethod != null)
                        {
                            billingInfo.PaymentSource = new BillingInfo.BillingSource(paymentMethod);
                        }
                    }

                    var invoices = await _stripeAdapter.InvoiceListAsync(new Stripe.InvoiceListOptions
                    {
                        Customer = customer.Id,
                        Limit = 50
                    });
                    billingInfo.Invoices = invoices.Data.Where(i => i.Status != "void" && i.Status != "draft")
                        .OrderByDescending(i => i.Created).Select(i => new BillingInfo.BillingInvoice(i));
                }
            }

            return billingInfo;
        }

        public async Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber)
        {
            var subscriptionInfo = new SubscriptionInfo();

            if (subscriber.IsUser() && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
                subscriptionInfo.UsingInAppPurchase = customer.Metadata.ContainsKey("appleReceipt");
            }

            if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
            {
                var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
                if (sub != null)
                {
                    subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub);
                }

                if (!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
                {
                    try
                    {
                        var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync(
                            new Stripe.UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId });
                        if (upcomingInvoice != null)
                        {
                            subscriptionInfo.UpcomingInvoice =
                                new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice);
                        }
                    }
                    catch (Stripe.StripeException) { }
                }
            }

            return subscriptionInfo;
        }

        public async Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber)
        {
            if (subscriber == null || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                return null;
            }

            var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);

            if (customer == null)
            {
                return null;
            }

            var address = customer.Address;
            var taxId = customer.TaxIds?.FirstOrDefault();

            // Line1 is required, so if missing we're using the subscriber name
            // see: https://stripe.com/docs/api/customers/create#create_customer-address-line1
            if (address != null && string.IsNullOrWhiteSpace(address.Line1))
            {
                address.Line1 = null;
            }

            return new TaxInfo
            {
                TaxIdNumber = taxId?.Value,
                BillingAddressLine1 = address?.Line1,
                BillingAddressLine2 = address?.Line2,
                BillingAddressCity = address?.City,
                BillingAddressState = address?.State,
                BillingAddressPostalCode = address?.PostalCode,
                BillingAddressCountry = address?.Country,
            };
        }

        public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo)
        {
            if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
            {
                var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new Stripe.CustomerUpdateOptions
                {
                    Address = new Stripe.AddressOptions
                    {
                        Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
                        Line2 = taxInfo.BillingAddressLine2,
                        City = taxInfo.BillingAddressCity,
                        State = taxInfo.BillingAddressState,
                        PostalCode = taxInfo.BillingAddressPostalCode,
                        Country = taxInfo.BillingAddressCountry,
                    },
                });

                if (!subscriber.IsUser() && customer != null)
                {
                    var taxId = customer.TaxIds?.FirstOrDefault();

                    if (taxId != null)
                    {
                        await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id);
                    }
                    if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
                        !string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
                    {
                        await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions
                        {
                            Type = taxInfo.TaxIdType,
                            Value = taxInfo.TaxIdNumber,
                        });
                    }
                }
            }
        }

        public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
        {
            var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions()
            {
                DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
                Inclusive = false,
                Percentage = taxRate.Rate,
                Active = true
            };
            var stripeTaxRate = await _stripeAdapter.TaxRateCreateAsync(stripeTaxRateOptions);
            taxRate.Id = stripeTaxRate.Id;
            await _taxRateRepository.CreateAsync(taxRate);
            return taxRate;
        }

        public async Task UpdateTaxRateAsync(TaxRate taxRate)
        {
            if (string.IsNullOrWhiteSpace(taxRate.Id))
            {
                return;
            }

            await ArchiveTaxRateAsync(taxRate);
            await CreateTaxRateAsync(taxRate);
        }

        public async Task ArchiveTaxRateAsync(TaxRate taxRate)
        {
            if (string.IsNullOrWhiteSpace(taxRate.Id))
            {
                return;
            }

            var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
                    taxRate.Id,
                    new Stripe.TaxRateUpdateOptions() { Active = false }
            );
            if (!updatedStripeTaxRate.Active)
            {
                taxRate.Active = false;
                await _taxRateRepository.ArchiveAsync(taxRate);
            }
        }

        private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
        {
            var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
                new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" });
            return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
        }

        private async Task VerifyAppleReceiptNotInUseAsync(string receiptOriginalTransactionId, ISubscriber subscriber)
        {
            var existingReceipt = await _appleIapService.GetReceiptAsync(receiptOriginalTransactionId);
            if (existingReceipt != null && existingReceipt.Item2.HasValue && existingReceipt.Item2 != subscriber.Id)
            {
                var existingUser = await _userRepository.GetByIdAsync(existingReceipt.Item2.Value);
                if (existingUser != null)
                {
                    throw new GatewayException("Apple receipt already in use by another user.");
                }
            }
        }
    }
}
