/* eslint-disable max-lines */
import { Platform } from 'react-native';
import uuidModule from 'react-native-uuid';

import { emptyFn, isDefined, isEmptyArray, isError, isNotEmptyArray, isNotEmptyString } from '@rnw-community/shared';

import { AndroidPaymentMethodTokenizationType } from '../../@standard/android/enum/android-payment-method-tokenization-type.enum.js';
import { defaultAndroidPaymentDataRequest } from '../../@standard/android/request/android-payment-data-request.js';
import { defaultAndroidPaymentMethod } from '../../@standard/android/request/android-payment-method.js';
import { defaultAndroidTransactionInfo } from '../../@standard/android/request/android-transaction-info.js';
import { IOSPKContactField } from '../../@standard/ios/enum/ios-pk-contact-field.enum.js';
import { IosPKMerchantCapability } from '../../@standard/ios/enum/ios-pk-merchant-capability.enum.js';
import { IosPKPaymentNetworksEnum } from '../../@standard/ios/enum/ios-pk-payment-networks.enum.js';
import { PaymentMethodNameEnum } from '../../enum/payment-method-name.enum.js';
import { PaymentsErrorEnum } from '../../enum/payments-error.enum.js';
import { SupportedNetworkEnum } from '../../enum/supported-networks.enum.js';
import { ConstructorError } from '../../error/constructor.error.js';
import { DOMException } from '../../error/dom.exception.js';
import { PaymentsError } from '../../error/payments.error.js';
import { getNativePaymentsEventEmitter } from '../../util/get-native-payments-event-emitter/get-native-payments-event-emitter.util.js';
import { isNativeUserCancellation } from '../../util/is-native-user-cancellation.util.js';
import { resolvePaymentDetailsModifier } from '../../util/resolve-payment-details-modifier.util.js';
import { validateAndroidTransactionInfo } from '../../util/validate-android-transaction-info.util.js';
import { validateDetailsUpdate } from '../../util/validate-details-update.util.js';
import { validateDisplayItems } from '../../util/validate-display-items.util.js';
import { validateModifiers } from '../../util/validate-modifiers.util.js';
import { validatePaymentMethods } from '../../util/validate-payment-methods.util.js';
import { validateShippingOptions } from '../../util/validate-shipping-options.util.js';
import { validateShippingType } from '../../util/validate-shipping-type.util.js';
import { validateTotal } from '../../util/validate-total.util.js';
import { warnChangeEventError } from '../../util/warn-change-event-error.util.js';
import { ChangeEventDispatcher } from '../change-event-dispatcher/change-event-dispatcher.js';
import { NativePayments } from '../native-payments/native-payments.js';
import { AndroidPaymentResponse } from '../payment-response/android-payment-response.js';
import { IosPaymentResponse } from '../payment-response/ios-payment-response.js';

import type { AndroidAllowedCardNetworksEnum } from '../../@standard/android/enum/android-allowed-card-networks.enum.js';
import type { AndroidPaymentMethodDataDataInterface } from '../../@standard/android/mapping/android-payment-method-data-data.interface.js';
import type { AndroidPaymentDataRequest } from '../../@standard/android/request/android-payment-data-request.js';
import type { IosPaymentMethodDataDataInterface } from '../../@standard/ios/mapping/ios-payment-method-data-data.interface.js';
import type { IosPaymentDataRequest } from '../../@standard/ios/request/ios-payment-data-request.js';
import type { PaymentDetailsInit } from '../../@standard/w3c/payment-details-init.js';
import type { PaymentDetailsUpdate } from '../../@standard/w3c/payment-details-update.js';
import type { PaymentItem } from '../../@standard/w3c/payment-item.js';
import type { PaymentMethodData } from '../../@standard/w3c/payment-method-data.js';
import type { NativePaymentDetailsUpdateInterface } from '../../interface/native-payment-details-update.interface.js';
import type { PaymentRequestEventPayloadInterface } from '../../interface/payment-request-event-payload.interface.js';
import type { PaymentRequestEventRegistrationInterface } from '../../interface/payment-request-event-registration.interface.js';
import type { PaymentResponseAddressInterface } from '../../interface/payment-response-address.interface.js';
import type { ResolvedPaymentDetailsInterface } from '../../interface/resolved-payment-details.interface.js';
import type { PaymentMethodChangeEventListener } from '../../type/payment-method-change-event-listener.type.js';
import type { PaymentRequestEventListener } from '../../type/payment-request-event-listener.type.js';
import type { PaymentRequestEventType } from '../../type/payment-request-event.type.js';
import type { PaymentRequestUpdateEvent } from '../payment-request-update-event/payment-request-update-event.js';
import type { Maybe } from '@rnw-community/shared';
import type { EmitterSubscription } from 'react-native';

