// @ts-check

import recurly from 'recurly'

import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import { callbackify } from 'node:util'
import UserGetter from '../User/UserGetter.mjs'
import {
  PaymentProviderSubscription,
  PaymentProviderSubscriptionAddOn,
  PaymentProviderSubscriptionChange,
  PaypalPaymentMethod,
  CreditCardPaymentMethod,
  PaymentProviderAddOn,
  PaymentProviderPlan,
  PaymentProviderCoupon,
  PaymentProviderAccount,
  PaymentProviderImmediateCharge,
} from './PaymentProviderEntities.mjs'
import {
  MissingBillingInfoError,
  SubtotalLimitExceededError,
} from './Errors.mjs'
import RecurlyMetrics from './RecurlyMetrics.mjs'
import { isStandaloneAiAddOnPlanCode, AI_ADD_ON_CODE } from './AiHelper.mjs'
import _ from 'lodash'

/**
 * @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities.mjs'
 * @import { PaymentProviderSubscriptionUpdateRequest } from './PaymentProviderEntities.mjs'
 * @import { PaymentMethod } from './types'
 * @import { CurrencyCode } from '../../../../types/subscription/currency'
 */

class RecurlyClientWithErrorHandling extends recurly.Client {
  /**
   * @param {any} response
   * @return {Error | null}
   * @private
   */
  _errorFromResponse(response) {
    RecurlyMetrics.recordMetrics(
      response.status,
      response.rateLimit,
      response.rateLimitRemaining,
      response.rateLimitReset.getTime()
    )
    // @ts-ignore
    return super._errorFromResponse(response)
  }
}

const recurlySettings = Settings.apis.recurly
const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined

const client = new RecurlyClientWithErrorHandling(recurlyApiKey)

/**
 * Get account for a given user
 *
 * @param {string} userId
 * @return {Promise<PaymentProviderAccount | null>}
 */
async function getAccountForUserId(userId) {
  try {
    const account = await client.getAccount(`code-${userId}`)
    return accountFromApi(account)
  } catch (err) {
    if (err instanceof recurly.errors.NotFoundError) {
      // An expected error, we don't need to handle it, just return nothing
      logger.debug({ userId }, 'no recurly account found for user')
      return null
    } else {
      throw err
    }
  }
}

/**
 * @param {any} userId
 */
async function createAccountForUserId(userId) {
  const user = await UserGetter.promises.getUser(userId, {
    _id: 1,
    first_name: 1,
    last_name: 1,
    email: 1,
  })
  const accountCreate = {
    code: user._id.toString(),
    email: user.email,
    firstName: user.first_name,
    lastName: user.last_name,
  }
  const account = await client.createAccount(accountCreate)
  logger.debug({ userId, account }, 'created recurly account')
  return accountFromApi(account)
}

/**
 * Get active coupons for a given user
 *
 * @param {string} userId
 * @return {Promise<PaymentProviderCoupon[]>}
 */
async function getActiveCouponsForUserId(userId) {
  try {
    const redemptions = await client.listActiveCouponRedemptions(
      `code-${userId}`
    )

    const coupons = []
    for await (const redemption of redemptions.each()) {
      coupons.push(couponFromApi(redemption))
    }

    return coupons
  } catch (err) {
    // An expected error if no coupons have been redeemed
    if (err instanceof recurly.errors.NotFoundError) {
      return []
    } else {
      throw err
    }
  }
}

/**
 * Get hosted customer management link
 *
 * @param {string} userId
 * @param {string} pageType
 * @return {Promise<string|null>}
 */
async function getCustomerManagementLink(userId, pageType) {
  try {
    const account = await client.getAccount(`code-${userId}`)
    const recurlySubdomain = Settings.apis.recurly.subdomain
    const hostedLoginToken = account.hostedLoginToken
    if (!hostedLoginToken) {
      throw new OError('recurly account does not have hosted login token')
    }
    let path = ''
    if (pageType === 'billing-details') {
      path = 'billing_info/edit?ht='
    }
    return [
      'https://',
      recurlySubdomain,
      '.recurly.com/account/',
      path,
      hostedLoginToken,
    ].join('')
  } catch (err) {
    if (err instanceof recurly.errors.NotFoundError) {
      // An expected error, we don't need to handle it, just return nothing
      logger.debug({ userId }, 'no recurly account found for user')
      return null
    } else {
      throw err
    }
  }
}

