import PropTypes from 'prop-types';
import { and, assocPath, omit, path, prop, reduce, values } from 'ramda';
import React, { Component } from 'react';
import { Form } from 'semantic-ui-react';

import { withSite } from '../../addons/Site';
import { validateEmail } from '../../lib';
import Context from './Context';

/* Interface */
const propTypes = {
  ...Form.propTypes,

  /** Identity provider to use for resetting the password. */
  identityProvider: PropTypes.string,

  /** Marks the form as invalid (incomplete). */
  invalid: PropTypes.bool,

  /** Event handler called when registration is successful. */
  onRegister: PropTypes.func,

  /**
   * StoryShare `Site`.
   * @private
   */
  site: PropTypes.object.isRequired,
};

/* Defaults */
const defaultProps = {
  identityProvider: 'email',
};

/**
 * A register form
 * Renders an SUI `<Form>` that can contain other components to build the
 * dynamic form.
 * Register forms must include the following components:
 *  - `<RegisterForm.EmailInput>`
 *  - `<RegisterForm.PasswordInput>`
 *  - `<RegisterForm.FirstNameInput>`
 *  - `<RegisterForm.LastNameInput>`
 * We recommend including the following components to display error details:
 *  - `<RegisterForm.AlreadyRegisteredMessage>`
 *  - `<RegisterForm.ErrorMessage>`
 *  - `<RegisterForm.InvalidEmailMessage>`
 * Any submit button will work, but `<RegisterForm.Button>` disables itself
 * until the required data is entered.
 * You can also include the following components to enhance your form:
 *  - `<RegisterForm.Checkbox>`, a checkbox that must be ticked to proceed
 *  - `<RegisterForm.PasswordStrength>`, a password strength meter
 * All props are passed down to SUI's `<Form>`
 * @returns {Object} component
 */
export class RegisterForm extends Component {
  /**
   * Handles input changes by updating value in local state
   * The following components have `<Input>` boxes that need to propagate their
   * changes to the top level:
   *  - `<RegisterForm.EmailInput>`
   *  - `<RegisterForm.PasswordInput>`
   *  - `<RegisterForm.FirstNameInput>`
   *  - `<RegisterForm.LastNameInput>`
   * @param {ChangeEvent} event - React's original SyntheticEvent.
   * @param {Object} data - All props and a proposed value.
   * @param {string} data.name - `<Input>` field name.
   * @param {value} data.value - Proposed value.
   * @returns {void}
   */
  handleChange = (event, { name, value }) => this.setState({ [name]: value });

  /**
   * Handles changes to checkbox state
   * Used by `<RegisterForm.Checkbox>` to report checkbox changes.
   * @param {SyntheticEvent} event - React's original SyntheticEvent.
   * @param {Object} data - All props and proposed checked state.
   * @param {bool} data.checked - Proposed state.
   * @param {string} data.name - `<Checkbox>` field name.
   * @returns {void}
   */
  handleCheckboxChange = (event, { checked, name }) => this.setState(
    (state) => assocPath(['checkboxes', name], checked, state)
  );

  /**
   * Handles password validation information
   * Used by `<RegisterForm.PasswordStrength>` to report password strength.
   * @param {bool} isValid - Whether the password is valid.
   * @returns {void}
   */
  handlePasswordValidation = (isValid) =>
    this.setState({ isPasswordValid: isValid });

  /**
   * Checks whether form data is correctly entered
   * Used internally before submission, but also by `<RegisterForm.Button>`
   * which disables itself when the form is incomplete.
   * @returns {bool} whether the form is complete
   */
  isFormValid = () => {
    const { invalid } = this.props;
    const {
      checkboxes,
      email,
      firstName,
      isPasswordValid,
      lastName,
      password,
    } = this.state;

    return !!(
      !invalid
        && reduce(and, true, values(checkboxes)) // all checkboxes ticked
        && email.trim()
        && firstName.trim()
        && isPasswordValid
        && lastName.trim()
        && password
    );
  };