const uuid = uuidModule as unknown as { v4: () => string };

/*
 * HINT: Troubleshooting: https://developers.google.com/pay/api/android/support/troubleshooting
 * HINT: Google Pay API Errors: https://developers.google.com/pay/api/web/reference/error-objects
 */
export class PaymentRequest {
    // https://www.w3.org/TR/payment-request/#id-attribute
    readonly id: string;
    updating = false;
    state: 'closed' | 'created' | 'interactive' = 'created';
    couponCode: Maybe<string> = null;
    shippingAddress: Maybe<PaymentResponseAddressInterface> = null;
    shippingOption: Maybe<string> = null;

    // Internal Slots https://www.w3.org/TR/payment-request/#internal-slots
    private readonly serializedMethodData: string;
    private readonly platformMethodData: AndroidPaymentMethodDataDataInterface | IosPaymentMethodDataDataInterface;
    private readonly eventRegistrations = new Map<PaymentRequestEventType, PaymentRequestEventRegistrationInterface>();
    private readonly pendingDispatchers = new Set<ChangeEventDispatcher>();
    private readonly attributeHandlers = new Map<
        PaymentRequestEventType,
        PaymentMethodChangeEventListener | PaymentRequestEventListener
    >();
    private readonly attributeHandlerWrappers = new Map<PaymentRequestEventType, PaymentRequestEventListener>();
    private eventGeneration = 0;
    private isNativeEventsSynced = false;

    private acceptPromiseRejecter: (reason: unknown) => void = emptyFn;

    constructor(
        readonly methodData: PaymentMethodData[],
        public details: PaymentDetailsInit
    ) {
        // 3. Establish the request's id:
        if (!isNotEmptyString(details.id)) {
            // TODO: Can we avoid using external lib? Use Math.random?

            details.id = uuid.v4();
        }
        this.id = details.id;

        this.validateConstructorInputs(methodData, details);

        // 17. Set request.[[serializedMethodData]] to serializedMethodData.         */
        this.platformMethodData = this.findPlatformPaymentMethodData();

        const nativePlatformMethodData =
            Platform.OS === 'android'
                ? this.getAndroidPaymentMethodData(
                      this.platformMethodData as AndroidPaymentMethodDataDataInterface,
                      this.resolveEffectiveDetails(details).total
                  )
                : this.getIosPaymentMethodData(this.platformMethodData as IosPaymentMethodDataDataInterface);

        this.serializedMethodData = JSON.stringify(nativePlatformMethodData);
    }

    // https://www.w3.org/TR/payment-request/#dom-paymentrequest-onshippingaddresschange
    get onshippingaddresschange(): Maybe<PaymentRequestEventListener> {
        return this.getAttributeHandler('shippingaddresschange') as Maybe<PaymentRequestEventListener>;
    }

    // https://www.w3.org/TR/payment-request/#dom-paymentrequest-onshippingoptionchange
    get onshippingoptionchange(): Maybe<PaymentRequestEventListener> {
        return this.getAttributeHandler('shippingoptionchange') as Maybe<PaymentRequestEventListener>;
    }

    // https://www.w3.org/TR/payment-request/#dom-paymentrequest-onpaymentmethodchange
    get onpaymentmethodchange(): Maybe<PaymentMethodChangeEventListener> {
        return this.getAttributeHandler('paymentmethodchange');
    }

    // couponcodechange is a PassKit extension: https://developer.apple.com/documentation/passkit/pkpaymentrequest/3801275-couponcode?language=objc
    get oncouponcodechange(): Maybe<PaymentRequestEventListener> {
        return this.getAttributeHandler('couponcodechange') as Maybe<PaymentRequestEventListener>;
    }

    set onshippingaddresschange(listener: Maybe<PaymentRequestEventListener>) {
        this.setAttributeHandler('shippingaddresschange', listener);
    }

    set onshippingoptionchange(listener: Maybe<PaymentRequestEventListener>) {
        this.setAttributeHandler('shippingoptionchange', listener);
    }