/**
 * Get a subscription from Recurly
 *
 * @param {string} subscriptionId
 * @return {Promise<PaymentProviderSubscription>}
 */
async function getSubscription(subscriptionId) {
  const subscription = await client.getSubscription(`uuid-${subscriptionId}`)
  return subscriptionFromApi(subscription)
}

/**
 * Get the subscription for a given user
 *
 * Returns null if the user doesn't have an account or a subscription. Throws an
 * error if the user has more than one subscription.
 *
 * @param {string} userId
 * @return {Promise<PaymentProviderSubscription | null>}
 */
async function getSubscriptionForUser(userId) {
  try {
    const subscriptions = client.listAccountSubscriptions(`code-${userId}`, {
      params: { state: 'active', limit: 2 },
    })

    let result = null

    // The async iterator returns a NotFoundError if the account doesn't exist.
    for await (const subscription of subscriptions.each()) {
      if (result != null) {
        throw new OError('User has more than one Recurly subscription', {
          userId,
        })
      }
      result = subscription
    }
    if (result == null) {
      return null
    }
    return subscriptionFromApi(result)
  } catch (err) {
    if (err instanceof recurly.errors.NotFoundError) {
      return null
    } else {
      throw err
    }
  }
}

/**
 * Request a subscription update from Recurly
 *
 * @param {PaymentProviderSubscriptionUpdateRequest} updateRequest
 */
async function updateSubscriptionDetails(updateRequest) {
  const body = subscriptionUpdateRequestToApi(updateRequest)

  const updatedSubscription = await client.updateSubscription(
    `uuid-${updateRequest.subscription.id}`,
    body
  )

  logger.debug(
    {
      subscriptionId: updateRequest.subscription.id,
      updateId: updatedSubscription.id,
    },
    'updated subscription'
  )
}

/**
 * Request a subscription change from Recurly
 *
 * @param {PaymentProviderSubscriptionChangeRequest} changeRequest
 */
async function applySubscriptionChangeRequest(changeRequest) {
  const body = subscriptionChangeRequestToApi(changeRequest)

  // capture pending change before immediate update, as it will be removed by Recurly
  const pendingChange =
    changeRequest.timeframe === 'now'
      ? changeRequest.subscription.pendingChange
      : null

  if (pendingChange != null) {
    logger.warn(
      {
        subscriptionId: changeRequest.subscription.id,
        timeframe: changeRequest.timeframe,
        pendingPlanCode: pendingChange.nextPlanCode,
        pendingAddOnCodes: pendingChange.nextAddOns.map(a => a.code),
      },
      'Applying immediate change to subscription with pending term_end change - will attempt to re-apply'
    )
  }

  try {
    const change = await client.createSubscriptionChange(
      `uuid-${changeRequest.subscription.id}`,
      body
    )
    logger.debug(
      { subscriptionId: changeRequest.subscription.id, changeId: change.id },
      'created subscription change'
    )

    if (pendingChange != null) {
      await _reapplyPendingChangeAfterImmediateUpdate(
        changeRequest.subscription.id,
        pendingChange
      )
    }
  } catch (err) {
    if (err instanceof recurly.errors.ValidationError) {
      /**
       * @type {{params?: { param?: string }[] | null}}
       */
      const validationError = err
      if (
        validationError.params?.some(
          p => p.param === 'subtotal_amount_in_cents'
        )
      ) {
        throw new SubtotalLimitExceededError(
          'Subtotal amount in cents exceeded error',
          {
            subscriptionId: changeRequest.subscription.id,
          }
        )
      }
    }
    throw err
  }
}

/**
 * Re-apply a pending term_end change after an immediate update.
 *
 * When an immediate change is applied to a subscription that has a pending
 * term_end change (e.g., scheduled add-on removal or plan downgrade), we need
 * to re-create the pending change with the correct items based on what the
 * pending change was trying to achieve and what happened in the immediate change.
 *
 * @param {string} subscriptionId
 * @param {PaymentProviderSubscriptionChange} pendingChange
 */