  /**
   * Component state
   * Also provides the public API to child components using `Context`.
   * N.B.: must be defined _after_ `handleChange`, `handleCheckboxChange`,
   * `handlePasswordValidation` and `isFormValid` as it
   * exposes those class properties but they aren't hoisted.
   */
  state = {
    /**
     * Checkbox states
     * May contain an arbitrary number of checkboxes indexed by `name`.
     * Used by `<RegisterForm.Checkbox>`
     * @public
     * @type {Object}
     */
    checkboxes: { },

    /**
     * Entered email address
     * Used by `<RegisterForm.EmailInput>`
     * @public
     * @type {string}
     */
    email: '',

    /**
     * Error message
     * Used by the following components:
     *  - `<RegisterForm.AlreadyRegisteredMessage>`
     *  - `<RegisterForm.ErrorMessage>`
     *  - `<RegisterForm.InvalidEmailMessage>`
     * @public
     * @type {'email'|'error'|'registered'|null}
     */
    error: null,

    /**
     * Entered first name
     * Used by `<RegisterForm.FirstNameInput>`
     * @public
     * @type {string}
     */
    firstName: '',

    /**
     * Handles input changes by updating value in local state
     * Used by the following components:
     *  - `<RegisterForm.EmailInput>`
     *  - `<RegisterForm.PasswordInput>`
     *  - `<RegisterForm.FirstNameInput>`
     *  - `<RegisterForm.LastNameInput>`
     * @public
     * @type {func}
     */
    handleChange: this.handleChange,

    /**
     * Handles changes to checkbox state
     * Used by `<RegisterForm.Checkbox>` to report checkbox changes.
     * @public
     * @type {func}
     */
    handleCheckboxChange: this.handleCheckboxChange,

    /**
     * Handles password validation information
     * Used by `<RegisterForm.PasswordStrength>` to report password strength.
     * @public
     */
    handlePasswordValidation: this.handlePasswordValidation,

    /**
     * Checks whether the email and password are correctly entered
     * Exposed in `state` for use by `<LoginForm.Button>`.
     * @public
     * @type {func}
     */
    isFormValid: this.isFormValid,

    /**
     * Whether or not we're in the process of registering
     * @private
     * @type {bool}
     */
    isLoading: false,

    /**
     * Whether or not the password is valid
     * Defaults to `true` in case there's no strength meter in use.
     * @private
     * @type {bool}
     */
    isPasswordValid: true,

    /**
     * Entered last name
     * Used by `<RegisterForm.LastNameInput>`
     * @public
     * @type {string}
     */
    lastName: '',

    /**
     * Entered password
     * Used by `<RegisterForm.PasswordInput>`
     * @public
     * @type {string}
     */
    password: '',
  };

  /**
   * Handles form submission (runs registration)
   * Calls the `onRegister` event handler when complete.
   * @returns {Promise<void>} resolves when complete
   */
  handleSubmit = async () => {
    if (!this.isFormValid()) {
      return;
    }

    // New submission attempt: reset error state and display loading indicator
    this.setState({ error: null, isLoading: true });

    const { identityProvider, onRegister, site } = this.props;
    const { email, firstName, lastName, password } = this.state;

    if (!validateEmail(email.trim())) {
      this.setState({ error: 'email', isLoading: false });
      return;
    }

    try {
      const registration = await site.req(
        `identityprovider/${identityProvider}/register`
      )
        .setParams({
          email: email.trim(),
          first_name: firstName.trim(),
          last_name: lastName.trim(),
          password,
        })
        .promise()
        .create();

      const token = prop('token', registration);
      if (!token) {
        this.setState({ error: 'error', isLoading: false });
        return;
      }

      await site.login({ jwt: token });

      this.setState({ isLoading: false });
      if (onRegister) {
        onRegister();
      }
    } catch (err) {
      if (path(['body', 'code'], err) === 'user_exists') {
        this.setState({ error: 'registered', isLoading: false });
      } else {
        this.setState({ error: 'error', isLoading: false });
      }
    }
  };

  render = () => (
    <Context.Provider value={ this.state }>
      <Form
        { ...omit([
          'identityProvider',
          'invalid',
          'onRegister',
          'site',
        ], this.props) }
        error={ this.props.error || !!this.state.error }
        loading={ this.props.loading || this.state.isLoading }
        onSubmit={ this.handleSubmit }
      />
    </Context.Provider>
  );
}

RegisterForm.defaultProps = defaultProps;
RegisterForm.propTypes = propTypes;

export default withSite(RegisterForm);
