import { getIntentType, IntentType } from './models/IntentType';
import { IntentMethods, intentMethodsFromJson } from './models/IntentMethods';
import { MoneyHash } from './nativeModule';
import { IntentDetails, intentDetailsFromJson } from './models/IntentDetails';
import type { ApplePayData, IntentResult } from './models';
import { intentResultFromJson } from './models/IntentResult';
import type { EmbedStyle } from './models';
import { embedStyleToJson } from './models/style/EmbedStyle';
import { throwMhError } from './utils/throwIf';
import { MethodType, getMethodType } from './models/MethodType';
import { MethodMetaData, methodMetaDataToJson } from './models/MethodMetaData';
import type { VaultData } from './SecureFields/types';
import { logLevelToJson, type LogLevel } from './models/log/LogLevel';
import {
  DiscountData,
  DiscountItem,
  FeeItem,
  FeesData,
  Language,
} from './models';
import { feeItemToJson } from './models/fee/FeeItem';
import { feesDataFromJson } from './models/fee/FeesData';
import { discountItemToJson } from './models/discount/DiscountItem';
import { discountDataFromJson } from './models/discount/DiscountData';
import { languageToJson } from './models/language/Language';
import { Platform } from 'react-native';
import {
  NativePayReceipt,
  nativePayReceiptFromJson,
  nativePayReceiptToJson,
} from './models/receipt/NativePayReceipt';
import { type ApplePayNativeConfig } from './models/config/ApplePayNativeConfig';
import {
  InstallmentPlan,
  installmentPlanFromJson,
} from './models/InstallmentPlans';
import {
  type InstallmentPlanData,
  installmentPlanDataToJson,
} from './models/InstallmentPlanData';
import { lookupDataFromJson, type LookupData } from './models/LookupData';
import { applePayDataToJson } from './models/NativePayData';
import {
  subscriptionPlanFromJson,
  type SubscriptionPlan,
} from './models/subscriptions/SubscriptionPlan';
import {
  subscriptionPlanGroupsResponseFromJson,
  type SubscriptionPlanGroupsResponse,
} from './models/subscriptions/SubscriptionPlanGroupsResponse';
import {
  type GooglePayNativeConfig,
  googlePayNativeConfigToJson,
} from './models/config/GooglePayNativeConfig';
import {
  type BankAccountTokenizationStatus,
  bankAccountTokenizationStatusFromJson,
} from './models/BankAccountTokenizationStatus';
import {
  type RecurringPaymentRequest,
  recurringPaymentRequestToJson,
} from './models/RecurringPaymentRequest';
import {
  type AutomaticReloadPaymentRequest,
  automaticReloadPaymentRequestToJson,
} from './models/AutomaticReloadPaymentRequest';

export * from './SecureFields';
export * from './models';

export class MoneyHashSDKBuilder {
  private static instance: MoneyHashSDK;
  private googlePayConfig?: GooglePayNativeConfig;
  private applePayConfig?: ApplePayNativeConfig;

  setNativeGooglePayConfig(config: GooglePayNativeConfig): MoneyHashSDKBuilder {
    this.googlePayConfig = config;
    return this;
  }

  setNativeApplePayConfig(config: ApplePayNativeConfig): MoneyHashSDKBuilder {
    this.applePayConfig = config;
    return this;
  }

  build(): MoneyHashSDK {
    if (!MoneyHashSDKBuilder.instance) {
      MoneyHashSDKBuilder.instance = new MoneyHashSDK();
    }

    if (this.googlePayConfig && Platform.OS === 'android') {
      MoneyHashSDKBuilder.instance.setNativeGooglePayConfig(
        this.googlePayConfig
      );
    }

    if (this.applePayConfig && Platform.OS === 'ios') {
      MoneyHashSDKBuilder.instance.setNativeApplePayConfig(this.applePayConfig);
    }

    return MoneyHashSDKBuilder.instance;
  }

  static build() {
    if (!this.instance) {
      MoneyHashSDKBuilder.instance = new MoneyHashSDK();
    }
    return MoneyHashSDKBuilder.instance;
  }
}

export class MoneyHashSDK {
  constructor() {
    this.#setVersionName('reactnative@4.0.19');
  }

  /**
   * @deprecated getIntentMethods is deprecated and will be removed in the next release. Use getMethods instead.
   */
  getIntentMethods(
    intentId: string,
    intentType: IntentType
  ): Promise<IntentMethods> {
    return this.getMethods(intentId, intentType);
  }