async function _reapplyPendingChangeAfterImmediateUpdate(
  subscriptionId,
  pendingChange
) {
  const updatedSubscription = await getSubscription(subscriptionId)

  const planCodeDiffers =
    pendingChange.nextPlanCode !== updatedSubscription.planCode

  const immediateChangeIncludedPlanChange =
    pendingChange.subscription.planCode !== updatedSubscription.planCode

  // Build add-on objects for comparison: { code: { quantity, unitPrice } }
  const preUpdateAddOns = pendingChange.subscription.addOns.reduce(
    (acc, addOn) => ({
      ...acc,
      [addOn.code]: { quantity: addOn.quantity, unitPrice: addOn.unitPrice },
    }),
    {}
  )
  const preUpdatePendingAddOns = pendingChange.nextAddOns.reduce(
    (acc, addOn) => ({
      ...acc,
      [addOn.code]: { quantity: addOn.quantity, unitPrice: addOn.unitPrice },
    }),
    {}
  )
  const postUpdateAddOns = updatedSubscription.addOns.reduce(
    (acc, addOn) => ({
      ...acc,
      [addOn.code]: { quantity: addOn.quantity, unitPrice: addOn.unitPrice },
    }),
    {}
  )

  // Merge: start with pending add-ons, add any new add-ons from immediate update
  /** @type {Record<string, any>} */
  const mergedAddOns = { ...preUpdatePendingAddOns }
  for (const [code, details] of Object.entries(postUpdateAddOns)) {
    // include any add-ons that were added via immediate update just now
    if (!(code in preUpdateAddOns) && !(code in mergedAddOns)) {
      mergedAddOns[code] = details
    }
  }

  const addOnsDiffer = !_.isEqual(mergedAddOns, postUpdateAddOns)

  if (!planCodeDiffers && !addOnsDiffer) {
    logger.debug(
      { subscriptionId },
      'No pending changes to re-apply after immediate update'
    )
    return
  }

  // only re-apply the pending plan change if:
  // 1. immediate change didn't include a plan change (was add-on only)
  // 2. pending plan actually differs from the current plan
  // If the immediate change was a plan change, the user's new plan choice supersedes the old pending plan.
  const shouldReapplyPlanCode =
    !immediateChangeIncludedPlanChange && planCodeDiffers

  logger.debug(
    {
      subscriptionId,
      shouldReapplyPlanCode,
      shouldReapplyAddOns: addOnsDiffer,
    },
    're-applying pending term_end change after immediate update'
  )

  /** @type {recurly.SubscriptionChangeCreate} */
  const requestBody = {
    timeframe: 'term_end',
    addOns: Object.entries(mergedAddOns).map(([code, details]) => ({
      code,
      quantity: details.quantity,
      unitAmount: details.unitPrice,
    })),
  }

  if (shouldReapplyPlanCode) {
    requestBody.planCode = pendingChange.nextPlanCode
  }

  const change = await client.createSubscriptionChange(
    `uuid-${subscriptionId}`,
    requestBody
  )

  logger.debug(
    { subscriptionId, changeId: change.id },
    're-applied pending term_end change'
  )
}

/**
 * Preview a subscription change
 *
 * @param {PaymentProviderSubscriptionChangeRequest} changeRequest
 * @return {Promise<PaymentProviderSubscriptionChange>}
 */
async function previewSubscriptionChange(changeRequest) {
  const body = subscriptionChangeRequestToApi(changeRequest)

  try {
    const subscriptionChange = await client.previewSubscriptionChange(
      `uuid-${changeRequest.subscription.id}`,
      body
    )

    return subscriptionChangeFromApi(
      changeRequest.subscription,
      subscriptionChange
    )
  } catch (err) {
    if (err instanceof recurly.errors.ValidationError) {
      /**
       * @type {{params?: { param?: string }[] | null}}
       */
      const validationError = err
      if (
        validationError.params?.some(
          p => p.param === 'subtotal_amount_in_cents'
        )
      ) {
        throw new SubtotalLimitExceededError(
          'Subtotal amount in cents exceeded error',
          {
            subscriptionId: changeRequest.subscription.id,
          }
        )
      }
    }
    throw err
  }
}

/**
 * @param {any} subscriptionId
 */
async function removeSubscriptionChange(subscriptionId) {
  const removed = await client.removeSubscriptionChange(subscriptionId)
  logger.debug({ subscriptionId }, 'removed pending subscription change')
  return removed
}

/**
 * @param {any} subscriptionUuid
 */
