import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { matchIsValidTel } from 'mui-tel-input';
import BooleanPicker from '../BooleanPicker';
import EnumPicker from '../EnumPicker';
import Loading from '../Loading';
import PhoneTextField from '../PhoneTextField';
import ModelAutocomplete from '../ModelAutocomplete';
import { useApp } from '../app';

const lcFirst = str => `${str[0].toLowerCase()}${str.slice(1)}`;

const intl = {
  es: {
    'submit-text': 'Guardar',
    'cancel-text': 'Cancelar',
  },
  pt: {
    'submit-text': 'Salvar',
    'cancel-text': 'Cancelar',
  },
};

const ModelForm = ({
  name,
  id,
  fields = {},
  defaults = {},
  onSave = null,
  title,
  submitText,
  cancelText,
  labels = {},
  errorTexts = {},
  helperTexts = {},
  lang
}) => {
  const navigate = useNavigate();
  const [errors, setErrors] = useState({});
  const [data, setData] = useState(defaults);
  const app = useApp();
  const model = app[name];
  const primaryKey = model.schema.primaryKey || 'id';

  useEffect(() => {
    if (!id) {
      return;
    }
    model.findById(id, {
      ...(!!model.relations.oneToOne.length && {
        include: model.relations.oneToOne.reduce(
          (memo, k) => Object.assign(memo, { [k]: true }),
          {},
        ),
      }),
    })
      .then(setData)
      .catch(console.error);
  }, [app, model, id]);

  if (id && !data[primaryKey]) {
    return <Loading />;
  }

  const createSetDataHandler = key => (value) => {
    setErrors(errors => ({
      ...errors,
      [key]: model.validateAttr(key, value),
    }));
    setData(d => ({ ...d, [key]: value }));
  };

  const buildLabelText = (schema, key) => (
    schema.label
    || key[0].toUpperCase().concat(
      key.slice(1).replace(/([A-Z])/g, ' $1').toLowerCase(),
    )
  );

  const buildTextField = (schema, key, onChange, helperText) => {
    const { inputType, isRequired } = schema;
    return (
      <TextField
        multiline={inputType === 'textarea'}
        type={inputType}
        label={labels[key] || buildLabelText(schema, key)}
        value={
          (inputType === 'date' && data[key])
            ? data[key].toISOString().slice(0, 10)
            : inputType === 'number'
              ? (typeof data[key] === 'number' ? data[key] : '')
              : (data[key] || '')
        }
        error={!!errors[key]}
        helperText={helperText}
        required={!!isRequired}
        InputLabelProps={{ shrink: true }}
        onChange={e => {
          onChange(
            inputType === 'number'
              ? parseInt(e.target.value, 10)
              : inputType === 'date'
                ? (e.target.value && !isNaN(new Date(e.target.value)) ? new Date(e.target.value) : null)
                : e.target.value
          );
          // TODO: reemplazar con CustomTextField?
          if (e.target.validity?.valueMissing) {
            return setErrors(errors => ({
              ...errors,
              [key]: { id: 'value-missing-validation-error' },
            }));
          }
          if (e.target.validity?.typeMismatch) {
            return setErrors(errors => ({
              ...errors,
              [key]: { id: 'type-missmatch-validation-error' },
            }));
          }
          if (e.target.validationMessage) {
            setErrors(errors => ({
              ...errors,
              [key]: e.target.validationMessage,
            }));
          }
        }}
      />
    );
  };

  const buildBoolSwitch = (schema, key, onChange, helperText) => (
    <BooleanPicker
      label={labels[key] || buildLabelText(schema, key)}
      value={data[key]}
      onChange={onChange}
      error={errors[key]}
      helperText={helperText}
      required={schema.isRequired}
      lang={lang}
    />
  );

  const buildSelect = (schema, key, onChange) => (
    <EnumPicker
      label={labels[key] || buildLabelText(schema, key)}
      model={model}
      field={key}
      value={data[key]}
      onChange={onChange}
      error={errors[key]}
    />
  );

  const buildRelationField = (schema, key, onChange, helperText) => {
    const relationModel = lcFirst(schema.$ref.split('/').pop());
    return (
      <ModelAutocomplete
        label={labels[key] || buildLabelText(schema, key)}
        model={app[relationModel]}
        value={data[key]}
        onChange={onChange}
        error={!!errors[key]}
        helperText={helperText}
        required={!!schema.required}
      />
    );
  };

  const buildPhoneInput = (schema, key, onChange, helperText) => {
    return (
      <PhoneTextField
        label={labels[key] || buildLabelText(schema, key)}
        value={data[key]}
        onChange={onChange}
        error={!!errors[key]}
        helperText={helperText}
        required={!!schema.required}
      />
    );
  };

  const buildField = (key) => {
    const schema = model.schema.properties[key] || {};
    const onChange = createSetDataHandler(key);

    if (typeof fields[key] === 'function') {
      return fields[key](key, data, setData, errors, setErrors);
    }

    if (key === primaryKey) {
      return null;
    }

    const error = errors[key];
    const helperText = (
      !error
        ? helperTexts[key]
        : error?.id
          ? errorTexts[error.id] || error.id
          : errorTexts[error] || error
    );

    if (schema.enum) {
      return buildSelect(schema, key, onChange);
    }

    if (schema.$ref) {
      return buildRelationField(schema, key, onChange, helperText);
    }

    // Optional relations have `anyOf` prop, an  array with the relation and a
    // null indicating it is optional.
    if (
      Array.isArray(schema.anyOf)
      && schema.anyOf[0].$ref
      && schema.anyOf[1].type === 'null'
    ) {
      return buildRelationField(
        { ...schema, $ref: schema.anyOf[0].$ref },
        key,
        onChange,
        helperText,
      );
    }

    switch (schema.inputType) {
      case 'boolean':
        return buildBoolSwitch(schema, key, onChange, helperText);
      case 'array':
        return null;
      case 'phone':
        return buildPhoneInput(schema, key, onChange, helperText);
      case 'date':
      case 'number':
      case 'text':
      case 'textarea':
      default:
        return buildTextField(schema, key, onChange, helperText);
    }
  };

  const onSubmit = () => {
    // Validate phone inputType
    const { properties } = model.schema;
    const errors = Object.keys(properties).reduce(
      (memo, key) => {
        if (
          properties[key].inputType !== 'phone'
          || typeof data[key] !== 'string'
        ) {
          return memo;
        }

        if (!matchIsValidTel(data[key])) {
          return {
            ...memo,
            [key]: { id: 'invalid-phone-number' },
          };
        }

        return memo;
      },
      model.validate(data),
    );

    if (Object.keys(errors).length) {
      return setErrors(prev => ({ ...prev, ...errors }));
    }

    const serialized = Object.keys(data).reduce(
      (memo, key) => {
        if (model.relations.oneToOne.includes(key) && data[key]) {
          const ref = (
            model.relations.optionalOneToOne.includes(key)
              ? properties[key].anyOf[0].$ref
              : properties[key].$ref
          );
          const relationModel = lcFirst(ref.split('/').pop());
          const relationModelPrimaryKey = app[relationModel].schema.primaryKey;
          return Object.assign(memo, {
            [key]: {
              connect: {
                [relationModelPrimaryKey]: data[key][relationModelPrimaryKey],
              },
            },
          });
        }
        return Object.assign(memo, { [key]: data[key] });
      },
      {},
    );

    model.upsert({
      where: { [primaryKey]: id },
      update: serialized,
      create: serialized,
    })
      .then((obj) => {
        if (typeof onSave === 'function') {
          return onSave(obj);
        }
      })
      .catch((err) => {
        const { meta, message } = err.body;
        if (meta && meta.target && meta.target.length) {
          return setErrors(errors => ({
            ...errors,
            [meta.target[0]]: message,
          }));
        }
        alert(message);
      });
  };

  const inputProps = (
    model.schema.inputProps || Object.keys(model.schema.properties)
  );

  return (
    <>
      <Typography variant="h1">
        {title || (`${id ? 'Edit' : 'New'} ${name}`)}
      </Typography>
      <Grid container spacing={3} direction="column">
        {inputProps.map((key) => {
          const field = buildField(key);
          return field && <Grid key={key} item>{field}</Grid>;
        })}
        <Grid item>
          <Button color="secondary" onClick={() => navigate(-1)}>
            {cancelText || (intl[lang] || {})['cancel-text'] || 'Cancel'}
          </Button>
          &nbsp;
          <Button color="primary" onClick={onSubmit}>
            {submitText || (intl[lang] || {})['submit-text'] || 'Submit'}
          </Button>
        </Grid>
      </Grid>
    </>
  );
};

export default ModelForm;