  async getMethods(options: {
    currency: string;
    amount?: number;
    customer?: string;
    flowId?: string;
    operation?: 'purchase' | 'authorize';
    customFields?: Record<string, string | number | boolean>;
  }): Promise<IntentMethods>;

  async getMethods(
    intentId: string,
    intentType: IntentType
  ): Promise<IntentMethods>;
  async getMethods(
    intentIdOrOptions:
      | string
      | {
          currency: string;
          amount?: number;
          customer?: string;
          flowId?: string;
          operation?: 'purchase' | 'authorize';
          customFields?: Record<string, string | number | boolean>;
        },
    intentType?: IntentType
  ): Promise<IntentMethods> {
    if (typeof intentIdOrOptions === 'object') {
      const { currency, amount, customer, flowId, operation, customFields } =
        intentIdOrOptions;
      return handlePromiseWithJsonParsing(
        MoneyHash.getMethods(
          '',
          '',
          currency,
          (amount ?? '').toString(),
          customer ?? '',
          flowId ?? '',
          operation ?? '',
          Object.fromEntries(
            Object.entries(customFields || {}).map(([key, value]) => [
              key,
              {
                type: typeof value,
                value,
              },
            ])
          )
        ),
        intentMethodsFromJson
      );
    } else {
      return handlePromiseWithJsonParsing(
        MoneyHash.getMethods(
          intentIdOrOptions,
          getIntentType(intentType!),
          '',
          '',
          '',
          '',
          '',
          {}
        ),
        intentMethodsFromJson
      );
    }
  }

