import React from 'react';
import { TextInput, type TextInputProps } from 'react-native';
import { useSecureCardForm, fieldNamesMap } from './Context';
import { useFocus } from './hooks/useFocus';
import { useUpdateEffect } from './hooks/useUpdateEffect';
import type { SecureFieldType } from './types';
import { MoneyHash } from '../nativeModule';
import { throwMhError } from '../utils/throwIf';

export type CardInfo = {
  first6Digits: string;
  brand: 'visa' | 'mastercard' | 'mada' | 'troy' | 'amex' | 'unknown';
  brandIconUrl: string;
};

type Field = {
  onFocus?: () => void;
  onBlur?: () => void;
  onChange?: (options: {
    isValid: boolean;
    length: number;
    errorMessage?: string;
  }) => void;
  onErrorChange?: (options: {
    isValid: boolean;
    errorMessage?: string;
  }) => void;
  style?:
    | TextInputProps['style']
    | ((states: {
        isDirty: boolean;
        isFocused: boolean;
        isError: boolean;
      }) => Required<TextInputProps>['style']);
};

type NonCvvFieldTypes = {
  [K in Exclude<SecureFieldType, 'cardNumber' | 'cardHolderName' | 'cvv'>]: {
    name: K;
    onCardBrandChange?: never;
    maskCvv?: never;
  } & Field;
}[Exclude<SecureFieldType, 'cardNumber' | 'cardHolderName' | 'cvv'>];

type CvvField = Field & {
  name: Extract<SecureFieldType, 'cvv'>;
  onCardBrandChange?: never;
  /**
   * When `true`, renders the CVV input with password-style masking (dots)
   * via `TextInput`'s `secureTextEntry`. The underlying stored value is
   * unchanged — this is a purely visual setting. Defaults to `false`.
   *
   * Available only on the CVV field; setting this prop on any other field
   * name is a compile-time error.
   */
  maskCvv?: boolean;
};

type CardNumberField = Field & {
  name: Extract<SecureFieldType, 'cardNumber'>;
  onCardBrandChange?: (cardInfo: CardInfo | undefined) => void;
  maskCvv?: never;
};

type CardHolderNameField = Field & {
  name: Extract<SecureFieldType, 'cardHolderName'>;
  onCardBrandChange?: never;
  maskCvv?: never;
};

export type SecureTextFieldProps = (
  | NonCvvFieldTypes
  | CvvField
  | CardNumberField
  | CardHolderNameField
) &
  Omit<
    TextInputProps,
    'style' | 'onFocus' | 'onBlur' | 'onChangeText' | 'onChange' | 'maxLength'
  >;

const autoCompleteMap: Record<SecureFieldType, TextInputProps['autoComplete']> =
  {
    cardNumber: 'cc-number',
    cardHolderName: 'name',
    expiryYear: 'cc-exp-year',
    expiryMonth: 'cc-exp-month',
    cvv: 'cc-csc',
  };

type FieldState = {
  isValid: boolean;
  errorMessage?: string;
  formattedValue: string;
  length: number;
  cardBrand?: {
    first6Digits: string;
    brand: 'visa' | 'mastercard' | 'mada' | 'troy' | 'amex' | 'unknown';
    brandIconUrl: string;
  };
};

function parseFieldState(result: any): FieldState {
  const parsedJson = JSON.parse(result);
  return {
    isValid: parsedJson.isValid,
    errorMessage: parsedJson.isValid ? undefined : parsedJson.errorMessage,
    formattedValue: parsedJson.formattedValue,
    length: parsedJson.length,
    cardBrand: parsedJson.cardBrand
      ? {
          first6Digits: parsedJson.cardBrand.first6Digits,
          brand: parsedJson.cardBrand.brand,
          brandIconUrl: parsedJson.cardBrand.brandIconUrl,
        }
      : undefined,
  };
}