    set onpaymentmethodchange(listener: Maybe<PaymentMethodChangeEventListener>) {
        this.setAttributeHandler('paymentmethodchange', listener);
    }

    set oncouponcodechange(listener: Maybe<PaymentRequestEventListener>) {
        this.setAttributeHandler('couponcodechange', listener);
    }

    // https://www.w3.org/TR/payment-request/#canmakepayment-method
    async canMakePayment(): Promise<boolean> {
        if (this.state !== 'created') {
            throw new DOMException(PaymentsErrorEnum.InvalidStateError);
        }

        return NativePayments.canMakePayments(this.serializedMethodData);
    }

    // https://www.w3.org/TR/payment-request/#hasenrolledinstrument-method
    async hasEnrolledInstrument(): Promise<boolean> {
        if (this.state !== 'created') {
            throw new DOMException(PaymentsErrorEnum.InvalidStateError);
        }

        return NativePayments.hasEnrolledInstrument(this.serializedMethodData);
    }

    // https://www.w3.org/TR/payment-request/#show-method
    show(): Promise<AndroidPaymentResponse | IosPaymentResponse> {
        if (this.state !== 'created') {
            return Promise.reject(new DOMException(PaymentsErrorEnum.InvalidStateError));
        }

        this.state = 'interactive';
        this.syncActiveEvents();

        const resolvedDetails = this.resolveEffectiveDetails(this.details);

        // HINT: We need to pass Android environment configuration to native module via details
        const details =
            Platform.OS === 'android'
                ? {
                      ...this.details,
                      environment: (this.platformMethodData as AndroidPaymentMethodDataDataInterface).environment,
                      ...resolvedDetails,
                  }
                : { ...this.details, ...resolvedDetails };

        return new Promise<AndroidPaymentResponse | IosPaymentResponse>((resolve, reject) => {
            this.acceptPromiseRejecter = reject;

            NativePayments.show(this.id, this.serializedMethodData, details)
                .then(jsonDetails => {
                    const paymentResponse = this.handleAccept(jsonDetails);

                    this.closeRequest();
                    resolve(paymentResponse);

                    return void 0;
                })
                .catch((error: unknown) => {
                    this.closeRequest();

                    if (isNativeUserCancellation(error)) {
                        reject(new DOMException(PaymentsErrorEnum.AbortError));

                        return;
                    }

                    reject(isError(error) ? error : new PaymentsError(`Failed showing PaymentRequest`));
                });
        });
    }

    // https://www.w3.org/TR/payment-request/#abort-method
    async abort(): Promise<void> {
        if (this.state !== 'interactive') {
            throw new DOMException(PaymentsErrorEnum.InvalidStateError);
        }

        await NativePayments.abort().catch(() => {
            throw new PaymentsError(`Failed aborting PaymentRequest`);
        });

        this.closeRequest();
        this.acceptPromiseRejecter(new DOMException(PaymentsErrorEnum.AbortError));
    }

    addEventListener(type: 'paymentmethodchange', listener: PaymentMethodChangeEventListener): void;
    addEventListener(type: PaymentRequestEventType, listener: PaymentRequestEventListener): void;
    addEventListener(
        type: PaymentRequestEventType,
        eventListener: PaymentMethodChangeEventListener | PaymentRequestEventListener
    ): void {
        if (this.state === 'closed') {
            return;
        }

        const listener = eventListener as PaymentRequestEventListener;
        const registration = this.eventRegistrations.get(type);

        if (isDefined(registration)) {
            if (!registration.listeners.includes(listener)) {
                registration.listeners.push(listener);
            }

            return;
        }

        const listeners = [listener];

        this.eventRegistrations.set(type, { listeners, subscription: this.subscribeToNativeEvent(type, listeners) });
        this.syncActiveEvents();
    }

    removeEventListener(type: 'paymentmethodchange', listener: PaymentMethodChangeEventListener): void;
    removeEventListener(type: PaymentRequestEventType, listener: PaymentRequestEventListener): void;
    removeEventListener(
        type: PaymentRequestEventType,
        eventListener: PaymentMethodChangeEventListener | PaymentRequestEventListener
    ): void {
        const registration = this.eventRegistrations.get(type);

        if (!isDefined(registration)) {
            return;
        }

        this.forgetListener(registration, eventListener as PaymentRequestEventListener);

        if (isNotEmptyArray(registration.listeners)) {
            return;
        }

        this.dropRegistration(type, registration);
    }

