import React, { useLayoutEffect } from 'react';
import type { UseSecureCardFormRefMethods } from './hooks/useSecureCardForm';
import { useUpdateEffect } from './hooks/useUpdateEffect';
import { MoneyHash } from '../nativeModule';
import type { SecureFieldType, VaultData } from './types';
import { throwIf, throwMhError } from '../utils/throwIf';
import type { IntentDetails, IntentStateDetails, LookupData } from '../models';
import {
  type InstallmentPlanData,
  installmentPlanDataToJson,
} from '../models/InstallmentPlanData';
import { intentDetailsFromJson } from '../models/IntentDetails';
import { lookupDataFromJson } from '../models/LookupData';
import {
  cardFormConfigurationToJson,
  type CardFormConfiguration,
} from '../models/CardFormConfiguration';

export const fieldNamesMap: Record<SecureFieldType, string> = {
  cardNumber: 'card_number',
  cardHolderName: 'card_holder_name',
  expiryYear: 'expiry_year',
  expiryMonth: 'expiry_month',
  cvv: 'cvv',
};

type SecureCardFormContextType = {
  updateValue: (options: { name: string; value: string }) => void;
  updateFieldValidity: (options: { name: string; isValid: boolean }) => void;
  // The detected card brand, shared so the CVV can re-validate when it changes.
  // CVV length is brand-dependent (e.g. Amex requires a 4-digit CVV), so its
  // validity can change when the brand changes even though its value didn't.
  brand?: string;
  setBrand: (brand?: string) => void;
};
const SecureCardFormContext =
  React.createContext<SecureCardFormContextType | null>(null);

export type SecureCardFormProps = {
  children: React.ReactNode;
  onFormValidityChange?: (isValid: boolean) => void;
  configuration?: CardFormConfiguration;
};

export const SecureCardForm = React.forwardRef<unknown, SecureCardFormProps>(
  function SecureCardForm(
    { children, onFormValidityChange, configuration },
    ref
  ) {
    const valuesRef = React.useRef<Record<string, string>>({});

    const [fieldsValidityMap, setFieldsValidityMap] = React.useState<
      Record<string, boolean>
    >({});

    const [isMoneyHashFormValid, setIsMoneyHashFormValid] =
      React.useState(false);

    React.useEffect(() => {
      const checkMoneyHashValidity = async () => {
        try {
          const result = await MoneyHash.isValidCardForm();
          const parsed = JSON.parse(result);
          setIsMoneyHashFormValid(parsed.isValid);
        } catch (error) {
          setIsMoneyHashFormValid(true);
          console.error(error);
        }
      };

      checkMoneyHashValidity();
    }, [fieldsValidityMap]);

    const isValidForm = React.useMemo(() => {
      const areFieldsValid = Object.values(fieldsValidityMap).every(Boolean);

      return areFieldsValid && isMoneyHashFormValid;
    }, [fieldsValidityMap, isMoneyHashFormValid]);

    useLayoutEffect(() => {
      if (!configuration) return;
      MoneyHash.setCardFormConfiguration(
        cardFormConfigurationToJson(configuration)
      );
    }, []);

    useUpdateEffect(() => {
      onFormValidityChange?.(isValidForm);
    }, [isValidForm]);

    React.useImperativeHandle<unknown, UseSecureCardFormRefMethods>(
      ref,
      () => ({
        async collect(options?: { accessToken: string }) {
          const missingField = getMissingField(
            Object.keys(valuesRef.current) as SecureFieldType[]
          );

          throwIf(
            !!missingField,
            `${missingField} must be registered as <SecureTextField /> within <SecureCardForm />`
          );

          return collectCardForm({
            accessToken: options?.accessToken,
          });
        },
        pay,
        createCardToken,
        binLookup,
      }),
      []
    );

    const updateValue: SecureCardFormContextType['updateValue'] =
      React.useCallback(
        ({ name, value }) => (valuesRef.current[name] = value),
        []
      );

    const updateFieldValidity: SecureCardFormContextType['updateFieldValidity'] =
      React.useCallback(
        ({ name, isValid }) =>
          setFieldsValidityMap((prev) => ({ ...prev, [name]: isValid })),
        []
      );

    const [brand, setBrand] = React.useState<string>();

    const contextValue = React.useMemo(
      () => ({
        updateValue,
        updateFieldValidity,
        brand,
        setBrand,
      }),
      [updateValue, updateFieldValidity, brand]
    );

    return (
      <SecureCardFormContext.Provider value={contextValue}>
        {children}
      </SecureCardFormContext.Provider>
    );
  }
);

function collectCardForm({
  accessToken,
}: {
  accessToken?: string;
}): Promise<VaultData> {
  return new Promise<VaultData>((resolve, reject) => {
    MoneyHash.collectCardForm(accessToken)
      .then((result: any) => {
        try {
          const data = JSON.parse(result);
          resolve(data);
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

function pay({
  intentId,
  saveCard = false,
  cardData,
  billingData,
  shippingData,
  installmentPlanData,
}: {
  intentId: string;
  saveCard?: boolean;
  cardData: VaultData;
  billingData?: Record<string, string>;
  shippingData?: Record<string, string>;
  installmentPlanData?: InstallmentPlanData;
}): Promise<IntentDetails> {
  return new Promise<IntentDetails>((resolve, reject) => {
    MoneyHash.pay(
      intentId,
      cardData,
      saveCard ?? false,
      billingData,
      shippingData,
      installmentPlanData ? installmentPlanDataToJson(installmentPlanData) : {}
    )
      .then((result: any) => {
        try {
          const data = JSON.parse(result);
          resolve(intentDetailsFromJson(data));
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

function createCardToken({
  cardIntentId,
  cardData,
}: {
  cardIntentId: string;
  cardData: VaultData;
}): Promise<IntentStateDetails> {
  return new Promise<IntentStateDetails>((resolve, reject) => {
    MoneyHash.createCardToken(cardIntentId, cardData)
      .then((result: any) => {
        try {
          const data = JSON.parse(result);
          resolve(data);
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

function binLookup(): Promise<LookupData>;
function binLookup(cardData: VaultData): Promise<LookupData>;
function binLookup(cardData?: VaultData): Promise<LookupData> {
  return new Promise<LookupData>((resolve, reject) => {
    const nativeCall = cardData
      ? MoneyHash.binLookupWithCardData(cardData)
      : MoneyHash.binLookup();

    nativeCall
      .then((result: any) => {
        try {
          const parsedData = JSON.parse(result);
          resolve(lookupDataFromJson(parsedData));
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

export function useSecureCardForm() {
  const context = React.useContext(SecureCardFormContext);
  if (!context) {
    throw new Error(
      '<SecureTextField /> must be used within a <SecureCardForm />'
    );
  }
  return context;
}

function getMissingField(registeredFields: SecureFieldType[]) {
  const requiredFields = new Set<SecureFieldType>([
    'cardNumber',
    'expiryMonth',
    'expiryYear',
    'cvv',
  ]);

  for (const element of requiredFields) {
    if (!registeredFields.includes(element)) {
      return element;
    }
  }

  return null;
}
