import { XMLParser, XMLBuilder } from 'fast-xml-parser'
import { getErrorDescription } from '../helpers'
import type {
  PaymentLink,
  OrderItem,
  CreateHashParams,
  Extra as ExtraType
} from './types'

// Use a simple cache for hash values to avoid recalculating
const hashCache = new Map<string, string>()

const createHash = async ({
  origin,
  clientId
}: CreateHashParams): Promise<string> => {
  const cacheKey = `${origin}-${clientId}`

  // Return cached value if available
  if (hashCache.has(cacheKey)) {
    return hashCache.get(cacheKey)!
  }

  const stringToHash = `${origin}-ORIGIN-${clientId}`
  const hashBuffer = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(stringToHash)
  )
  const hash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))

  // Cache the result
  hashCache.set(cacheKey, hash)
  return hash
}
// Optimized to avoid unnecessary object creation
const buildOrderItems = (items: OrderItem[]) =>
  items.map(({ id, itemnumber, productcode, qty, desc, price }) => {
    const orderItem: Record<string, any> = { OrderItem: {} }
    const item = orderItem.OrderItem

    if (id) item.id = id
    if (itemnumber) item.itemnumber = itemnumber
    if (productcode) item.productcode = productcode
    if (qty) item.qty = qty
    if (desc) item.desc = desc
    if (price) item.price = price
    if (price && qty) item.Total = price * qty

    return orderItem
  })

// More efficient implementation using for...of loop
const filterUndefinedValues = (obj: Record<string, any>) => {
  const result: Record<string, any> = {}

  for (const [key, value] of Object.entries(obj)) {
    if (value !== undefined) {
      result[key] = value
    }
  }

  return result
}

// Reuse parser and builder instances
const xmlParser = new XMLParser()
const xmlBuilder = new XMLBuilder({ format: true })

/**
 * Handles the creation of a payment link by interacting with the NestPay API.
 * @docs NestPay® | Merchant Integration API Manual. Page: 50
 *
 * @param {PaymentLink} param - The input parameters for the payment link creation.
 * @param {object} param.data - The data required for the payment link.
 * @param {string} [param.data.OrderId] - The unique identifier for the order.
 * @param {string} [param.data.Type='Auth'] - The type of transaction (e.g., 'Auth').
 * @param {number} [param.data.Total] - The total amount for the transaction.
 * @param {number} [param.data.Currency=941] - The currency code for the transaction (default is 941).
 * @param {number} [param.data.Instalment] - The number of instalments for the payment.
 * @param {OrderItem[]} [param.data.OrderItemList] - The list of items in the order.
 * @param {object} [param.data.PbOrder={ OrderType: 0 }] - Additional order details.
 * @param {object} [param.data.BillTo] - Billing information for the customer.
 * @param {ExtraType} [param.data.Extra={ PAYMENTLINKTYPE: 'SINGLE_LINK_PAYMENT' }] - Additional parameters for the payment link.
 * @param {object} param.env - The environment variables for the API interaction.
 * @param {string} param.env.CLIENT_USERNAME - The username for the NestPay client.
 * @param {string} param.env.CLIENT_PASSWORD - The password for the NestPay client.
 * @param {string} param.env.CLIENT_ID - The client ID for the NestPay client.
 * @param {string} param.env.NESTPAY_API_URL - The URL for the NestPay API.
 *
 * @returns {Promise<object>} The response data from the NestPay API.
 * @returns {object} [response.error] - The error details if the request fails.
 * @returns {string} [response.error.message] - The error message.
 * @returns {number} [response.error.status] - The HTTP status code of the error.
 * @returns {string} [response.error.statusText] - The HTTP status text of the error.
 * @returns {object} [responseData] - The parsed response data from the API.
 * @returns {string} [responseData.ProcReturnCode] - The processing return code from the API.
 * @returns {string} [responseData.OrderId] - The order ID returned by the API.
 * @returns {object} [responseData.Extra] - Additional response details.
 * @returns {string} [responseData.Extra.PAYMENTLINKEXPIRATIONDATE] - The expiration date of the payment link.
 * @returns {string} [responseData.Extra.PAYMENTLINKTOKEN] - The token for the payment link.
 * @returns {string} [responseData.Extra.PAYMENTLINKURL] - The URL for the payment link.
 * @returns {string} [responseData.Extra.PAYMENTLINKTYPE] - The type of the payment link.
 *
 * @throws {Error} If an unexpected error occurs during the process.
 */