    private closeRequest(): void {
        this.state = 'closed';
        this.clearEventRegistrations();
    }

    private validateConstructorInputs(methodData: PaymentMethodData[], details: PaymentDetailsInit): void {
        // 4. Process payment methods
        validatePaymentMethods(methodData);
        validateAndroidTransactionInfo(methodData, ConstructorError);
        validateShippingType(methodData, ConstructorError);

        // 5. Process the total
        validateTotal(details.total, ConstructorError);

        // 6. If the displayItems member of details is present, then for each item in details.displayItems:
        validateDisplayItems(ConstructorError, details.displayItems);

        validateShippingOptions(ConstructorError, details.shippingOptions);
        validateModifiers(ConstructorError, details.modifiers);
    }

    private getAttributeHandler(
        type: PaymentRequestEventType
    ): Maybe<PaymentMethodChangeEventListener | PaymentRequestEventListener> {
        return this.attributeHandlers.get(type) ?? null;
    }

    private setAttributeHandler(
        type: PaymentRequestEventType,
        listener: Maybe<PaymentMethodChangeEventListener | PaymentRequestEventListener>
    ): void {
        if (!isDefined(listener)) {
            this.attributeHandlers.delete(type);
            this.removeAttributeHandlerWrapper(type);

            return;
        }

        this.attributeHandlers.set(type, listener);
        this.ensureAttributeHandlerWrapper(type);
    }

    private ensureAttributeHandlerWrapper(type: PaymentRequestEventType): void {
        if (this.attributeHandlerWrappers.has(type)) {
            return;
        }

        const wrapper: PaymentRequestEventListener = event => this.dispatchToAttributeHandler(type, event);

        this.attributeHandlerWrappers.set(type, wrapper);
        this.addEventListener(type, wrapper);
    }

    private removeAttributeHandlerWrapper(type: PaymentRequestEventType): void {
        const wrapper = this.attributeHandlerWrappers.get(type);

        if (!isDefined(wrapper)) {
            return;
        }

        this.attributeHandlerWrappers.delete(type);
        this.removeEventListener(type, wrapper);
    }

    private dispatchToAttributeHandler(
        type: PaymentRequestEventType,
        event: PaymentRequestUpdateEvent
    ): Promise<void> | void {
        return (this.attributeHandlers.get(type) as PaymentRequestEventListener)(event);
    }

    private handleAccept(details: string): AndroidPaymentResponse | IosPaymentResponse {
        return Platform.OS === 'android'
            ? new AndroidPaymentResponse(this.id, PaymentMethodNameEnum.AndroidPay, details, this.shippingOption)
            : new IosPaymentResponse(this.id, PaymentMethodNameEnum.ApplePay, details, this.shippingOption);
    }

    private subscribeToNativeEvent(
        type: PaymentRequestEventType,
        listeners: PaymentRequestEventListener[]
    ): Maybe<EmitterSubscription> {
        const eventEmitter = getNativePaymentsEventEmitter();

        if (!isDefined(eventEmitter)) {
            return null;
        }

        return eventEmitter.addListener(type, (payload: PaymentRequestEventPayloadInterface) => {
            this.handleChangeEvent(type, listeners, payload).catch(warnChangeEventError);
        });
    }

    private dropRegistration(type: PaymentRequestEventType, registration: PaymentRequestEventRegistrationInterface): void {
        if (isDefined(registration.subscription)) {
            registration.subscription.remove();
        }

        this.eventRegistrations.delete(type);
        this.syncActiveEvents();
    }

    private syncActiveEvents(): void {
        const eventNames = [...this.eventRegistrations.keys()];
        const { setActiveEvents } = NativePayments;

        if (!isDefined(setActiveEvents) || (isEmptyArray(eventNames) && !this.isNativeEventsSynced)) {
            return;
        }

        this.isNativeEventsSynced = isNotEmptyArray(eventNames);
        setActiveEvents(this.id, eventNames).catch(emptyFn);
    }

    private clearEventRegistrations(): void {
        this.eventGeneration += 1;

        this.pendingDispatchers.forEach(dispatcher => {
            dispatcher.abandon();
        });
        this.pendingDispatchers.clear();

        this.eventRegistrations.forEach(registration => {
            if (isDefined(registration.subscription)) {
                registration.subscription.remove();
            }
        });
        this.eventRegistrations.clear();
        this.syncActiveEvents();
    }