async function removeSubscriptionChangeByUuid(subscriptionUuid) {
  return await removeSubscriptionChange('uuid-' + subscriptionUuid)
}

/**
 * @param {any} subscriptionUuid
 */
async function reactivateSubscriptionByUuid(subscriptionUuid) {
  return await client.reactivateSubscription('uuid-' + subscriptionUuid)
}

/**
 * @param {any} subscriptionUuid
 */
async function cancelSubscriptionByUuid(subscriptionUuid) {
  try {
    return await client.cancelSubscription('uuid-' + subscriptionUuid)
  } catch (err) {
    if (!(err instanceof recurly.errors.ValidationError)) {
      throw err
    }

    const errorMessage = err.message || ''

    if (
      errorMessage === 'Only active and future subscriptions can be canceled.'
    ) {
      logger.debug(
        { subscriptionUuid },
        'subscription cancellation failed, subscription not active'
      )
    } else if (
      errorMessage.includes(
        'Cannot cancel a paused subscription in the last cycle of the term'
      )
    ) {
      logger.debug(
        { subscriptionUuid },
        'Terminating subscription in last cycle of paused term'
      )
      return await terminateSubscriptionByUuid(subscriptionUuid)
    }

    throw err
  }
}

/**
 * @param {any} subscriptionUuid
 * @param {any} pauseCycles
 */
async function pauseSubscriptionByUuid(subscriptionUuid, pauseCycles) {
  return await client.pauseSubscription('uuid-' + subscriptionUuid, {
    remainingPauseCycles: pauseCycles,
  })
}

/**
 * @param {any} subscriptionUuid
 */
async function resumeSubscriptionByUuid(subscriptionUuid) {
  return await client.resumeSubscription('uuid-' + subscriptionUuid)
}

/**
 * Get the payment method for the given user
 *
 * @param {string} userId
 * @return {Promise<PaymentMethod>}
 */
async function getPaymentMethod(userId) {
  let billingInfo

  try {
    billingInfo = await client.getBillingInfo(`code-${userId}`)
  } catch (error) {
    if (error instanceof recurly.errors.NotFoundError) {
      throw new MissingBillingInfoError('This account has no billing info', {
        userId,
      })
    }
    throw error
  }

  return paymentMethodFromApi(billingInfo)
}

/**
 * Get the configuration for a given add-on
 *
 * @param {string} planCode
 * @param {string} addOnCode
 * @return {Promise<PaymentProviderAddOn>}
 */
async function getAddOn(planCode, addOnCode) {
  const addOn = await client.getPlanAddOn(
    `code-${planCode}`,
    `code-${addOnCode}`
  )
  return addOnFromApi(addOn)
}

/**
 * Get the configuration for a given plan
 *
 * @param {string} planCode
 * @return {Promise<PaymentProviderPlan>}
 */
async function getPlan(planCode) {
  const plan = await client.getPlan(`code-${planCode}`)
  return planFromApi(plan)
}

/**
 * @param {any} subscription
 */
function subscriptionIsCanceledOrExpired(subscription) {
  const state = subscription?.recurlyStatus?.state
  return state === 'canceled' || state === 'expired'
}

/**
 * Build a PaymentProviderAccount from Recurly API data
 *
 * @param {recurly.Account} apiAccount
 * @return {PaymentProviderAccount}
 */
function accountFromApi(apiAccount) {
  if (apiAccount.code == null || apiAccount.email == null) {
    throw new OError('Invalid Recurly account', {
      account: apiAccount,
    })
  }
  return new PaymentProviderAccount({
    code: apiAccount.code,
    email: apiAccount.email,
    hasPastDueInvoice: apiAccount.hasPastDueInvoice ?? false,
  })
}

/**
 * Build a PaymentProviderCoupon from Recurly API data
 *
 * @param {recurly.CouponRedemption} apiRedemption
 * @return {PaymentProviderCoupon}
 */
function couponFromApi(apiRedemption) {
  if (apiRedemption.coupon == null || apiRedemption.coupon.code == null) {
    throw new OError('Invalid Recurly coupon', {
      coupon: apiRedemption,
    })
  }
  return new PaymentProviderCoupon({
    code: apiRedemption.coupon.code,
    name: apiRedemption.coupon.name ?? '',
    description: apiRedemption.coupon.hostedPageDescription ?? '',
  })
}