  getIntentDetails(
    intentId: string,
    intentType: IntentType
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.getIntentDetails(intentId, getIntentType(intentType)),
      intentDetailsFromJson
    );
  }

  resetSelectedMethod(
    intentId: string,
    intentType: IntentType
  ): Promise<IntentResult> {
    return handlePromiseWithJsonParsing(
      MoneyHash.resetSelectedMethod(intentId, getIntentType(intentType)),
      intentResultFromJson
    );
  }

  renderForm(
    intentId: string,
    intentType: IntentType,
    embedStyle?: EmbedStyle
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.renderForm(
        intentId,
        getIntentType(intentType),
        embedStyle ? embedStyleToJson(embedStyle) : {}
      ),
      intentDetailsFromJson
    );
  }

  renderSubscriptionEmbed(
    intentId: string,
    embedStyle?: EmbedStyle
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.renderSubscriptionEmbed(
        intentId,
        embedStyle ? embedStyleToJson(embedStyle) : {}
      ),
      intentDetailsFromJson
    );
  }

  renderURL({
    url,
    intentId,
    intentType,
    embedStyle,
  }: {
    url: string;
    intentId: string;
    intentType: IntentType;
    embedStyle?: EmbedStyle;
  }): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.renderURL(
        url,
        intentId,
        getIntentType(intentType),
        embedStyle ? embedStyleToJson(embedStyle) : {}
      ),
      intentDetailsFromJson
    );
  }

  deleteSavedCard(cardTokenId: string, intentSecret: string): Promise<void> {
    return handlePromiseWithJsonParsing(
      MoneyHash.deleteSavedCard(cardTokenId, intentSecret)
    );
  }

  submitForm(
    intentId: string,
    selectedMethodId: string,
    billingData?: Record<string, string>,
    shippingData?: Record<string, string>,
    vaultData?: VaultData,
    saveCard?: boolean,
    installmentPlanData?: InstallmentPlanData
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.submitForm(
        intentId,
        selectedMethodId,
        billingData ?? null,
        shippingData ?? null,
        vaultData,
        saveCard ?? false,
        installmentPlanData
          ? installmentPlanDataToJson(installmentPlanData)
          : {}
      ),
      intentDetailsFromJson
    );
  }

  proceedWithMethod(
    intentId: string,
    intentType: IntentType,
    selectedMethodId: string,
    methodType: MethodType,
    methodMetaData?: MethodMetaData,
    useWalletBalance?: boolean,
    installmentPlanData?: InstallmentPlanData
  ): Promise<IntentResult> {
    return handlePromiseWithJsonParsing(
      MoneyHash.proceedWithMethod(
        intentId,
        getIntentType(intentType),
        selectedMethodId,
        getMethodType(methodType),
        methodMetaDataToJson(methodMetaData),
        useWalletBalance ?? false,
        installmentPlanData
          ? installmentPlanDataToJson(installmentPlanData)
          : {}
      ),
      intentResultFromJson
    );
  }

  submitCardCVV(
    intentId: string,
    cvv: string,
    installmentPlanData?: InstallmentPlanData
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.submitCardCVV(
        intentId,
        cvv,
        installmentPlanData
          ? installmentPlanDataToJson(installmentPlanData)
          : {}
      ),
      intentDetailsFromJson
    );
  }

  submitPaymentReceipt(
    intentId: string,
    receipt: NativePayReceipt
  ): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.submitPaymentReceipt(intentId, nativePayReceiptToJson(receipt)),
      intentDetailsFromJson
    );
  }

  setLogLevel(level: LogLevel) {
    MoneyHash.setLogLevel(logLevelToJson(level));
  }

  setLocale(language: Language) {
    MoneyHash.setLocale(languageToJson(language));
  }

  setPublicKey(publicKey: string) {
    MoneyHash.setPublicKey(publicKey);
  }

  #setVersionName(versionName: string) {
    MoneyHash.setVersionName(versionName);
  }

  updateFees(intentId: string, fees: FeeItem[]): Promise<FeesData> {
    const items = fees.map((item) => feeItemToJson(item));
    return handlePromiseWithJsonParsing(
      MoneyHash.updateFees(intentId, items),
      feesDataFromJson
    );
  }

  updateDiscount(
    intentId: string,
    discount: DiscountItem
  ): Promise<DiscountData> {
    return handlePromiseWithJsonParsing(
      MoneyHash.updateDiscount(intentId, discountItemToJson(discount)),
      discountDataFromJson
    );
  }

  isDeviceCompatibleWithApplePay(): Promise<boolean> {
    if (Platform.OS === 'ios') {
      return MoneyHash.isDeviceCompatibleWithApplePay();
    } else {
      return Promise.reject(
        new Error('Apple Pay is only available on iOS devices.')
      );
    }
  }

  selectInstallmentPlan({
    intentId,
    installmentPlanData,
  }: {
    intentId: string;
    installmentPlanData: InstallmentPlanData;
  }): Promise<IntentDetails> {
    return handlePromiseWithJsonParsing(
      MoneyHash.selectInstallmentPlan(intentId, installmentPlanData),
      intentDetailsFromJson
    );
  }

  getInstallmentPlans({
    amount,
    currency,
    first6Digits,
  }: {
    amount: number;
    currency: string;
    first6Digits?: string;
  }): Promise<InstallmentPlan[]> {
    return handlePromiseWithJsonParsing(
      MoneyHash.getInstallmentPlans(amount, currency, first6Digits ?? ''),
      (parsedJson) => parsedJson.map(installmentPlanFromJson)
    );
  }

  generateApplePayReceipt(params: {
    depositAmount: number;
    applePayData: ApplePayData;
  }): Promise<NativePayReceipt>;

  generateApplePayReceipt(params: {
    depositAmount: number;
    merchantIdentifier: string;
    currencyCode: string;
    countryCode: string;
    supportedNetworks?: string[];
    merchantCapabilities?: string[];
    recurringPayment?: RecurringPaymentRequest;
    automaticReload?: AutomaticReloadPaymentRequest;
  }): Promise<NativePayReceipt>;

  generateApplePayReceipt(
    params:
      | {
          depositAmount: number;
          applePayData: ApplePayData;
        }
      | {
          depositAmount: number;
          merchantIdentifier: string;
          currencyCode: string;
          countryCode: string;
          supportedNetworks?: string[];
          merchantCapabilities?: string[];
          recurringPayment?: RecurringPaymentRequest;
          automaticReload?: AutomaticReloadPaymentRequest;
        }
  ): Promise<NativePayReceipt> {
    if (Platform.OS !== 'ios') {
      return Promise.reject(
        new Error('Apple Pay is only available on iOS devices.')
      );
    }

    if ('applePayData' in params) {
      const { depositAmount, applePayData } = params;
      return handlePromiseWithJsonParsing(
        MoneyHash.generateApplePayReceiptWithApplePayData(
          depositAmount,
          applePayDataToJson(applePayData)
        ),
        nativePayReceiptFromJson
      );
    } else {
      const {
        depositAmount,
        merchantIdentifier,
        currencyCode,
        countryCode,
        supportedNetworks = [],
        merchantCapabilities = [],
        recurringPayment,
        automaticReload,
      } = params;
      return handlePromiseWithJsonParsing(
        MoneyHash.generateRecurringApplePayReceipt(
          depositAmount,
          merchantIdentifier,
          currencyCode,
          countryCode,
          supportedNetworks,
          merchantCapabilities,
          recurringPayment
            ? recurringPaymentRequestToJson(recurringPayment)
            : null,
          automaticReload
            ? automaticReloadPaymentRequestToJson(automaticReload)
            : null
        ),
        nativePayReceiptFromJson
      );
    }
  }

  getApplePayBinLookup({
    receipt,
    methodID,
    flowId,
  }: {
    receipt: NativePayReceipt;
    methodID: string;
    flowId?: string;
  }): Promise<LookupData> {
    if (Platform.OS === 'ios') {
      return handlePromiseWithJsonParsing(
        MoneyHash.getApplePayBinLookup(receipt, methodID, flowId),
        lookupDataFromJson
      );
    } else {
      return Promise.reject(
        new Error('Apple Pay BIN lookup is only available on iOS devices.')
      );
    }
  }

  /**
   * Tokenizes a receipt (e.g., Apple Pay receipt) to create a card token.
   * This feature is only supported on iOS devices.
   *
   * @param receipt The receipt string obtained from native payment methods (e.g., Apple Pay).
   * @param methodId The ID of the payment method.
   * @param cardTokenIntentId The unique identifier of the card token intent.
   * @returns A Promise that resolves to a string representing the created card token ID.
   */
  tokenizeReceipt({
    receipt,
    methodId,
    cardTokenIntentId,
  }: {
    receipt: string;
    methodId: string;
    cardTokenIntentId: string;
  }): Promise<string> {
    if (Platform.OS === 'ios') {
      return MoneyHash.tokenizeReceipt(receipt, methodId, cardTokenIntentId);
    } else {
      return Promise.reject(
        new Error('Receipt tokenization is only available on iOS devices.')
      );
    }
  }

  isReadyForGooglePay(): Promise<boolean> {
    if (Platform.OS === 'android') {
      return MoneyHash.isReadyForGooglePay();
    } else {
      return Promise.reject(
        new Error('Google Pay is only available on Android devices.')
      );
    }
  }

  setNativeGooglePayConfig(
    googlePayConfig: GooglePayNativeConfig
  ): Promise<void> {
    if (Platform.OS === 'android') {
      return MoneyHash.setNativeGooglePayConfig(
        googlePayNativeConfigToJson(googlePayConfig)
      );
    } else {
      return Promise.reject(
        new Error('Google Pay config is only available on Android devices.')
      );
    }
  }

  setNativeApplePayConfig(applePayConfig: ApplePayNativeConfig): Promise<void> {
    if (Platform.OS === 'ios') {
      return MoneyHash.setNativeApplePayConfig(applePayConfig);
    } else {
      return Promise.reject(
        new Error('Apple Pay config is only available on iOS devices.')
      );
    }
  }

  isGooglePayReady(params?: {
    allowedCardNetworks?: string[];
    allowedCardAuthMethods?: string[];
  }): Promise<boolean> {
    if (Platform.OS !== 'android') {
      return Promise.reject(
        new Error('Google Pay is only available on Android devices.')
      );
    }
    const { allowedCardNetworks, allowedCardAuthMethods } = params || {};
    return MoneyHash.isGooglePayReady(
      allowedCardNetworks || [],
      allowedCardAuthMethods || []
    );
  }

  proceedWithGooglePay(params: {
    intentId: string;
    currency: string;
    amount: number;
    countryCode: string;
    gateway: string;
    gatewayMerchantId: string;
    merchantId: string;
    merchantName: string;
    allowedCardNetworks?: string[];
    allowedCardAuthMethods?: string[];
  }): Promise<IntentDetails> {
    if (Platform.OS !== 'android') {
      return Promise.reject(
        new Error('Google Pay is only available on Android devices.')
      );
    }

    const {
      intentId,
      currency,
      amount,
      countryCode,
      gateway,
      gatewayMerchantId,
      merchantId,
      merchantName,
      allowedCardNetworks,
      allowedCardAuthMethods,
    } = params;

    return handlePromiseWithJsonParsing(
      MoneyHash.proceedWithGooglePay(
        intentId,
        currency,
        amount,
        countryCode,
        gateway,
        gatewayMerchantId,
        merchantId,
        merchantName,
        allowedCardNetworks || [],
        allowedCardAuthMethods || []
      ),
      intentDetailsFromJson
    );
  }

  generateGooglePayReceipt(params: {
    currency: string;
    amount: number;
    countryCode: string;
    gateway: string;
    gatewayMerchantId: string;
    merchantId: string;
    merchantName: string;
    allowedCardNetworks?: string[];
    allowedCardAuthMethods?: string[];
  }): Promise<NativePayReceipt> {
    if (Platform.OS !== 'android') {
      return Promise.reject(
        new Error('Google Pay is only available on Android devices.')
      );
    }

    const {
      currency,
      amount,
      countryCode,
      gateway,
      gatewayMerchantId,
      merchantId,
      merchantName,
      allowedCardNetworks,
      allowedCardAuthMethods,
    } = params;

    return handlePromiseWithJsonParsing(
      MoneyHash.generateGooglePayReceipt(
        currency,
        amount,
        countryCode,
        gateway,
        gatewayMerchantId,
        merchantId,
        merchantName,
        allowedCardNetworks || [],
        allowedCardAuthMethods || []
      ),
      nativePayReceiptFromJson
    );
  }

  getSubscriptionPlans({
    planGroupId,
    customerId,
  }: {
    /** Short alphanumeric ID that identifies the plan group (e.g. "k9mJaKL"). */
    planGroupId: string;
    /** UUID of the customer requesting the plans. */
    customerId: string;
  }): Promise<SubscriptionPlan[]> {
    // Invoke the native method and decode the JSON array → SubscriptionPlan[]
    return handlePromiseWithJsonParsing(
      MoneyHash.getSubscriptionPlans(planGroupId, customerId),
      (parsedJson) => parsedJson.map(subscriptionPlanFromJson)
    );
  }

  selectSubscriptionPlan({
    planGroupId,
    customerId,
    planId,
  }: {
    /** Plan group ID (same as above). */
    planGroupId: string;
    /** UUID of the customer who is subscribing. */
    customerId: string;
    /** Unique ID of the plan selected by the customer. */
    planId: string;
  }): Promise<IntentDetails> {
    // Invoke the native method and decode the JSON → IntentDetails
    return handlePromiseWithJsonParsing(
      MoneyHash.selectSubscriptionPlan(planGroupId, customerId, planId),
      intentDetailsFromJson
    );
  }

  getSubscriptionPlanGroups({
    limit = 10,
    offset = 0,
    currency,
  }: {
    /** Optional limit for the number of plan groups to retrieve. Defaults to 10 if not specified. */
    limit?: number;
    /** Optional offset for pagination. Defaults to 0 if not specified. */
    offset?: number;
    /** Optional currency filter to retrieve plan groups for a specific currency. */
    currency?: string;
  } = {}): Promise<SubscriptionPlanGroupsResponse> {
    // Invoke the native method and decode the JSON → SubscriptionPlanGroupsResponse
    return handlePromiseWithJsonParsing(
      MoneyHash.getSubscriptionPlanGroups(limit, offset, currency),
      subscriptionPlanGroupsResponseFromJson
    );
  }

  /**
   * Renders a bank account tokenization form as an embedded component.
   *
   * @param intentId The unique identifier of the payment intent associated with the bank account token.
   * @returns A Promise that resolves to the bank account tokenization status.
   */
  renderCreateBankAccountTokenEmbed({
    intentId,
  }: {
    intentId: string;
  }): Promise<BankAccountTokenizationStatus> {
    return handlePromiseWithJsonParsing(
      MoneyHash.renderCreateBankAccountTokenEmbed(intentId),
      bankAccountTokenizationStatusFromJson
    );
  }
}

function handlePromiseWithJsonParsing(promise: Promise<any>): Promise<void>;

function handlePromiseWithJsonParsing<T>(
  promise: Promise<any>,
  parseSuccess: (parsedJson: any) => T
): Promise<T>;
function handlePromiseWithJsonParsing<T>(
  promise: Promise<any>,
  parseSuccess?: (parsedJson: any) => T
): Promise<T | void> {
  return new Promise((resolve, reject) => {
    promise
      .then((result: any) => {
        try {
          const parsedJson = JSON.parse(result);
          resolve(parseSuccess ? parseSuccess(parsedJson) : undefined);
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}