    private forgetListener(
        registration: PaymentRequestEventRegistrationInterface,
        listener: PaymentRequestEventListener
    ): void {
        const listenerIndex = registration.listeners.indexOf(listener);

        if (listenerIndex >= 0) {
            registration.listeners.splice(listenerIndex, 1);
        }
    }

    private async handleChangeEvent(
        type: PaymentRequestEventType,
        listeners: PaymentRequestEventListener[],
        payload: PaymentRequestEventPayloadInterface
    ): Promise<void> {
        const generation = this.eventGeneration;

        if (payload.requestId !== this.id || !this.isDispatchActive(generation)) {
            return;
        }

        this.applyEventPayload(payload);

        if (this.updating) {
            await this.sendDetailsUpdate(type, payload.eventId, null, generation);

            return;
        }

        this.updating = true;

        try {
            await this.dispatchChangeEvent(type, listeners, payload, generation);
        } finally {
            this.updating = false;
        }
    }

    private isDispatchActive(generation: number): boolean {
        return this.state === 'interactive' && this.eventGeneration === generation;
    }

    private async dispatchChangeEvent(
        type: PaymentRequestEventType,
        listeners: PaymentRequestEventListener[],
        payload: PaymentRequestEventPayloadInterface,
        generation: number
    ): Promise<void> {
        const dispatcher = new ChangeEventDispatcher(type, payload, () => this.isDispatchActive(generation));

        this.pendingDispatchers.add(dispatcher);

        try {
            const detailsUpdate = await this.resolveDetailsUpdate(dispatcher, listeners);

            await this.sendDetailsUpdate(type, payload.eventId, detailsUpdate, generation);
        } finally {
            this.pendingDispatchers.delete(dispatcher);
        }
    }

    private applyEventPayload(payload: PaymentRequestEventPayloadInterface): void {
        if (isDefined(payload.shippingAddress)) {
            this.shippingAddress = payload.shippingAddress;
        }

        if (isDefined(payload.shippingOption)) {
            this.shippingOption = payload.shippingOption;
        }

        if (isDefined(payload.couponCode)) {
            this.couponCode = payload.couponCode;
        }
    }

    private async resolveDetailsUpdate(
        dispatcher: ChangeEventDispatcher,
        listeners: PaymentRequestEventListener[]
    ): Promise<Maybe<PaymentDetailsUpdate>> {
        try {
            const detailsUpdate = await dispatcher.dispatch(listeners);

            if (isDefined(detailsUpdate)) {
                validateDetailsUpdate(detailsUpdate);
            }

            return detailsUpdate;
        } catch (error) {
            warnChangeEventError(error);

            return null;
        }
    }

    private async sendDetailsUpdate(
        type: PaymentRequestEventType,
        eventId: number | undefined,
        detailsUpdate: Maybe<PaymentDetailsUpdate>,
        generation: number
    ): Promise<void> {
        const { updatePaymentDetails } = NativePayments;

        if (!this.isDispatchActive(generation) || !isDefined(updatePaymentDetails)) {
            return;
        }

        const updatedDetails = this.getUpdatedDetails(detailsUpdate);
        const resolvedDetails = this.resolveEffectiveDetails(updatedDetails);
        const update: NativePaymentDetailsUpdateInterface = {
            error: detailsUpdate?.error ?? '',
            eventName: type,
            requestId: this.id,
            total: resolvedDetails.total,
            ...(isDefined(eventId) && { eventId }),
        };

        await updatePaymentDetails(update, resolvedDetails.displayItems, updatedDetails.shippingOptions ?? []);

        this.details = updatedDetails;
    }

    private getUpdatedDetails(detailsUpdate: Maybe<PaymentDetailsUpdate>): PaymentDetailsInit {
        if (!isDefined(detailsUpdate)) {
            return this.details;
        }

        return {
            ...this.details,
            ...(isDefined(detailsUpdate.total) && { total: detailsUpdate.total }),
            ...(isDefined(detailsUpdate.displayItems) && { displayItems: detailsUpdate.displayItems }),
            ...(isDefined(detailsUpdate.shippingOptions) && { shippingOptions: detailsUpdate.shippingOptions }),
            ...(isDefined(detailsUpdate.modifiers) && { modifiers: detailsUpdate.modifiers }),
        };
    }