/**
 * Build a PaymentProviderSubscription from Recurly API data
 *
 * @param {recurly.Subscription} apiSubscription
 * @return {PaymentProviderSubscription}
 */
function subscriptionFromApi(apiSubscription) {
  if (
    apiSubscription.uuid == null ||
    apiSubscription.plan == null ||
    apiSubscription.plan.code == null ||
    apiSubscription.plan.name == null ||
    apiSubscription.account == null ||
    apiSubscription.account.code == null ||
    apiSubscription.unitAmount == null ||
    apiSubscription.subtotal == null ||
    apiSubscription.total == null ||
    apiSubscription.currency == null ||
    apiSubscription.currentPeriodStartedAt == null ||
    apiSubscription.currentPeriodEndsAt == null ||
    apiSubscription.collectionMethod == null ||
    apiSubscription.netTerms == null ||
    // The values below could be null initially if the subscription has never updated
    !('poNumber' in apiSubscription) ||
    !('termsAndConditions' in apiSubscription)
  ) {
    throw new OError('Invalid Recurly subscription', {
      subscription: apiSubscription,
    })
  }

  const subscription = new PaymentProviderSubscription({
    id: apiSubscription.uuid,
    userId: apiSubscription.account.code,
    planCode: apiSubscription.plan.code,
    planName: apiSubscription.plan.name,
    planPrice: apiSubscription.unitAmount,
    addOns: (apiSubscription.addOns ?? []).map(subscriptionAddOnFromApi),
    subtotal: apiSubscription.subtotal,
    taxRate: apiSubscription.taxInfo?.rate ?? 0,
    taxAmount: apiSubscription.tax ?? 0,
    total: apiSubscription.total,
    currency: /** @type {CurrencyCode} */ (apiSubscription.currency),
    periodStart: apiSubscription.currentPeriodStartedAt,
    periodEnd: apiSubscription.currentPeriodEndsAt,
    collectionMethod: apiSubscription.collectionMethod,
    netTerms: apiSubscription.netTerms ?? 0,
    poNumber: apiSubscription.poNumber ?? '',
    termsAndConditions: apiSubscription.termsAndConditions ?? '',
    service: 'recurly',
    state: apiSubscription.state ?? 'active',
    trialPeriodStart: apiSubscription.trialStartedAt,
    trialPeriodEnd: apiSubscription.trialEndsAt,
    pausePeriodStart: apiSubscription.pausedAt,
    remainingPauseCycles: apiSubscription.remainingPauseCycles,
  })

  if (apiSubscription.pendingChange != null) {
    subscription.pendingChange = subscriptionChangeFromApi(
      subscription,
      apiSubscription.pendingChange
    )
  }

  return subscription
}

/**
 * Build a PaymentProviderSubscriptionAddOn from Recurly API data
 *
 * @param {recurly.SubscriptionAddOn} addOn
 * @return {PaymentProviderSubscriptionAddOn}
 */
function subscriptionAddOnFromApi(addOn) {
  if (
    addOn.addOn == null ||
    addOn.addOn.code == null ||
    addOn.addOn.name == null ||
    addOn.unitAmount == null
  ) {
    throw new OError('Invalid Recurly add-on', { addOn })
  }

  return new PaymentProviderSubscriptionAddOn({
    code: addOn.addOn.code,
    name: addOn.addOn.name,
    quantity: addOn.quantity ?? 1,
    unitPrice: addOn.unitAmount,
  })
}

/**
 * Build a PaymentProviderSubscriptionChange from Recurly API data
 *
 * @param {PaymentProviderSubscription} subscription - the current subscription
 * @param {recurly.SubscriptionChange} subscriptionChange - the subscription change returned from the API
 * @return {PaymentProviderSubscriptionChange}
 */
function subscriptionChangeFromApi(subscription, subscriptionChange) {
  if (
    subscriptionChange.plan == null ||
    subscriptionChange.plan.code == null ||
    subscriptionChange.plan.name == null ||
    subscriptionChange.unitAmount == null
  ) {
    throw new OError('Invalid Recurly subscription change', {
      subscriptionChange,
    })
  }
  const nextAddOns = (subscriptionChange.addOns ?? []).map(
    subscriptionAddOnFromApi
  )

  return new PaymentProviderSubscriptionChange({
    subscription,
    nextPlanCode: subscriptionChange.plan.code,
    nextPlanName: subscriptionChange.plan.name,
    nextPlanPrice: subscriptionChange.unitAmount,
    nextAddOns,
    immediateCharge: computeImmediateCharge(subscriptionChange),
  })
}

