import { StyleSheet, Text, TextInput, View } from 'react-native';
import MaskInput from 'react-native-mask-input';
import { CountryPicker } from '../components/CountryPicker';
import { FormField } from '../components/FormField';
import { getCreditCardBrand } from '../utils/getCreditCardBrand';
import { getPostcodeLabelFn } from '../utils/getPostcodeLabelFn';
import { getPostcodePlaceholderFn } from '../utils/getPostcodePlaceholderFn';
import { newExpiryDate } from '../utils/newExpiryDate';
import {
  validateFieldFn,
  type KeyToValidate,
  type VirtualFields,
} from '../utils/validateFieldFn';
import type {
  CreateCardForTemporaryUsePayload,
  DuffelCardFormProps,
  DuffelCardFormStrings,
  SaveCardPayload,
} from '../../../index';
import React from 'react';

export type ConditionalFormPayload<T_HideCVC extends boolean = false> =
  T_HideCVC extends true ? SaveCardPayload : CreateCardForTemporaryUsePayload;

export interface CreateCardForTemporaryUseFormProps<T_HideCVC extends boolean> {
  hideCVC?: T_HideCVC;

  data: ConditionalFormPayload<T_HideCVC> & VirtualFields;
  onChange: (data: ConditionalFormPayload<T_HideCVC> & VirtualFields) => void;

  onValidateSuccess: DuffelCardFormProps['onValidateSuccess'];
  onValidateFailure: DuffelCardFormProps['onValidateFailure'];

  styles: DuffelCardFormProps['styles'];

  customStrings: DuffelCardFormStrings;
}

export interface FieldState {
  touched: boolean;
  valid: boolean;
  error: null | string;
}

type Fields = Record<KeyToValidate, FieldState>;

const creditCardMask = [
  /\d/,
  /\d/,
  /\d/,
  /\d/,
  ' ',
  /\d/,
  /\d/,
  /\d/,
  /\d/,
  ' ',
  /\d/,
  /\d/,
  /\d/,
  /\d/,
  ' ',
  /\d/,
  /\d/,
  /\d/,
  /\d/,
];

const expirationDateMask = [/\d/, /\d/, '/', /\d/, /\d/];

export function CreateCardForTemporaryUseForm<
  T_hideCVC extends boolean = false,