// Stores the field's value and refreshes the brand. Returns no state — call this
// only when the field's text changes, then call `validate` to read the state.
function update(type: SecureFieldType, value: string): Promise<void> {
  return new Promise((resolve, reject) => {
    MoneyHash.updateCardField(fieldNamesMap[type], value)
      .then(() => resolve())
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

// Validates the field's already-stored value against the current brand and
// returns its formatted/validated state. Call after `update` on text change,
// and on focus change / blur / brand change (CVV) — it never re-sends a value.
function validate(type: SecureFieldType): Promise<FieldState> {
  return new Promise((resolve, reject) => {
    MoneyHash.validateCardField(fieldNamesMap[type])
      .then((result: any) => {
        try {
          resolve(parseFieldState(result));
        } catch (error: any) {
          throwMhError(error, reject);
        }
      })
      .catch((error: any) => {
        throwMhError(error, reject);
      });
  });
}

export type SecureTextFieldRef = {
  focus: () => void;
  blur: () => void;
  clear: () => void;
};

export const SecureTextField = React.forwardRef<
  SecureTextFieldRef,
  SecureTextFieldProps
>(
  (
    {
      name,
      onChange,
      onBlur,
      onFocus,
      onErrorChange,
      onCardBrandChange,
      style,
      maskCvv,
      ...props
    },
    ref
  ) => {
    const textInputRef = React.useRef<TextInput>(null);
    const [value, setValue] = React.useState('');
    const valueRef = React.useRef('');
    const [isDirty, setIsDirty] = React.useState(false);
    const isDirtyRef = React.useRef(false);
    const [isError, setIsError] = React.useState(false);
    const isErrorRef = React.useRef(false);
    const [cardInfo, setCardInfo] = React.useState<CardInfo | undefined>(
      undefined
    );
    const [isFocused, _onFocus, _onBlur] = useFocus();
    const { updateValue, updateFieldValidity, brand, setBrand } =
      useSecureCardForm();

    React.useImperativeHandle(
      ref,
      () => ({
        focus: () => textInputRef.current?.focus(),
        blur: () => textInputRef.current?.blur(),
        clear: () => textInputRef.current?.clear(),
      }),
      [textInputRef]
    );

    React.useEffect(() => {
      updateValue({ name, value });
    }, [name, updateValue, value]);

    React.useEffect(() => {
      update(name, '')
        .then(() => validate(name))
        .then(({ isValid }) => {
          updateFieldValidity({
            name,
            isValid,
          });
        });
    }, [name, updateFieldValidity]);

    // Re-validate this field against the current card brand without changing its
    // value. The form calls this for the CVV whenever the brand changes, since
    // the CVV's required length is brand-dependent (e.g. Amex needs 4 digits).
    // Surface the new state whenever the field has a value (mirrors the native
    // cvvStateHandler, which emits on brand change for any non-empty field), so
    // a 3-digit CVV is flagged the moment the number reveals Amex.
    const revalidate = React.useCallback(async () => {
      const { isValid, errorMessage, length } = await validate(name);
      if (length === 0) return; // not filled yet — nothing to surface
      updateFieldValidity({ name, isValid });
      if (!isDirtyRef.current) {
        setIsDirty(true);
        isDirtyRef.current = true;
      }
      if (!isValid !== isErrorRef.current) {
        setIsError(!isValid);
        isErrorRef.current = !isValid;
        onErrorChange?.({ isValid, errorMessage });
      }
    }, [name, updateFieldValidity, onErrorChange]);

    // Only the CVV re-validates when the brand changes (its length is
    // brand-dependent). useUpdateEffect skips the first render, so this runs only
    // on actual brand changes.
    useUpdateEffect(() => {
      if (name === 'cvv') revalidate();
    }, [brand]);

    return (
      <TextInput
        ref={textInputRef}
        value={value}
        autoCapitalize="none"
        autoComplete={autoCompleteMap[name]}
        inputMode={name !== 'cardHolderName' ? 'numeric' : 'text'}
        onFocus={() => {
          _onFocus();
          onFocus?.();
        }}
        onBlur={async () => {
          _onBlur();
          onBlur?.();

          // Re-validate (only) on every blur so the field's error is always
          // consistent with its current value — leaving any field flags it red
          // when invalid, clears it when valid. The value was already stored by
          // the last text-change `update`, so no update is needed here.
          if (!isDirtyRef.current) {
            setIsDirty(true);
            isDirtyRef.current = true;
          }
          const { isValid, errorMessage } = await validate(name);
          updateFieldValidity({ name, isValid });
          if (!isValid !== isErrorRef.current) {
            setIsError(!isValid);
            isErrorRef.current = !isValid;
            onErrorChange?.({ isValid, errorMessage });
          }
        }}
        style={
          typeof style === 'function'
            ? style({ isFocused, isDirty, isError })
            : style
        }
        {...props}
        secureTextEntry={name === 'cvv' && maskCvv === true}
        onChangeText={async (updatedValue) => {
          setValue(updatedValue);
          valueRef.current = updatedValue;

          // Text changed: update (store) first, then validate to read state.
          await update(name, updatedValue);
          const { isValid, errorMessage, cardBrand, formattedValue, length } =
            await validate(name);

          setValue(formattedValue);
          valueRef.current = formattedValue;

          if (name === 'cardNumber') {
            if (
              cardBrand?.brand !== cardInfo?.brand ||
              cardBrand?.first6Digits !== cardInfo?.first6Digits
            ) {
              setCardInfo(cardBrand);
              onCardBrandChange?.(cardBrand);
              // Share the new brand so the CVV re-validates (its length is
              // brand-dependent, e.g. Amex needs 4 digits).
              setBrand(cardBrand?.brand);
            }
          }

          // Once a field has been interacted with (blurred at least once), keep
          // its error in sync as the user edits. Same rule for every field.
          if (isDirtyRef.current && !isValid !== isErrorRef.current) {
            setIsError(!isValid);
            isErrorRef.current = !isValid;
            onErrorChange?.({ isValid, errorMessage });
          }

          updateFieldValidity({
            name,
            isValid,
          });

          onChange?.({
            isValid,
            length,
            errorMessage,
          });
        }}
      />
    );
  }
);