    private resolveEffectiveDetails(details: PaymentDetailsInit): ResolvedPaymentDetailsInterface {
        return resolvePaymentDetailsModifier(
            this.getPlatformSupportedMethod(),
            details.total,
            details.displayItems,
            details.modifiers
        );
    }

    private getPlatformSupportedMethod(): PaymentMethodNameEnum {
        return Platform.OS === 'ios' ? PaymentMethodNameEnum.ApplePay : PaymentMethodNameEnum.AndroidPay;
    }

    private findPlatformPaymentMethodData(): AndroidPaymentMethodDataDataInterface | IosPaymentMethodDataDataInterface {
        const platformSupportedMethod = this.getPlatformSupportedMethod();

        const platformMethod = this.methodData.find(
            paymentMethodData => paymentMethodData.supportedMethods === platformSupportedMethod
        );

        if (!isDefined(platformMethod)) {
            throw new DOMException(PaymentsErrorEnum.NotSupportedError);
        }

        return platformMethod.data;
    }

    private getAndroidPaymentMethodData(
        methodData: AndroidPaymentMethodDataDataInterface,
        total: PaymentItem
    ): AndroidPaymentDataRequest {
        const isBillingRequired =
            methodData.requestBillingAddress === true ||
            methodData.requestPayerName === true ||
            methodData.requestPayerPhone === true;

        const totalPriceStatus = methodData.totalPriceStatus ?? defaultAndroidTransactionInfo.totalPriceStatus;

        return {
            ...defaultAndroidPaymentDataRequest,
            merchantInfo: {
                merchantName: total.label,
            },
            transactionInfo: {
                ...defaultAndroidTransactionInfo,
                currencyCode: methodData.currencyCode,
                totalPrice: total.amount.value,
                totalPriceLabel: total.label,
                countryCode: methodData.countryCode,
                totalPriceStatus,
                ...(isDefined(methodData.checkoutOption) && { checkoutOption: methodData.checkoutOption }),
                ...(isDefined(methodData.transactionId) && { transactionId: methodData.transactionId }),
            },
            allowedPaymentMethods: [
                {
                    ...defaultAndroidPaymentMethod,
                    parameters: {
                        ...defaultAndroidPaymentMethod.parameters,
                        allowedCardNetworks: methodData.supportedNetworks.map(
                            network => network.toUpperCase() as AndroidAllowedCardNetworksEnum
                        ),
                        allowedAuthMethods:
                            methodData.allowedAuthMethods ?? defaultAndroidPaymentMethod.parameters.allowedAuthMethods,
                        ...(isBillingRequired && {
                            billingAddressRequired: true,
                            billingAddressParameters: {
                                format: methodData.requestBillingAddress === true ? 'FULL' : 'MIN',
                                phoneNumberRequired: methodData.requestPayerPhone === true,
                            },
                        }),
                    },
                    ...(isDefined(methodData.gatewayConfig) && {
                        tokenizationSpecification: {
                            parameters: methodData.gatewayConfig,
                            type: AndroidPaymentMethodTokenizationType.PAYMENT_GATEWAY,
                        },
                    }),
                    ...(isDefined(methodData.directConfig) && {
                        tokenizationSpecification: {
                            parameters: methodData.directConfig,
                            type: AndroidPaymentMethodTokenizationType.DIRECT,
                        },
                    }),
                },
            ],
            ...(methodData.requestPayerEmail === true && { emailRequired: true }),
            ...(methodData.requestShipping === true && {
                shippingAddressRequired: true,
                shippingAddressParameters: {
                    phoneNumberRequired: methodData.requestPayerPhone === true,
                },
            }),
        };
    }