>({
  hideCVC,
  data,
  onChange,
  onValidateSuccess,
  onValidateFailure,
  styles,
  customStrings,
}: CreateCardForTemporaryUseFormProps<T_hideCVC>) {
  const [hasEmittedValidationState, setHasEmittedValidationState] =
    React.useState(false);

  const initialFieldsObject: Fields = {
    ...copyObjectWithFieldValue(
      // Assertion is ok here because we want the most complete object, not a variable one
      data as CreateCardForTemporaryUsePayload,
      { touched: false, valid: false, error: '' },
      [
        // `multi_use` and `brand` are not in the form, just the payload,
        // these are already part of the initial values
        'multi_use',
        'brand',

        // the form will have a better representation of these values as a Date
        'expiry_month',
        'expiry_year',

        // we exclude the cvc field if it's not rendered on the form
        ...(hideCVC ? ['cvc' as const] : []),
      ]
    ),
    // address_line_2 doesn't go through validation, so we can set valid to true
    address_line_2: { valid: true, touched: false, error: '' },
    expiry_date: { valid: false, touched: false, error: '' },
  };

  const [fields, setFields] = React.useState<Fields>(initialFieldsObject);

  function setFieldValidationState(
    fieldName: KeyToValidate,
    valid: boolean,
    error: string | null
  ): Fields {
    const fieldState = fields[fieldName];
    const newState = {
      ...fields,
      [fieldName]: { ...fieldState, valid, error },
    };

    setFields(newState);
    return newState;
  }

  function setFieldTouched(fieldName: KeyToValidate, fieldsState = fields) {
    const fieldState = fieldsState[fieldName];
    const newState = {
      ...fieldsState,
      [fieldName]: { ...fieldState, touched: true },
    };
    setFields(newState);
    return newState;
  }

  function maybeEmitValidationState(fieldsState: Fields) {
    if (Object.values(fieldsState).every((field) => field.valid)) {
      onValidateSuccess();
      setHasEmittedValidationState(true);
    }

    if (
      hasEmittedValidationState &&
      Object.values(fieldsState).some((field) => !field.valid)
    ) {
      onValidateFailure();
    }
  }

  const validateField = validateFieldFn(customStrings);

  function validate(
    fieldName: KeyToValidate,
    dataState: ConditionalFormPayload<T_hideCVC> & VirtualFields
  ) {
    const { valid, error } = validateField(fieldName, dataState);
    const newFieldsState = setFieldValidationState(fieldName, valid, error);
    maybeEmitValidationState(newFieldsState);
  }

  const getPostcodeLabel = getPostcodeLabelFn(customStrings);
  const getPostcodePlaceholder = getPostcodePlaceholderFn(customStrings);

  return (
    <View style={styles?.formContainer}>
      <FormField
        label={customStrings.cardNumberLabel}
        error={
          fields.number.touched && fields.number.error !== null
            ? fields.number.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <MaskInput
          testID="card-number"
          autoComplete="cc-number"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.number}
          onChangeText={(_masked, unmasked) => {
            const newState = {
              ...data,
              number: unmasked,
              brand: getCreditCardBrand(unmasked),
            };
            onChange(newState);
            validate('number', newState);
          }}
          onEndEditing={() => {
            validate('number', data);
            if (data.number !== '') {
              setFieldTouched('number');
            }
          }}
          mask={creditCardMask}
          placeholder={customStrings.cardNumberPlaceholder}
        />
      </FormField>

      <FormField
        label={customStrings.expirationDateLabel}
        error={
          fields.expiry_date.touched && fields.expiry_date.error !== null
            ? fields.expiry_date.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <MaskInput
          testID="expiration-date"
          autoComplete="cc-exp"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.expiry_month + '/' + data.expiry_year}
          onChangeText={(masked, _unmasked) => {
            const [expiry_month, expiry_year] = masked.split('/');
            const newState = {
              ...data,
              expiry_month: expiry_month || '',
              expiry_year: expiry_year || '',
              expiry_date: newExpiryDate(expiry_month, expiry_year),
            };

            onChange(newState);
            validate('expiry_date', newState);
          }}
          onEndEditing={() => {
            validate('expiry_date', data);

            if (data.expiry_month !== '') {
              setFieldTouched('expiry_date');
            }
          }}
          placeholder={customStrings.expirationDatePlaceholder}
          mask={expirationDateMask}
        />
      </FormField>

      {!hideCVC && (
        <FormField
          label={customStrings.cvvLabel}
          error={
            fields.cvc.touched && fields.cvc.error !== null
              ? fields.cvc.error
              : ''
          }
          labelStyles={styles?.label}
          formFieldStyle={styles?.formField}
          errorMessageStyles={styles?.errorMessage}
        >
          <TextInput
            testID="cvc"
            autoComplete="cc-csc"
            keyboardType="numeric"
            autoCorrect={false}
            style={[defaultStyles.formControl, styles?.input]}
            // Assertion is ok here because the guard above means
            // that the data is of the correct type, typescript just can't infer that.
            value={(data as CreateCardForTemporaryUsePayload).cvc}
            onChange={(e) => {
              const newState = { ...data, cvc: e.nativeEvent.text };
              onChange(newState);
              validate('cvc', newState);
            }}
            onEndEditing={() => {
              validate('cvc', data);
              if ((data as CreateCardForTemporaryUsePayload).cvc !== '') {
                setFieldTouched('cvc');
              }
            }}
            placeholder={customStrings.cvvPlaceholder}
          />
        </FormField>
      )}

      <FormField
        label={customStrings.nameLabel}
        error={
          fields.name.touched && fields.name.error !== null
            ? fields.name.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="name"
          autoComplete="name"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.name}
          onChange={(e) => {
            const newState = { ...data, name: e.nativeEvent.text };
            onChange(newState);
            validate('name', newState);
          }}
          onEndEditing={() => {
            validate('name', data);
            if (data.name !== '') {
              setFieldTouched('name');
            }
          }}
          placeholder={customStrings.namePlaceholder}
        />
      </FormField>

      <Text style={[defaultStyles.sectionTitle, styles?.sectionTitle]}>
        {customStrings.billingAddressSectionTitle}
      </Text>

      <FormField
        label={customStrings.countryLabel}
        error={
          fields.address_country_code.touched &&
          fields.address_country_code.error !== null
            ? fields.address_country_code.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <CountryPicker
          style={{
            viewContainer: defaultStyles.formControl,
            ...(styles ? styles?.countryPicker : {}),
          }}
          value={data.address_country_code}
          onChange={(address_country_code) => {
            const newState = { ...data, address_country_code };
            onChange(newState);
            setFieldTouched('address_country_code');

            if (data.address_postal_code !== '') {
              validate('address_postal_code', newState);
            }
          }}
          onClose={() => {
            validate('address_country_code', data);
          }}
        />
      </FormField>

      <FormField
        label={customStrings.addressLine1Label}
        error={
          fields.address_line_1.touched && fields.address_line_1.error !== null
            ? fields.address_line_1.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="address-line-1"
          autoComplete="address-line1"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.address_line_1}
          onChange={(e) => {
            const newState = {
              ...data,
              address_line_1: e.nativeEvent.text,
            };
            onChange(newState);
            validate('address_line_1', newState);
          }}
          onEndEditing={() => {
            validate('address_line_1', data);
            if (data.address_line_1 !== '') {
              setFieldTouched('address_line_1');
            }
          }}
          placeholder={customStrings.addressLine1Placeholder}
        />
      </FormField>

      <FormField
        label={customStrings.addressLine2Label}
        error={
          fields.address_line_2.touched && fields.address_line_2.error !== null
            ? fields.address_line_2.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="address-line-2"
          autoComplete="address-line2"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.address_line_2}
          onChange={(e) => {
            const newState = {
              ...data,
              address_line_2: e.nativeEvent.text,
            };
            onChange(newState);
            validate('address_line_2', newState);
          }}
          onEndEditing={() => {
            validate('address_line_2', data);
            if (data.address_line_2 !== '') {
              setFieldTouched('address_line_2');
            }
          }}
          placeholder={customStrings.addressLine2Placeholder}
        />
      </FormField>

      <FormField
        label={customStrings.cityLabel}
        error={
          fields.address_city.touched && fields.address_city.error !== null
            ? fields.address_city.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="address-city"
          autoComplete="postal-address-locality"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.address_city}
          onChange={(e) => {
            const newState = { ...data, address_city: e.nativeEvent.text };
            onChange(newState);
            validate('address_city', newState);
          }}
          onEndEditing={() => {
            validate('address_city', data);
            if (data.address_city !== '') {
              setFieldTouched('address_city');
            }
          }}
          placeholder={customStrings.cityPlaceholder}
        />
      </FormField>

      <FormField
        label={getPostcodeLabel(data.address_country_code)}
        error={
          fields.address_postal_code.touched &&
          fields.address_postal_code.error !== null
            ? fields.address_postal_code.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="address-postal-code"
          autoComplete="postal-code"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.address_postal_code}
          onChange={(e) => {
            const newState = {
              ...data,
              address_postal_code: e.nativeEvent.text,
            };
            onChange(newState);
            validate('address_postal_code', newState);
          }}
          onEndEditing={() => {
            validate('address_postal_code', data);
            if (data.address_postal_code !== '') {
              setFieldTouched('address_postal_code');
            }
          }}
          placeholder={getPostcodePlaceholder(data.address_country_code)}
        />
      </FormField>

      <FormField
        label={customStrings.regionLabel}
        error={
          fields.address_region.touched && fields.address_region.error !== null
            ? fields.address_region.error
            : ''
        }
        labelStyles={styles?.label}
        formFieldStyle={styles?.formField}
        errorMessageStyles={styles?.errorMessage}
      >
        <TextInput
          testID="address-region"
          autoComplete="postal-address-region"
          autoCorrect={false}
          style={[defaultStyles.formControl, styles?.input]}
          value={data.address_region}
          onChange={(e) => {
            const newState = {
              ...data,
              address_region: e.nativeEvent.text,
            };
            onChange(newState);
            validate('address_region', newState);
          }}
          onEndEditing={() => {
            validate('address_region', data);
            if (data.address_region !== '') {
              setFieldTouched('address_region');
            }
          }}
          placeholder={customStrings.regionPlaceholder}
        />
      </FormField>
    </View>
  );
}

function copyObjectWithFieldValue<T_Object extends object, T_Value>(
  data: T_Object,
  fieldValue: T_Value,
  excludeKeys: (keyof T_Object)[] = []
): Record<keyof T_Object, T_Value> {
  return Object.keys(data)
    .filter((key) => !excludeKeys.includes(key as keyof T_Object))
    .reduce(
      (acc, key) => ({ ...acc, [key]: fieldValue }),
      {} as Record<keyof T_Object, T_Value>
    );
}

const defaultStyles = StyleSheet.create({
  formControl: {
    fontSize: 16,
    borderColor: 'lightgrey',
    borderWidth: 1,
    borderStyle: 'solid',
    paddingTop: 6,
    marginBottom: 6,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 20,
  },
});
