/**
 * Users controlpanel container.
 * @module components/manage/Controlpanels/UsersControlpanel
 */
import {
  createUser,
  deleteUser,
  listUsers,
  updateUser,
  getUser,
} from '@plone/volto/actions/users/users';
import { listRoles } from '@plone/volto/actions/roles/roles';
import { listGroups, updateGroup } from '@plone/volto/actions/groups/groups';
import { getControlpanel } from '@plone/volto/actions/controlpanels/controlpanels';
import { getUserSchema } from '@plone/volto/actions/userschema/userschema';
import { asyncConnect } from '@plone/volto/helpers/AsyncConnect';
import jwtDecode from 'jwt-decode';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import Pagination from '@plone/volto/components/theme/Pagination/Pagination';
import Error from '@plone/volto/components/theme/Error/Error';
import { ModalForm } from '@plone/volto/components/manage/Form';
import RenderUsers from '@plone/volto/components/manage/Controlpanels/Users/RenderUsers';
import { Link } from 'react-router-dom';
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
import { messages } from '@plone/volto/helpers/MessageLabels/MessageLabels';
import { isManager, canAssignGroup } from '@plone/volto/helpers/User/User';
import { getErrorMessage } from '@plone/volto/helpers/Utils/Utils';
import clearSVG from '@plone/volto/icons/clear.svg';
import addUserSvg from '@plone/volto/icons/add-user.svg';
import saveSVG from '@plone/volto/icons/save.svg';
import ploneSVG from '@plone/volto/icons/plone.svg';
import find from 'lodash/find';
import map from 'lodash/map';
import pull from 'lodash/pull';
import difference from 'lodash/difference';