    private getIosPaymentMethodData(methodData: IosPaymentMethodDataDataInterface): IosPaymentDataRequest {
        // TODO: Add mappings for other systems if needed
        const supportedNetworkMap: Record<SupportedNetworkEnum, IosPKPaymentNetworksEnum> = {
            [SupportedNetworkEnum.Amex]: IosPKPaymentNetworksEnum.PKPaymentNetworkAmex,
            [SupportedNetworkEnum.Mastercard]: IosPKPaymentNetworksEnum.PKPaymentNetworkMasterCard,
            [SupportedNetworkEnum.Visa]: IosPKPaymentNetworksEnum.PKPaymentNetworkVisa,
            [SupportedNetworkEnum.Discover]: IosPKPaymentNetworksEnum.PKPaymentNetworkDiscover,
            [SupportedNetworkEnum.Bancontact]: IosPKPaymentNetworksEnum.PKPaymentNetworkBancontact,
            [SupportedNetworkEnum.CartesBancaires]: IosPKPaymentNetworksEnum.PKPaymentNetworkCartesBancaires,
            [SupportedNetworkEnum.ChinaUnionPay]: IosPKPaymentNetworksEnum.PKPaymentNetworkChinaUnionPay,
            [SupportedNetworkEnum.Dankort]: IosPKPaymentNetworksEnum.PKPaymentNetworkDankort,
            [SupportedNetworkEnum.Eftpos]: IosPKPaymentNetworksEnum.PKPaymentNetworkEftpos,
            [SupportedNetworkEnum.Electron]: IosPKPaymentNetworksEnum.PKPaymentNetworkElectron,
            [SupportedNetworkEnum.Elo]: IosPKPaymentNetworksEnum.PKPaymentNetworkElo,
            [SupportedNetworkEnum.Girocard]: IosPKPaymentNetworksEnum.PKPaymentNetworkGirocard,
            [SupportedNetworkEnum.Interac]: IosPKPaymentNetworksEnum.PKPaymentNetworkInterac,
            [SupportedNetworkEnum.Jcb]: IosPKPaymentNetworksEnum.PKPaymentNetworkJCB,
            [SupportedNetworkEnum.Mada]: IosPKPaymentNetworksEnum.PKPaymentNetworkMada,
            [SupportedNetworkEnum.Maestro]: IosPKPaymentNetworksEnum.PKPaymentNetworkMaestro,
            [SupportedNetworkEnum.Mir]: IosPKPaymentNetworksEnum.PKPaymentNetworkMir,
            [SupportedNetworkEnum.PrivateLabel]: IosPKPaymentNetworksEnum.PKPaymentNetworkPrivateLabel,
            [SupportedNetworkEnum.Vpay]: IosPKPaymentNetworksEnum.PKPaymentNetworkVPay,
        };

        const defaultMerchantCapabilities = [
            IosPKMerchantCapability.PKMerchantCapability3DS,
            IosPKMerchantCapability.PKMerchantCapabilityDebit,
            IosPKMerchantCapability.PKMerchantCapabilityCredit,
        ];

        const requestedShippingFields = this.getRequestedShippingFields(methodData);

        const isShippingRequested = requestedShippingFields.length > 0;

        return {
            countryCode: methodData.countryCode,
            currencyCode: methodData.currencyCode,
            merchantIdentifier: methodData.merchantIdentifier,
            supportedNetworks: methodData.supportedNetworks.map(network => supportedNetworkMap[network]),
            merchantCapabilities: isNotEmptyArray(methodData.merchantCapabilities)
                ? methodData.merchantCapabilities
                : defaultMerchantCapabilities,
            ...(methodData.requestBillingAddress === true && {
                requiredBillingContactFields: [IOSPKContactField.PKContactFieldPostalAddress],
            }),
            ...(isShippingRequested && { requiredShippingContactFields: requestedShippingFields }),
            ...(isDefined(methodData.applicationData) && { applicationData: methodData.applicationData }),
            ...(isNotEmptyString(methodData.couponCode) && { couponCode: methodData.couponCode }),
            ...(isDefined(methodData.shippingType) && { shippingType: methodData.shippingType }),
        };
    }

    private getRequestedShippingFields(methodData: IosPaymentMethodDataDataInterface): IOSPKContactField[] {
        const requiredShippingFields: IOSPKContactField[] = [];
        if (methodData.requestPayerEmail ?? false) {
            requiredShippingFields.push(IOSPKContactField.PKContactFieldEmailAddress);
        }
        if (methodData.requestPayerName ?? false) {
            requiredShippingFields.push(IOSPKContactField.PKContactFieldName);
        }
        if (methodData.requestPayerPhone ?? false) {
            requiredShippingFields.push(IOSPKContactField.PKContactFieldPhoneNumber);
        }
        if (methodData.requestShipping ?? false) {
            requiredShippingFields.push(IOSPKContactField.PKContactFieldPostalAddress);
        }

        return requiredShippingFields;
    }
}