/**
 * Compute immediate charge based on invoice collection
 *
 * @param {recurly.SubscriptionChange} subscriptionChange - the subscription change returned from the API
 * @return {PaymentProviderImmediateCharge}
 */
function computeImmediateCharge(subscriptionChange) {
  const roundToTwoDecimal = (/** @type {number} */ num) =>
    Math.round(num * 100) / 100
  let subtotal =
    subscriptionChange.invoiceCollection?.chargeInvoice?.subtotal ?? 0
  let tax = subscriptionChange.invoiceCollection?.chargeInvoice?.tax ?? 0
  let total = subscriptionChange.invoiceCollection?.chargeInvoice?.total ?? 0
  let discount =
    subscriptionChange.invoiceCollection?.chargeInvoice?.discount ?? 0

  const lineItems = []

  for (const lineItem of subscriptionChange.invoiceCollection?.chargeInvoice
    ?.lineItems || []) {
    lineItems.push({
      planCode: lineItem.planCode,
      isAiAssist:
        lineItem.addOnCode === AI_ADD_ON_CODE ||
        isStandaloneAiAddOnPlanCode(lineItem.planCode),
      description: lineItem.description ?? '',
      subtotal: roundToTwoDecimal(lineItem.subtotal ?? 0),
      discount: roundToTwoDecimal(lineItem.discount ?? 0),
      tax: roundToTwoDecimal(lineItem.tax ?? 0),
    })
  }

  for (const creditInvoice of subscriptionChange.invoiceCollection
    ?.creditInvoices ?? []) {
    // The credit invoice numbers are already negative
    subtotal = roundToTwoDecimal(subtotal + (creditInvoice.subtotal ?? 0))
    total = roundToTwoDecimal(total + (creditInvoice.total ?? 0))
    // Tax rate can be different in credit invoice if a user relocates
    tax = roundToTwoDecimal(tax + (creditInvoice.tax ?? 0))
    discount = roundToTwoDecimal(discount + (creditInvoice.discount ?? 0))

    for (const lineItem of creditInvoice.lineItems || []) {
      lineItems.push({
        planCode: lineItem.planCode,
        isAiAssist:
          lineItem.addOnCode === AI_ADD_ON_CODE ||
          isStandaloneAiAddOnPlanCode(lineItem.planCode),
        description: lineItem.description ?? '',
        subtotal: roundToTwoDecimal(lineItem.subtotal ?? 0),
        discount: roundToTwoDecimal(lineItem.discount ?? 0),
        tax: roundToTwoDecimal(lineItem.tax ?? 0),
      })
    }
  }
  return new PaymentProviderImmediateCharge({
    subtotal,
    total,
    tax,
    discount,
    lineItems,
  })
}

/**
 * Returns a payment method from Recurly API data
 *
 * @param {recurly.BillingInfo} billingInfo
 * @return {PaymentMethod}
 */
function paymentMethodFromApi(billingInfo) {
  if (billingInfo.paymentMethod == null) {
    throw new OError('Invalid Recurly billing info', { billingInfo })
  }
  const paymentMethod = billingInfo.paymentMethod

  if (paymentMethod.billingAgreementId != null) {
    return new PaypalPaymentMethod()
  }

  if (paymentMethod.cardType == null || paymentMethod.lastFour == null) {
    throw new OError('Invalid Recurly billing info', { billingInfo })
  }
  return new CreditCardPaymentMethod({
    cardType: paymentMethod.cardType,
    lastFour: paymentMethod.lastFour,
  })
}

/**
 * Build a PaymentProviderAddOn from Recurly API data
 *
 * @param {recurly.AddOn} addOn
 * @return {PaymentProviderAddOn}
 */
function addOnFromApi(addOn) {
  if (addOn.code == null || addOn.name == null) {
    throw new OError('Invalid Recurly add-on', { addOn })
  }
  return new PaymentProviderAddOn({
    code: addOn.code,
    name: addOn.name,
  })
}