import { useState, useEffect, useCallback } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { createPortal } from 'react-dom';
import { useSelector, useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import {
  Confirm,
  Container,
  Form,
  Input,
  Button,
  Dimmer,
  Segment,
  Table,
  Loader,
} from 'semantic-ui-react';

/**
 * UsersControlpanel functional component.
 * @function UsersControlpanel
 */
const UsersControlpanel = (props) => {
  const { staticContext } = props;
  const intl = useIntl();
  const dispatch = useDispatch();

  // Redux state selectors
  const roles = useSelector((state) => state.roles.roles);
  const users = useSelector((state) => state.users.users);
  const user = useSelector((state) => state.users.user);
  const userId = useSelector((state) =>
    state.userSession.token ? jwtDecode(state.userSession.token).sub : '',
  );
  const groups = useSelector((state) => state.groups.groups);
  const many_users = useSelector(
    (state) => state.controlpanels?.controlpanel?.data?.many_users,
  );

  const location = useLocation();
  const pathname = location.pathname;
  const deleteRequest = useSelector((state) => state.users.delete);
  const createRequest = useSelector((state) => state.users.create);
  const loadRolesRequest = useSelector((state) => state.roles);
  const inheritedRole = useSelector(
    (state) => state.authRole.authenticatedRole,
  );
  const userschema = useSelector((state) => state.userschema);
  const controlPanelData = useSelector(
    (state) => state.controlpanels?.controlpanel,
  );

  // Action creators
  const listRolesAction = useCallback(() => dispatch(listRoles()), [dispatch]);
  const listUsersAction = useCallback(
    (params) => dispatch(listUsers(params)),
    [dispatch],
  );
  const listGroupsAction = useCallback(
    () => dispatch(listGroups()),
    [dispatch],
  );
  const getControlpanelAction = useCallback(
    (panel) => dispatch(getControlpanel(panel)),
    [dispatch],
  );
  const deleteUserAction = useCallback(
    (userId) => dispatch(deleteUser(userId)),
    [dispatch],
  );
  const updateUserAction = useCallback(
    (userId, data) => dispatch(updateUser(userId, data)),
    [dispatch],
  );
  const updateGroupAction = useCallback(
    (groupId, data) => dispatch(updateGroup(groupId, data)),
    [dispatch],
  );
  const getUserSchemaAction = useCallback(
    () => dispatch(getUserSchema()),
    [dispatch],
  );
  const getUserAction = useCallback(
    (userId) => dispatch(getUser(userId)),
    [dispatch],
  );

  const [search, setSearch] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [showAddUser, setShowAddUser] = useState(false);
  const [addUserError, setAddUserError] = useState('');
  const [showDelete, setShowDelete] = useState(false);
  const [userToDelete, setUserToDelete] = useState(undefined);
  const [entries, setEntries] = useState([]);
  const [isClient, setIsClient] = useState(false);
  const [currentPage, setCurrentPage] = useState(0);
  const [pageSize] = useState(10);
  // eslint-disable-next-line no-unused-vars
  const [loginUsingEmail, setLoginUsingEmail] = useState(false); // Reserved for future use to disable username field when email login is enabled
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    await getControlpanelAction('usergroup');
    await listRolesAction();
    if (!many_users) {
      listGroupsAction();
      await listUsersAction();
      setEntries(users);
    }
    // Only fetch user schema if it hasn't been loaded yet (e.g. by asyncConnect SSR)
    if (!userschema?.loaded) {
      await getUserSchemaAction();
    }
    await getUserAction(userId);
  }, [
    getControlpanelAction,
    listRolesAction,
    many_users,
    listGroupsAction,
    listUsersAction,
    users,
    userschema,
    getUserSchemaAction,
    getUserAction,
    userId,
  ]);

  /**
   * Check login using email status from security control panel
   * @method checkLoginUsingEmailStatus
   * @returns {undefined}
   */
  const checkLoginUsingEmailStatus = useCallback(async () => {
    try {
      await getControlpanelAction('security');
      if (controlPanelData?.data?.use_email_as_login) {
        setLoginUsingEmail(controlPanelData.data.use_email_as_login);
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error('Error fetching security control panel', error);
    }
  }, [getControlpanelAction, controlPanelData]);

  const getUserFromProps = useCallback(
    (value) => {
      return find(users, ['@id', value]);
    },
    [users],
  );

  /**
   * Search handler
   * @method onSearch
   * @param {object} event Event object.
   * @returns {undefined}
   */
  const onSearch = useCallback(
    (event) => {
      event.preventDefault();
      setIsLoading(true);
      listUsersAction({
        search: search,
      })
        .then(() => {
          setIsLoading(false);
        })
        .catch((error) => {
          setIsLoading(false);
          // eslint-disable-next-line no-console
          console.error('Error searching users', error);
        });
    },
    [listUsersAction, search],
  );

  /**
   * On change search handler
   * @method onChangeSearch
   * @param {object} event Event object.
   * @returns {undefined}
   */
  const onChangeSearch = (event) => {
    setSearch(event.target.value);
  };

  /**
   * Handle delete user click
   * @method handleDeleteUser
   * @param {object} event Event object.
   * @param {string} value username.
   * @returns {undefined}
   */
  const handleDeleteUser = useCallback(
    (event, data) => {
      // Handle both formats: direct value from event target or object with value
      const value =
        data?.value || event?.target?.value || event?.currentTarget?.value;
      if (value) {
        setShowDelete(true);
        setUserToDelete(getUserFromProps(value));
      }
    },
    [getUserFromProps],
  );

  /**
   * On delete ok
   * @method onDeleteOk
   * @returns {undefined}
   */
  const onDeleteOk = useCallback(() => {
    if (userToDelete) {
      const deleteAction = deleteUserAction(userToDelete.id);
      if (deleteAction && typeof deleteAction.then === 'function') {
        deleteAction
          .then(() => {
            // Handle success
            setUserToDelete(undefined);
            setShowDelete(false);

            // Refresh users list
            listUsersAction({ search: search });

            // Show success message
            toast.success(
              <Toast
                success
                title={intl.formatMessage(messages.success)}
                content={intl.formatMessage(messages.userDeleted)}
              />,
            );
          })
          .catch((error) => {
            // Handle error
            // eslint-disable-next-line no-console
            console.error('Error deleting user', error);
          });
      }
    }
  }, [userToDelete, deleteUserAction, listUsersAction, search, intl]);

  /**
   * On delete cancel
   * @method onDeleteCancel
   * @returns {undefined}
   */
  const onDeleteCancel = () => {
    setShowDelete(false);
    setUserToDelete(undefined);
  };

  /**
   *@param {object} user
   *@returns {undefined}
   *@memberof UsersControlpanel
   */
  const addUserToGroup = useCallback(
    (user) => {
      const { groups: userGroups, username } = user;
      userGroups.forEach((group) => {
        updateGroupAction(group, {
          users: {
            [username]: true,
          },
        });
      });
    },
    [updateGroupAction],
  );

  /**
   * Callback to be called by the ModalForm when the form is submitted.
   *
   * @param {object} data Form data from the ModalForm.
   * @param {func} callback to set new form data in the ModalForm
   * @returns {undefined}
   */
  const onAddUserSubmit = useCallback(
    (data, callback) => {
      const { groups: userGroups, sendPasswordReset, password } = data;
      if (
        sendPasswordReset !== undefined &&
        sendPasswordReset === true &&
        password !== undefined
      ) {
        toast.error(
          <Toast
            error
            title={intl.formatMessage(messages.error)}
            content={intl.formatMessage(
              messages.addUserFormPasswordAndSendPasswordTogetherNotAllowed,
            )}
          />,
        );
      } else {
        if (userGroups && userGroups.length > 0) addUserToGroup(data);

        const createUserAction = createUser(data, sendPasswordReset);
        dispatch(createUserAction)
          .then(() => {
            // Handle success
            if (callback) {
              callback({});
            }
            setShowAddUser(false);
            setAddUserError(undefined);

            // Refresh users list
            listUsersAction({ search: search });

            // Show success message
            toast.success(
              <Toast
                success
                title={intl.formatMessage(messages.success)}
                content={intl.formatMessage(messages.userCreated)}
              />,
            );
          })
          .catch((error) => {
            // Handle error
            setAddUserError(getErrorMessage(error));
          });
      }
    },
    [intl, addUserToGroup, dispatch, search, listUsersAction],
  );

  /**
   * Update user role
   * @param {*} name
   * @param {*} value
   */
  const updateUserRole = useCallback(
    (name, value) => {
      setEntries(
        map(entries, (entry) => ({
          ...entry,
          roles:
            entry.id === name && !entry.roles.includes(value)
              ? [...entry.roles, value]
              : entry.id !== name
                ? entry.roles
                : pull(entry.roles, value),
        })),
      );
    },
    [entries],
  );

  /**
   * Update user role submit
   * @param {*} event
   */
  const updateUserRoleSubmit = useCallback(
    (e) => {
      e.stopPropagation();

      const roleIds = roles.map((item) => item.id);
      entries.forEach((item) => {
        const userData = { roles: {} };
        const removedRoles = difference(roleIds, item.roles);

        removedRoles.forEach((role) => {
          userData.roles[role] = false;
        });
        item.roles.forEach((role) => {
          userData.roles[role] = true;
        });
        updateUserAction(item.id, userData);
      });
      toast.success(
        <Toast
          success
          title={intl.formatMessage(messages.success)}
          content={intl.formatMessage(messages.updateRoles)}
        />,
      );
    },
    [roles, entries, updateUserAction, intl],
  );

  /**
   * Handle Errors after createUser()
   *
   * @param {object} error object
   * @returns {undefined}
   */
  const onAddUserError = useCallback((error) => {
    setAddUserError(getErrorMessage(error));
  }, []);

  /**
   * On change page
   * @method onChangePage
   * @param {object} event Event object.
   * @param {string} value Page value.
   * @returns {undefined}
   */
  const onChangePage = (event, { value }) => {
    setCurrentPage(value);
  };

  /**
   * Filters the roles a user can assign when adding a user.
   * @method canAssignAdd
   * @returns {arry}
   */
  const canAssignAdd = useCallback(
    (isManager) => {
      if (isManager) return roles;
      return user?.roles
        ? roles.filter((role) => user.roles.includes(role.id))
        : [];
    },
    [roles, user],
  );

  useEffect(() => {
    setIsClient(true);
    // Skip fetching if the store already has an error.
    if (loadRolesRequest?.error) return;
    fetchData();
    checkLoginUsingEmailStatus();
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    setEntries(users);
  }, [users]);

  useEffect(() => {
    if (createRequest?.error && !createRequest?.loading) {
      onAddUserError(createRequest.error);
    }
  }, [createRequest?.error, createRequest?.loading, onAddUserError]);

  useEffect(() => {
    if (loadRolesRequest?.error && !loadRolesRequest?.loading) {
      setError(loadRolesRequest.error);
    }
  }, [loadRolesRequest?.error, loadRolesRequest?.loading]);

  const effectiveError =
    error ||
    (loadRolesRequest?.error && !loadRolesRequest?.loading
      ? loadRolesRequest.error
      : null);

  if (effectiveError) {
    return <Error error={effectiveError} staticContext={staticContext} />;
  }

  const usernameToDelete = userToDelete ? userToDelete.username : '';

  // Copy the userschema using JSON serialization/deserialization
  // this is really ugly, but if we don't do this the original value
  // of the userschema is changed and it is used like that through
  // the lifecycle of the application
  let adduserschema = {};
  let isUserManager = false;
  if (userschema?.loaded) {
    isUserManager = isManager(user);
    adduserschema = JSON.parse(JSON.stringify(userschema?.userschema));

    // Add custom form fields to the schema
    adduserschema.properties.username = {
      title: intl.formatMessage(messages.addUserFormUsernameTitle),
      type: 'string',
      description: intl.formatMessage(messages.addUserFormUsernameDescription),
    };

    adduserschema.properties.password = {
      title: intl.formatMessage(messages.addUserFormPasswordTitle),
      type: 'password',
      description: intl.formatMessage(messages.addUserFormPasswordDescription),
      widget: 'password',
    };

    adduserschema.properties.sendPasswordReset = {
      title: intl.formatMessage(messages.addUserFormSendPasswordResetTitle),
      type: 'boolean',
    };

    adduserschema.properties.roles = {
      title: intl.formatMessage(messages.addUserFormRolesTitle),
      type: 'array',
      choices: canAssignAdd(isUserManager).map((role) => [role.id, role.title]),
      noValueOption: false,
    };

    adduserschema.properties.groups = {
      title: intl.formatMessage(messages.addUserGroupNameTitle),
      type: 'array',
      choices: groups
        .filter((group) => canAssignGroup(isUserManager, group))
        .map((group) => [group.id, group.id]),
      noValueOption: false,
    };
    // Add custom fields to the first fieldset if they don't already exist
    if (
      adduserschema.fieldsets &&
      adduserschema.fieldsets.length > 0 &&
      !adduserschema.fieldsets[0].fields.includes('username')
    ) {
      adduserschema.fieldsets[0].fields =
        adduserschema.fieldsets[0].fields.concat([
          'username',
          'password',
          'sendPasswordReset',
          'roles',
          'groups',
        ]);
    }
  }

  return (
    <Container className="users-control-panel">
      <Helmet title={intl.formatMessage(messages.users)} />
      <div className="container">
        <Confirm
          open={showDelete}
          header={intl.formatMessage(messages.deleteUserConfirmTitle)}
          content={
            <div className="content">
              <Dimmer active={deleteRequest?.loading}>
                <Loader>
                  <FormattedMessage id="Loading" defaultMessage="Loading." />
                </Loader>
              </Dimmer>

              <ul className="content">
                <FormattedMessage
                  id="Do you really want to delete the user {username}?"
                  defaultMessage="Do you really want to delete the user {username}?"
                  values={{
                    username: <b>{usernameToDelete}</b>,
                  }}
                />
              </ul>
            </div>
          }
          onCancel={onDeleteCancel}
          onConfirm={onDeleteOk}
          size={null}
        />
        {userschema?.loaded && showAddUser ? (
          <ModalForm
            open={showAddUser}
            className="modal"
            onSubmit={onAddUserSubmit}
            submitError={addUserError}
            onCancel={() => {
              setShowAddUser(false);
              setAddUserError(undefined);
            }}
            title={intl.formatMessage(messages.addUserFormTitle)}
            loading={createRequest?.loading}
            schema={adduserschema}
          />
        ) : null}
      </div>
      <Segment.Group raised>
        <Segment className="primary">
          <FormattedMessage id="Users" defaultMessage="Users" />
        </Segment>
        <Segment secondary>
          <FormattedMessage
            id="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
            defaultMessage="Note that roles set here apply directly to a user. The symbol{plone_svg}indicates a role inherited from membership in a group."
            values={{
              plone_svg: (
                <Icon
                  name={ploneSVG}
                  size="20px"
                  color="#007EB1"
                  title={'plone-svg'}
                />
              ),
            }}
          />
        </Segment>
        <Segment>
          <Form onSubmit={onSearch}>
            <Form.Field>
              <Input
                name="SearchableText"
                action={{
                  icon: 'search',
                  loading: isLoading,
                  disabled: isLoading,
                }}
                placeholder={intl.formatMessage(messages.searchUsers)}
                onChange={onChangeSearch}
                id="user-search-input"
              />
            </Form.Field>
          </Form>
        </Segment>
        <Form>
          {((many_users && entries.length > 0) || !many_users) && (
            <Table padded striped attached unstackable>
              <Table.Header>
                <Table.Row>
                  <Table.HeaderCell>
                    <FormattedMessage
                      id="User name"
                      defaultMessage="User name"
                    />
                  </Table.HeaderCell>
                  {roles.map((role) => (
                    <Table.HeaderCell key={role.id}>
                      {role.title}
                    </Table.HeaderCell>
                  ))}
                  <Table.HeaderCell>
                    <FormattedMessage id="Actions" defaultMessage="Actions" />
                  </Table.HeaderCell>
                </Table.Row>
              </Table.Header>
              <Table.Body data-user="users">
                {entries
                  .slice(currentPage * 10, pageSize * (currentPage + 1))
                  .map((userItem) => (
                    <RenderUsers
                      key={userItem.id}
                      onDelete={handleDeleteUser}
                      roles={roles}
                      user={userItem}
                      updateUser={updateUserRole}
                      inheritedRole={inheritedRole}
                      userschema={userschema}
                      listUsers={listUsersAction}
                      isUserManager={isUserManager}
                    />
                  ))}
              </Table.Body>
            </Table>
          )}
          {entries.length === 0 && search && (
            <Segment>
              {intl.formatMessage(messages.userSearchNoResults)}
            </Segment>
          )}
          <div className="contents-pagination">
            <Pagination
              current={currentPage}
              total={Math.ceil(entries?.length / pageSize)}
              onChangePage={onChangePage}
            />
          </div>
        </Form>
      </Segment.Group>
      {isClient &&
        createPortal(
          <Toolbar
            pathname={pathname}
            hideDefaultViewButtons
            inner={
              <>
                <Button
                  id="toolbar-save"
                  className="save"
                  aria-label={intl.formatMessage(messages.save)}
                  onClick={updateUserRoleSubmit}
                  loading={createRequest?.loading}
                >
                  <Icon
                    name={saveSVG}
                    className="circled"
                    size="30px"
                    title={intl.formatMessage(messages.save)}
                  />
                </Button>
                <Link to="/controlpanel" className="cancel">
                  <Icon
                    name={clearSVG}
                    className="circled"
                    aria-label={intl.formatMessage(messages.cancel)}
                    size="30px"
                    title={intl.formatMessage(messages.cancel)}
                  />
                </Link>
                <Button
                  id="toolbar-add"
                  aria-label={intl.formatMessage(messages.addUserButtonTitle)}
                  onClick={() => {
                    setShowAddUser(true);
                  }}
                  loading={createRequest?.loading}
                >
                  <Icon
                    name={addUserSvg}
                    size="45px"
                    color="#826A6A"
                    title={intl.formatMessage(messages.addUserButtonTitle)}
                  />
                </Button>
              </>
            }
          />,
          document.getElementById('toolbar'),
        )}
    </Container>
  );
};

export default asyncConnect([
  {
    key: 'controlpanels',
    promise: ({ store: { dispatch, getState } }) => {
      return dispatch(getControlpanel('usergroup')).then(() => {
        const state = getState();
        const many_users = state.controlpanels?.controlpanel?.data?.many_users;
        if (!many_users) {
          dispatch(listUsers());
          dispatch(listGroups());
        }
      });
    },
  },
  {
    key: 'roles',
    promise: ({ store: { dispatch } }) => {
      return dispatch(listRoles());
    },
  },
  {
    key: 'userschema',
    promise: ({ store: { dispatch } }) => {
      return dispatch(getUserSchema());
    },
  },
  {
    key: 'user',
    promise: ({ store: { dispatch, getState } }) => {
      const state = getState();
      const token = state.userSession.token;
      if (token) {
        const userId = jwtDecode(token).sub;
        if (userId) {
          return dispatch(getUser(userId));
        }
      }
    },
  },
])(UsersControlpanel);
