# frozen_string_literal: true

require 'payment/service'

# Stripe payement gateway
module Stripe; end

## create remote objects on stripe
class Stripe::Service < Payment::Service

  # Build the subscription base on the given shopping cart and create it on the remote stripe API
  def subscribe(payment_method_id, shopping_cart)
    price_details = shopping_cart.total

    payment_schedule = price_details[:schedule][:payment_schedule]
    payment_schedule.payment_schedule_items = price_details[:schedule][:items]
    first_item = price_details[:schedule][:items].min_by(&:due_date)
    subscription = shopping_cart.items.find { |item| item.class == CartItem::Subscription }.to_object
    reservable_stp_id = shopping_cart.items.find { |item| item.is_a?(CartItem::Reservation) }&.to_object
                          &.reservable&.payment_gateway_object&.gateway_object_id

    WalletService.debit_user_wallet(payment_schedule, shopping_cart.customer, transaction: false)
    handle_wallet_transaction(payment_schedule)

    price = create_price(first_item.details['recurring'],
                         subscription.plan.payment_gateway_object.gateway_object_id,
                         nil, monthly: true)
    # other items (not recurring)
    items = subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)

    create_remote_subscription(shopping_cart, payment_schedule, items, price, payment_method_id)
  end

  def create_subscription(payment_schedule, stp_object_id, stp_object_type)
    stripe_key = Setting.get('stripe_secret_key')

    stp_subscription = Stripe::Item.new(stp_object_type, stp_object_id).retrieve

    payment_method_id = Stripe::Customer.retrieve(stp_subscription.customer, api_key: stripe_key).invoice_settings.default_payment_method
    payment_method = Stripe::PaymentMethod.retrieve(payment_method_id, api_key: stripe_key)
    pgo = PaymentGatewayObject.new(item: payment_schedule)
    pgo.gateway_object = payment_method
    pgo.save!
  end

  def cancel_subscription(payment_schedule)
    stripe_key = Setting.get('stripe_secret_key')

    stp_subscription = payment_schedule.gateway_subscription.retrieve

    res = Stripe::Subscription.delete(stp_subscription.id, {}, api_key: stripe_key)
    res.status == 'canceled'
  end

  def create_user(user_id)
    StripeWorker.perform_async(:create_stripe_customer, user_id)
  end

  def create_coupon(coupon_id)
    coupon = Coupon.find(coupon_id)
    stp_coupon = { id: coupon.code }
    if coupon.type == 'percent_off'
      stp_coupon[:percent_off] = coupon.percent_off
    elsif coupon.type == stripe_amount('amount_off')
      stp_coupon[:amount_off] = coupon.amount_off
      stp_coupon[:currency] = Setting.get('stripe_currency')
    end

    stp_coupon[:duration] = coupon.validity_per_user == 'always' ? 'forever' : 'once'
    stp_coupon[:redeem_by] = coupon.valid_until.to_i unless coupon.valid_until.nil?
    stp_coupon[:max_redemptions] = coupon.max_usages unless coupon.max_usages.nil?

    Stripe::Coupon.create(stp_coupon, api_key: Setting.get('stripe_secret_key'))
  end

  def delete_coupon(coupon_id)
    coupon = Coupon.find(coupon_id)
    StripeWorker.perform_async(:delete_stripe_coupon, coupon.code)
  end

  def create_or_update_product(klass, id)
    StripeWorker.perform_async(:create_or_update_stp_product, klass, id)
  rescue Stripe::InvalidRequestError => e
    raise ::PaymentGatewayError, e
  end

  def stripe_amount(amount)
    currency = Setting.get('stripe_currency')
    return amount / 100 if zero_decimal_currencies.any? { |s| s.casecmp(currency).zero? }

    amount
  end

  def process_payment_schedule_item(payment_schedule_item)
    stripe_key = Setting.get('stripe_secret_key')
    stp_subscription = payment_schedule_item.payment_schedule.gateway_subscription.retrieve
    if stp_subscription.status == 'canceled'
      # the subscription was canceled by the gateway => notify & update the status
      notify_payment_schedule_gateway_canceled(payment_schedule_item)
      payment_schedule_item.update_attributes(state: 'gateway_canceled')
      return
    end
    stp_invoice = Stripe::Invoice.retrieve(stp_subscription.latest_invoice, api_key: stripe_key)
    if stp_invoice.status == 'paid'
      ##### Successfully paid
      PaymentScheduleService.new.generate_invoice(payment_schedule_item,
                                                  payment_method: 'card',
                                                  payment_id: stp_invoice.payment_intent,
                                                  payment_type: 'Stripe::PaymentIntent')
      payment_schedule_item.update_attributes(state: 'paid', payment_method: 'card')
      pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
      pgo.gateway_object = stp_invoice
      pgo.save!
    elsif stp_subscription.status == 'past_due' || stp_invoice.status == 'open'
      ##### Payment error
      notify_payment_schedule_item_failed(payment_schedule_item)
      stp_payment_intent = Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: stripe_key)
      payment_schedule_item.update_attributes(state: stp_payment_intent.status,
                                              client_secret: stp_payment_intent.client_secret)
      pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
      pgo.gateway_object = stp_invoice
      pgo.save!
    elsif stp_invoice.status == 'draft'
      return # Could be that the stripe invoice does not yet reflect the payment made by the member, because we called that service just after payment is made. We call return here and PaymentScheduleItemWorker will anyway call that method every hour
    else
      notify_payment_schedule_item_error(payment_schedule_item)
      payment_schedule_item.update_attributes(state: 'error')
    end
  end

  def pay_payment_schedule_item(payment_schedule_item)
    stripe_key = Setting.get('stripe_secret_key')
    stp_invoice = Stripe::Invoice.pay(@payment_schedule_item.payment_gateway_object.gateway_object_id, {}, { api_key: stripe_key })
    PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)

    { status: stp_invoice.status }
  rescue Stripe::StripeError => e
    stripe_key = Setting.get('stripe_secret_key')
    stp_invoice = Stripe::Invoice.retrieve(@payment_schedule_item.payment_gateway_object.gateway_object_id, api_key: stripe_key)
    PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)

    { status: stp_invoice.status, error: e }
  end

  def attach_method_as_default(payment_method_id, customer_id)
    stripe_key = Setting.get('stripe_secret_key')

    # attach the payment method to the given customer
    method = Stripe::PaymentMethod.attach(
      payment_method_id,
      { customer: customer_id },
      { api_key: stripe_key }
    )
    # then set it as the default payment method for this customer
    Stripe::Customer.update(
      customer_id,
      { invoice_settings: { default_payment_method: payment_method_id } },
      { api_key: stripe_key }
    )
    method
  end

  private


  # Create the provided PaymentSchedule on Stripe, using the Subscription API
  def create_remote_subscription(shopping_cart, payment_schedule, items, price, payment_method_id)
    stripe_key = Setting.get('stripe_secret_key')
    if payment_schedule.start_at.nil?
      Stripe::Subscription.create({
                                    customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
                                    cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month).to_i,
                                    add_invoice_items: items,
                                    coupon: payment_schedule.coupon&.code,
                                    items: [
                                      { price: price[:id] }
                                    ],
                                    default_payment_method: payment_method_id,
                                    expand: %w[latest_invoice.payment_intent]
                                  }, { api_key: stripe_key })
    else
      Stripe::SubscriptionSchedule.create({
                                            customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
                                            start_date: payment_schedule.start_at.to_i,
                                            end_behavior: 'cancel',
                                            phases: [
                                              {
                                                items: [
                                                  { price: price[:id] }
                                                ],
                                                add_invoice_items: items,
                                                coupon: payment_schedule.coupon&.code,
                                                default_payment_method: payment_method_id,
                                                end_date: (
                                                  payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month
                                                ).to_i
                                              }
                                            ]
                                          }, { api_key: stripe_key })
    end
  end

  def subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
    second_item = payment_schedule.payment_schedule_items.sort_by(&:due_date)[1]

    items = []
    if second_item && first_item.amount != second_item.amount
      unless first_item.details['adjustment']&.zero?
        # adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
        # The difference is invoiced here
        p1 = create_price(first_item.details['adjustment'],
                          subscription.plan.payment_gateway_object.gateway_object_id,
                          "Price adjustment for payment schedule #{payment_schedule.id}")
        items.push(price: p1[:id])
      end
      unless first_item.details['other_items']&.zero?
        # when taking a subscription at the same time of a reservation (space, machine or training), the amount of the
        # reservation is invoiced here.
        p2 = create_price(first_item.details['other_items'],
                          reservable_stp_id,
                          "Reservations for payment schedule #{payment_schedule.id}")
        items.push(price: p2[:id])
      end
    end

    items
  end

  def create_price(amount, stp_product_id, name, monthly: false)
    params = {
      unit_amount: stripe_amount(amount),
      currency: Setting.get('stripe_currency'),
      product: stp_product_id,
      nickname: name
    }
    params[:recurring] = { interval: 'month', interval_count: 1 } if monthly

    Stripe::Price.create(params, api_key: Setting.get('stripe_secret_key'))
  end

  def handle_wallet_transaction(payment_schedule)
    return unless payment_schedule.wallet_amount

    customer_id = payment_schedule.invoicing_profile.user.payment_gateway_object.gateway_object_id
    Stripe::Customer.update(customer_id, { balance: -payment_schedule.wallet_amount }, { api_key: Setting.get('stripe_secret_key') })
  end

  # @see https://stripe.com/docs/currencies#zero-decimal
  def zero_decimal_currencies
    %w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF]
  end
end