/**
 * Build a PaymentProviderPlan from Recurly API data
 *
 * @param {recurly.Plan} plan
 * @return {PaymentProviderPlan}
 */
function planFromApi(plan) {
  if (plan.code == null || plan.name == null) {
    throw new OError('Invalid Recurly add-on', { plan })
  }
  return new PaymentProviderPlan({
    code: plan.code,
    name: plan.name,
  })
}

/**
 * Build an API request from a PaymentProviderSubscriptionChangeRequest
 *
 * @param {PaymentProviderSubscriptionChangeRequest} changeRequest
 * @return {recurly.SubscriptionChangeCreate}
 */
function subscriptionChangeRequestToApi(changeRequest) {
  /** @type {recurly.SubscriptionChangeCreate} */
  const requestBody = {
    timeframe: changeRequest.timeframe,
  }
  if (changeRequest.planCode != null) {
    requestBody.planCode = changeRequest.planCode
  }
  if (changeRequest.addOnUpdates != null) {
    requestBody.addOns = changeRequest.addOnUpdates.map(addOnUpdate => {
      /** @type {recurly.SubscriptionAddOnUpdate} */
      const update = { code: addOnUpdate.code }
      if (addOnUpdate.quantity != null) {
        update.quantity = addOnUpdate.quantity
      }
      if (addOnUpdate.unitPrice != null) {
        update.unitAmount = addOnUpdate.unitPrice
      }
      return update
    })
  }
  return requestBody
}

/**
 * Build an API request from a PaymentProviderSubscriptionUpdateRequest
 *
 * @param {PaymentProviderSubscriptionUpdateRequest} updateRequest
 */
function subscriptionUpdateRequestToApi(updateRequest) {
  const requestBody = {}
  if (updateRequest.poNumber) {
    requestBody.poNumber = updateRequest.poNumber
  }
  if (updateRequest.termsAndConditions) {
    requestBody.termsAndConditions = updateRequest.termsAndConditions
  }
  return requestBody
}

/**
 * @param {any} subscriptionUuid
 */
async function terminateSubscriptionByUuid(subscriptionUuid) {
  const subscription = await client.terminateSubscription(
    'uuid-' + subscriptionUuid,
    {
      body: {
        refund: 'none',
      },
    }
  )

  logger.debug({ subscriptionUuid }, 'subscription terminated')

  return subscription
}

export default {
  errors: recurly.errors,

  getAccountForUserId: callbackify(getAccountForUserId),
  createAccountForUserId: callbackify(createAccountForUserId),
  getActiveCouponsForUserId: callbackify(getActiveCouponsForUserId),
  getSubscription: callbackify(getSubscription),
  getSubscriptionForUser: callbackify(getSubscriptionForUser),
  previewSubscriptionChange: callbackify(previewSubscriptionChange),
  updateSubscriptionDetails: callbackify(updateSubscriptionDetails),
  applySubscriptionChangeRequest: callbackify(applySubscriptionChangeRequest),
  removeSubscriptionChange: callbackify(removeSubscriptionChange),
  removeSubscriptionChangeByUuid: callbackify(removeSubscriptionChangeByUuid),
  reactivateSubscriptionByUuid: callbackify(reactivateSubscriptionByUuid),
  cancelSubscriptionByUuid: callbackify(cancelSubscriptionByUuid),
  getPaymentMethod: callbackify(getPaymentMethod),
  getAddOn: callbackify(getAddOn),
  getPlan: callbackify(getPlan),
  subscriptionIsCanceledOrExpired,
  pauseSubscriptionByUuid: callbackify(pauseSubscriptionByUuid),
  resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid),
  terminateSubscriptionByUuid: callbackify(terminateSubscriptionByUuid),

  promises: {
    getSubscription,
    getSubscriptionForUser,
    getAccountForUserId,
    createAccountForUserId,
    getActiveCouponsForUserId,
    getCustomerManagementLink,
    previewSubscriptionChange,
    updateSubscriptionDetails,
    applySubscriptionChangeRequest,
    removeSubscriptionChange,
    removeSubscriptionChangeByUuid,
    reactivateSubscriptionByUuid,
    cancelSubscriptionByUuid,
    pauseSubscriptionByUuid,
    resumeSubscriptionByUuid,
    getPaymentMethod,
    getAddOn,
    getPlan,
    terminateSubscriptionByUuid,
  },
}
