import React, { useState, useCallback, useContext } from "react";
import { InjectedRouter } from "react-router";
import { useQuery } from "react-query";
import memoize from "memoize-one";

import paths from "router/paths";
import { IApiError } from "interfaces/errors";
import { IInvite } from "interfaces/invite";
import { IUser, IUserFormErrors } from "interfaces/user";
import { ITeam } from "interfaces/team";
import { clearToken } from "utilities/local";

import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
import usersAPI from "services/entities/users";
import invitesAPI from "services/entities/invites";

import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants";
import TableContainer from "components/TableContainer";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import TableDataError from "components/DataError";
import EmptyTable from "components/EmptyTable";
import { generateTableHeaders, combineDataSets } from "./UsersTableConfig";
import DeleteUserModal from "../DeleteUserModal";
import ResetPasswordModal from "../ResetPasswordModal";
import ResetSessionsModal from "../ResetSessionsModal";
import { NewUserType } from "../UserForm/UserForm";
import CreateUserModal from "../CreateUserModal";
import EditUserModal from "../EditUserModal";

interface IUsersTableProps {
  router: InjectedRouter; // v3
}

const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
  const { config, currentUser, isPremiumTier } = useContext(AppContext);
  const { renderFlash } = useContext(NotificationContext);

  // STATES
  const [showCreateUserModal, setShowCreateUserModal] = useState(false);
  const [showEditUserModal, setShowEditUserModal] = useState(false);
  const [showDeleteUserModal, setShowDeleteUserModal] = useState(false);
  const [showResetPasswordModal, setShowResetPasswordModal] = useState(false);
  const [showResetSessionsModal, setShowResetSessionsModal] = useState(false);
  const [isUpdatingUsers, setIsUpdatingUsers] = useState(false);
  const [userEditing, setUserEditing] = useState<any>(null);
  const [createUserErrors, setCreateUserErrors] = useState<IUserFormErrors>(
    DEFAULT_CREATE_USER_ERRORS
  );
  const [editUserErrors, setEditUserErrors] = useState<IUserFormErrors>(
    DEFAULT_CREATE_USER_ERRORS
  );
  const [querySearchText, setQuerySearchText] = useState("");

  // API CALLS
  const {
    data: teams,
    isFetching: isFetchingTeams,
    error: loadingTeamsError,
  } = useQuery<ILoadTeamsResponse, Error, ITeam[]>(
    ["teams"],
    () => teamsAPI.loadAll(),
    {
      enabled: !!isPremiumTier,
      select: (data: ILoadTeamsResponse) => data.teams,
    }
  );

  const {
    data: users,
    isFetching: isFetchingUsers,
    error: loadingUsersError,
    refetch: refetchUsers,
  } = useQuery<IUser[], Error, IUser[]>(
    ["users", querySearchText],
    () => usersAPI.loadAll({ globalFilter: querySearchText }),
    {
      select: (data: IUser[]) => data,
    }
  );

  const {
    data: invites,
    isFetching: isFetchingInvites,
    error: loadingInvitesError,
    refetch: refetchInvites,
  } = useQuery<IInvite[], Error, IInvite[]>(
    ["invites", querySearchText],
    () => invitesAPI.loadAll({ globalFilter: querySearchText }),
    {
      select: (data: IInvite[]) => {
        return data;
      },
    }
  );

  // TOGGLE MODALS

  const toggleCreateUserModal = useCallback(() => {
    setShowCreateUserModal(!showCreateUserModal);

    // clear errors on close
    if (!showCreateUserModal) {
      setCreateUserErrors(DEFAULT_CREATE_USER_ERRORS);
    }
  }, [showCreateUserModal, setShowCreateUserModal]);

  const toggleDeleteUserModal = useCallback(
    (user?: IUser | IInvite) => {
      setShowDeleteUserModal(!showDeleteUserModal);
      setUserEditing(!showDeleteUserModal ? user : null);
    },
    [showDeleteUserModal, setShowDeleteUserModal, setUserEditing]
  );

  const toggleEditUserModal = useCallback(
    (user?: IUser | IInvite) => {
      setShowEditUserModal(!showEditUserModal);
      setUserEditing(!showEditUserModal ? user : null);
      setEditUserErrors(DEFAULT_CREATE_USER_ERRORS);
    },
    [showEditUserModal, setShowEditUserModal, setUserEditing]
  );

  const toggleResetPasswordUserModal = useCallback(
    (user?: IUser | IInvite) => {
      setShowResetPasswordModal(!showResetPasswordModal);
      setUserEditing(!showResetPasswordModal ? user : null);
    },
    [showResetPasswordModal, setShowResetPasswordModal, setUserEditing]
  );

  const toggleResetSessionsUserModal = useCallback(
    (user?: IUser | IInvite) => {
      setShowResetSessionsModal(!showResetSessionsModal);
      setUserEditing(!showResetSessionsModal ? user : null);
    },
    [showResetSessionsModal, setShowResetSessionsModal, setUserEditing]
  );

  // FUNCTIONS

  const combineUsersAndInvites = memoize(
    (usersData, invitesData, currentUserId) => {
      return combineDataSets(usersData, invitesData, currentUserId);
    }
  );

  const goToUserSettingsPage = () => {
    const { USER_SETTINGS } = paths;
    router.push(USER_SETTINGS);
  };

  // NOTE: this is called once on the initial rendering. The initial render of
  // the TableContainer child component calls this handler.
  const onTableQueryChange = (queryData: ITableQueryData) => {
    const { searchQuery, sortHeader, sortDirection } = queryData;
    let sortBy: any = []; // TODO
    if (sortHeader !== "") {
      sortBy = [{ id: sortHeader, direction: sortDirection }];
    }

    setQuerySearchText(searchQuery);

    refetchUsers();
    refetchInvites();
  };

  const onActionSelect = (value: string, user: IUser | IInvite) => {
    switch (value) {
      case "edit":
        toggleEditUserModal(user);
        break;
      case "delete":
        toggleDeleteUserModal(user);
        break;
      case "passwordReset":
        toggleResetPasswordUserModal(user);
        break;
      case "resetSessions":
        toggleResetSessionsUserModal(user);
        break;
      case "editMyAccount":
        goToUserSettingsPage();
        break;
      default:
        return null;
    }
    return null;
  };

  const getUser = (type: string, id: number) => {
    let userData;
    if (type === "user") {
      userData = users?.find((user) => user.id === id);
    } else {
      userData = invites?.find((invite) => invite.id === id);
    }
    return userData;
  };

  const onCreateUserSubmit = (formData: any) => {
    setIsUpdatingUsers(true);

    if (formData.newUserType === NewUserType.AdminInvited) {
      // Do some data formatting adding `invited_by` for the request to be correct and deleteing uncessary fields
      const requestData = {
        ...formData,
        invited_by: formData.currentUserId,
      };
      delete requestData.currentUserId; // this field is not needed for the request
      delete requestData.newUserType; // this field is not needed for the request
      delete requestData.password; // this field is not needed for the request
      invitesAPI
        .create(requestData)
        .then(() => {
          renderFlash(
            "success",
            `An invitation email was sent from ${config?.smtp_settings.sender_address} to ${formData.email}.`
          );
          toggleCreateUserModal();
          refetchInvites();
        })
        .catch((userErrors: { data: IApiError }) => {
          if (userErrors.data.errors[0].reason.includes("already exists")) {
            setCreateUserErrors({
              email: "A user with this email address already exists",
            });
          } else if (
            userErrors.data.errors[0].reason.includes("required criteria")
          ) {
            setCreateUserErrors({
              password: "Password must meet the criteria below",
            });
          } else {
            renderFlash("error", "Could not create user. Please try again.");
          }
        })
        .finally(() => {
          setIsUpdatingUsers(false);
        });
    } else {
      // Do some data formatting deleting unnecessary fields
      const requestData = {
        ...formData,
      };
      delete requestData.currentUserId; // this field is not needed for the request
      delete requestData.newUserType; // this field is not needed for the request
      usersAPI
        .createUserWithoutInvitation(requestData)
        .then(() => {
          renderFlash("success", `Successfully created ${requestData.name}.`);
          toggleCreateUserModal();
          refetchUsers();
        })
        .catch((userErrors: { data: IApiError }) => {
          if (userErrors.data.errors[0].reason.includes("Duplicate")) {
            setCreateUserErrors({
              email: "A user with this email address already exists",
            });
          } else if (
            userErrors.data.errors[0].reason.includes("required criteria")
          ) {
            setCreateUserErrors({
              password: "Password must meet the criteria below",
            });
          } else {
            renderFlash("error", "Could not create user. Please try again.");
          }
        })
        .finally(() => {
          setIsUpdatingUsers(false);
        });
    }
  };

  const onEditUser = (formData: any) => {
    const userData = getUser(userEditing.type, userEditing.id);

    let userUpdatedFlashMessage = `Successfully edited ${formData.name}`;
    if (userData?.email !== formData.email) {
      userUpdatedFlashMessage += `: A confirmation email was sent from ${config?.smtp_settings.sender_address} to ${formData.email}`;
    }
    const userUpdatedEmailError =
      "A user with this email address already exists";
    const userUpdatedPasswordError = "Password must meet the criteria below";
    const userUpdatedError = `Could not edit ${userEditing?.name}. Please try again.`;

    setIsUpdatingUsers(true);
    if (userEditing.type === "invite") {
      return (
        userData &&
        invitesAPI
          .update(userData.id, formData)
          .then(() => {
            renderFlash("success", userUpdatedFlashMessage);
            toggleEditUserModal();
            refetchInvites();
          })
          .catch((userErrors: { data: IApiError }) => {
            if (userErrors.data.errors[0].reason.includes("already exists")) {
              setEditUserErrors({
                email: userUpdatedEmailError,
              });
            } else if (
              userErrors.data.errors[0].reason.includes("required criteria")
            ) {
              setEditUserErrors({
                password: userUpdatedPasswordError,
              });
            } else {
              renderFlash("error", userUpdatedError);
            }
          })
          .finally(() => {
            setIsUpdatingUsers(false);
          })
      );
    }

    return (
      userData &&
      usersAPI
        .update(userData.id, formData)
        .then(() => {
          renderFlash("success", userUpdatedFlashMessage);
          toggleEditUserModal();
          refetchUsers();
        })
        .catch((userErrors: { data: IApiError }) => {
          if (userErrors.data.errors[0].reason.includes("already exists")) {
            setEditUserErrors({
              email: userUpdatedEmailError,
            });
          } else if (
            userErrors.data.errors[0].reason.includes("required criteria")
          ) {
            setEditUserErrors({
              password: userUpdatedPasswordError,
            });
          } else {
            renderFlash("error", userUpdatedError);
          }
        })
        .finally(() => {
          setIsUpdatingUsers(false);
        })
    );
  };

  const onDeleteUser = () => {
    setIsUpdatingUsers(true);
    if (userEditing.type === "invite") {
      invitesAPI
        .destroy(userEditing.id)
        .then(() => {
          renderFlash("success", `Successfully deleted ${userEditing?.name}.`);
        })
        .catch(() => {
          renderFlash(
            "error",
            `Could not delete ${userEditing?.name}. Please try again.`
          );
        })
        .finally(() => {
          toggleDeleteUserModal();
          refetchInvites();
          setIsUpdatingUsers(false);
        });
    } else {
      usersAPI
        .destroy(userEditing.id)
        .then(() => {
          renderFlash("success", `Successfully deleted ${userEditing?.name}.`);
        })
        .catch(() => {
          renderFlash(
            "error",
            `Could not delete ${userEditing?.name}. Please try again.`
          );
        })
        .finally(() => {
          toggleDeleteUserModal();
          refetchUsers();
          setIsUpdatingUsers(false);
        });
    }
  };

  const onResetSessions = () => {
    const isResettingCurrentUser = currentUser?.id === userEditing.id;

    usersAPI
      .deleteSessions(userEditing.id)
      .then(() => {
        if (isResettingCurrentUser) {
          clearToken();
          setTimeout(() => {
            window.location.href = "/";
          }, 500);
          return;
        }
        renderFlash("success", "Successfully reset sessions.");
      })
      .catch(() => {
        renderFlash("error", "Could not reset sessions. Please try again.");
      })
      .finally(() => {
        toggleResetSessionsUserModal();
      });
  };

  const resetPassword = (user: IUser) => {
    return usersAPI
      .requirePasswordReset(user.id, { require: true })
      .then(() => {
        renderFlash("success", "Successfully required a password reset.");
      })
      .catch(() => {
        renderFlash(
          "error",
          "Could not require a password reset. Please try again."
        );
      })
      .finally(() => {
        toggleResetPasswordUserModal();
      });
  };

  const renderEditUserModal = () => {
    const userData = getUser(userEditing.type, userEditing.id);

    return (
      <EditUserModal
        defaultEmail={userData?.email}
        defaultName={userData?.name}
        defaultGlobalRole={userData?.global_role}
        defaultTeams={userData?.teams}
        onCancel={toggleEditUserModal}
        onSubmit={onEditUser}
        availableTeams={teams || []}
        isPremiumTier={isPremiumTier || false}
        smtpConfigured={config?.smtp_settings.configured || false}
        sesConfigured={config?.email?.backend === "ses" || false}
        canUseSso={config?.sso_settings.enable_sso || false}
        isSsoEnabled={userData?.sso_enabled}
        isApiOnly={userData?.api_only || false}
        isModifiedByGlobalAdmin
        isInvitePending={userEditing.type === "invite"}
        editUserErrors={editUserErrors}
        isUpdatingUsers={isUpdatingUsers}
      />
    );
  };

  const renderCreateUserModal = () => {
    return (
      <CreateUserModal
        createUserErrors={createUserErrors}
        onCancel={toggleCreateUserModal}
        onSubmit={onCreateUserSubmit}
        availableTeams={teams || []}
        defaultGlobalRole={"observer"}
        defaultTeams={[]}
        isPremiumTier={isPremiumTier || false}
        smtpConfigured={config?.smtp_settings.configured || false}
        sesConfigured={config?.email?.backend === "ses" || false}
        canUseSso={config?.sso_settings.enable_sso || false}
        isUpdatingUsers={isUpdatingUsers}
        isModifiedByGlobalAdmin
      />
    );
  };

  const renderDeleteUserModal = () => {
    return (
      <DeleteUserModal
        name={userEditing.name}
        onDelete={onDeleteUser}
        onCancel={toggleDeleteUserModal}
        isUpdatingUsers={isUpdatingUsers}
      />
    );
  };

  const renderResetPasswordModal = () => {
    return (
      <ResetPasswordModal
        user={userEditing}
        onResetConfirm={resetPassword}
        onResetCancel={toggleResetPasswordUserModal}
      />
    );
  };

  const renderResetSessionsModal = () => {
    return (
      <ResetSessionsModal
        user={userEditing}
        onResetConfirm={onResetSessions}
        onResetCancel={toggleResetSessionsUserModal}
      />
    );
  };

  const tableHeaders = generateTableHeaders(
    onActionSelect,
    isPremiumTier || false
  );

  const loadingTableData =
    isFetchingUsers || isFetchingInvites || isFetchingTeams;
  const tableDataError =
    loadingUsersError || loadingInvitesError || loadingTeamsError;

  let tableData: unknown = [];
  if (!loadingTableData && !tableDataError) {
    tableData = combineUsersAndInvites(users, invites, currentUser?.id);
  }

  const emptyState = {
    header: "No users match the current criteria.",
    info:
      "Expecting to see users? Try again in a few seconds as the system catches up.",
  };

  return (
    <>
      {/* TODO: find a way to move these controls into the table component */}
      {tableDataError ? (
        <TableDataError />
      ) : (
        <TableContainer
          columns={tableHeaders}
          data={tableData}
          isLoading={loadingTableData}
          defaultSortHeader={"name"}
          defaultSortDirection={"asc"}
          inputPlaceHolder={"Search"}
          actionButton={{
            name: "create user",
            buttonText: "Create user",
            onActionButtonClick: toggleCreateUserModal,
          }}
          onQueryChange={onTableQueryChange}
          resultsTitle={"users"}
          emptyComponent={() => EmptyTable(emptyState)}
          searchable
          showMarkAllPages={false}
          isAllPagesSelected={false}
          isClientSidePagination
        />
      )}
      {showCreateUserModal && renderCreateUserModal()}
      {showEditUserModal && renderEditUserModal()}
      {showDeleteUserModal && renderDeleteUserModal()}
      {showResetSessionsModal && renderResetSessionsModal()}
      {showResetPasswordModal && renderResetPasswordModal()}
    </>
  );
};

export default UsersTable;