export async function paymentLink({ data, env }: PaymentLink) {
  try {
    const {
      OrderId,
      Type = 'Auth',
      Total,
      Currency = 941,
      Instalment,
      OrderItemList,
      PbOrder = { OrderType: 0 },
      BillTo,
      Extra = {} as ExtraType
    } = data

    const {
      PAYMENTLINKTYPE = 'SINGLE_LINK_PAYMENT',
      PAYMENTLINKEXPIRY,
      PAYMENTLINKEXPIRYUNIT,
      PAYMENTLINKLANGUAGE,
      PAYMENTLINKAMOUNT_EDITABLE,
      PAYMENTLINKCUSTOMERPHONEEDITABLE,
      PAYMENTLINKITEMIDEDITABLE,
      PAYMENTLINKCUSTOMERNAMEEDITABLE,
      PAYMENTLINKADDRESS_EDITABLE,
      PAYMENTLINKITEMDESCRIPTIONEDITABLE,
      PAYMENTLINKCUSTOMEREMAILEDITABLE
    } = Extra

    const origin = PAYMENTLINKTYPE === 'SINGLE_LINK_PAYMENT' ? 'SPL' : 'MPL'
    const ORIGIN = await createHash({
      origin,
      clientId: env.CLIENT_ID
    })

    // Build Extra object more efficiently
    const extraObj: Record<string, any> = {
      PAYMENTLINKTYPE,
      ORIGIN
    }

    if (PAYMENTLINKEXPIRY) extraObj.PAYMENTLINKEXPIRY = PAYMENTLINKEXPIRY
    if (PAYMENTLINKEXPIRYUNIT)
      extraObj.PAYMENTLINKEXPIRYUNIT = PAYMENTLINKEXPIRYUNIT
    if (PAYMENTLINKLANGUAGE) extraObj.PAYMENTLINKLANGUAGE = PAYMENTLINKLANGUAGE
    if (PAYMENTLINKAMOUNT_EDITABLE)
      extraObj.PAYMENTLINKAMOUNT_EDITABLE = PAYMENTLINKAMOUNT_EDITABLE
    if (PAYMENTLINKITEMIDEDITABLE)
      extraObj.PAYMENTLINKITEMIDEDITABLE = PAYMENTLINKITEMIDEDITABLE
    if (PAYMENTLINKCUSTOMERPHONEEDITABLE)
      extraObj.PAYMENTLINKCUSTOMERPHONEEDITABLE =
        PAYMENTLINKCUSTOMERPHONEEDITABLE
    if (PAYMENTLINKCUSTOMERNAMEEDITABLE)
      extraObj.PAYMENTLINKCUSTOMERNAMEEDITABLE = PAYMENTLINKCUSTOMERNAMEEDITABLE
    if (PAYMENTLINKADDRESS_EDITABLE)
      extraObj.PAYMENTLINKADDRESS_EDITABLE = PAYMENTLINKADDRESS_EDITABLE
    if (PAYMENTLINKITEMDESCRIPTIONEDITABLE)
      extraObj.PAYMENTLINKITEMDESCRIPTIONEDITABLE =
        PAYMENTLINKITEMDESCRIPTIONEDITABLE
    if (PAYMENTLINKCUSTOMEREMAILEDITABLE)
      extraObj.PAYMENTLINKCUSTOMEREMAILEDITABLE =
        PAYMENTLINKCUSTOMEREMAILEDITABLE

    // Build request object more efficiently
    const cc5Request: Record<string, any> = {
      Name: env.CLIENT_USERNAME,
      Password: env.CLIENT_PASSWORD,
      ClientId: env.CLIENT_ID,
      Type,
      Currency,
      Extra: extraObj
    }

    if (PAYMENTLINKTYPE === 'SINGLE_LINK_PAYMENT' && OrderId) {
      cc5Request.OrderId = OrderId
    }
    if (Instalment) cc5Request.Instalment = Instalment
    if (Total) cc5Request.Total = Total
    if (PbOrder) cc5Request.PbOrder = filterUndefinedValues(PbOrder)
    if (BillTo) cc5Request.BillTo = filterUndefinedValues(BillTo)
    if (OrderItemList?.length) {
      cc5Request.OrderItemList = buildOrderItems(OrderItemList)
    }

    const xmlData = { CC5Request: cc5Request }
    const xmlContent = xmlBuilder.build(xmlData)

    const response = await fetch(env.NESTPAY_API_URL, {
      method: 'POST',
      headers: {
        'content-type': 'text/xml; charset=utf-8',
        Accept: '*/*'
      },
      body: xmlContent
    })

    const responseText = await response.text()
    const parsed = xmlParser.parse(responseText)
    const responseData = parsed?.CC5Response || {}

    if (responseData.ProcReturnCode === '99') {
      return {
        error: {
          ...getErrorDescription(responseData.Extra.ERRORCODE),
          ...(responseData.OrderId && { OrderId: responseData.OrderId })
        }
      }
    }
    return responseData
  } catch (error) {
    return {
      error: {
        message:
          error instanceof Error ? error.message : 'Unknown error occurred',
        status: 500,
        statusText: 'Internal Server Error'
      }
    }
  }
}