/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 *
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

import {
  //@ts-ignore
  EuiBasicTable,
  EuiButton,
  EuiComboBoxOptionProps,
  EuiHorizontalRule,
  EuiPage,
  EuiPageBody,
  EuiSpacer,
} from '@elastic/eui';
import { debounce, get, isEmpty } from 'lodash';
import queryString from 'querystring';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import {
  CatIndex,
  GetDetectorsQueryParams,
  IndexAlias,
} from '../../../../../server/models/types';
import { DetectorListItem } from '../../../../models/interfaces';
import { SORT_DIRECTION } from '../../../../../server/utils/constants';
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
import { AppState } from '../../../../redux/reducers';
import {
  getDetectorList,
  startDetector,
  stopDetector,
  deleteDetector,
} from '../../../../redux/reducers/ad';
import {
  getIndices,
  getPrioritizedIndices,
} from '../../../../redux/reducers/opensearch';
import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants';
import { DETECTOR_STATE } from '../../../../../server/utils/constants';
import { getVisibleOptions, sanitizeSearchText } from '../../../utils/helpers';
import { EmptyDetectorMessage } from '../../components/EmptyMessage/EmptyMessage';
import { ListFilters } from '../../components/ListFilters/ListFilters';
import {
  MAX_DETECTORS,
  MAX_SELECTED_INDICES,
  GET_ALL_DETECTORS_QUERY_PARAMS,
  ALL_DETECTOR_STATES,
  ALL_INDICES,
  SINGLE_DETECTOR_NOT_FOUND_MSG,
} from '../../../utils/constants';
import { BREADCRUMBS } from '../../../../utils/constants';
import {
  getURLQueryParams,
  getDetectorsForAction,
  getMonitorsForAction,
} from '../../utils/helpers';
import {
  filterAndSortDetectors,
  getDetectorsToDisplay,
} from '../../../utils/helpers';
import { staticColumn } from '../../utils/tableUtils';
import { DETECTOR_ACTION } from '../../utils/constants';
import { getTitleWithCount, Listener } from '../../../../utils/utils';
import { ListActions } from '../../components/ListActions/ListActions';
import { searchMonitors } from '../../../../redux/reducers/alerting';
import { Monitor } from '../../../../models/interfaces';
import { ConfirmStartDetectorsModal } from '../ConfirmActionModals/ConfirmStartDetectorsModal';
import { ConfirmStopDetectorsModal } from '../ConfirmActionModals/ConfirmStopDetectorsModal';
import { ConfirmDeleteDetectorsModal } from '../ConfirmActionModals/ConfirmDeleteDetectorsModal';
import {
  NO_PERMISSIONS_KEY_WORD,
  prettifyErrorMessage,
} from '../../../../../server/utils/helpers';
import { CoreStart } from '../../../../../../../src/core/public';
import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices';

export interface ListRouterParams {
  from: string;
  size: string;
  search: string;
  indices: string;
  sortDirection: SORT_DIRECTION;
  sortField: string;
}
interface ListProps extends RouteComponentProps<ListRouterParams> {}
interface ListState {
  page: number;
  queryParams: GetDetectorsQueryParams;
  selectedDetectorStates: DETECTOR_STATE[];
  selectedIndices: string[];
}
interface ConfirmModalState {
  isOpen: boolean;
  action: DETECTOR_ACTION;
  isListLoading: boolean;
  isRequestingToClose: boolean;
  affectedDetectors: DetectorListItem[];
  affectedMonitors: { [key: string]: Monitor };
}
interface ListActionsState {
  isDisabled: boolean;
  isStartDisabled: boolean;
  isStopDisabled: boolean;
}

export const DetectorList = (props: ListProps) => {
  const core = React.useContext(CoreServicesContext) as CoreStart;
  const dispatch = useDispatch();
  const allDetectors = useSelector((state: AppState) => state.ad.detectorList);
  const allMonitors = useSelector((state: AppState) => state.alerting.monitors);
  const errorGettingDetectors = useSelector(
    (state: AppState) => state.ad.errorMessage
  );
  const opensearchState = useSelector((state: AppState) => state.opensearch);
  const isRequestingFromES = useSelector(
    (state: AppState) => state.ad.requesting
  );

  const [selectedDetectors, setSelectedDetectors] = useState(
    [] as DetectorListItem[]
  );
  const [detectorsToDisplay, setDetectorsToDisplay] = useState(
    [] as DetectorListItem[]
  );
  const [isLoadingFinalDetectors, setIsLoadingFinalDetectors] =
    useState<boolean>(true);
  const [selectedDetectorsForAction, setSelectedDetectorsForAction] = useState(
    [] as DetectorListItem[]
  );
  const isLoading = isRequestingFromES || isLoadingFinalDetectors;
  const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>(
    {
      isOpen: false,
      //@ts-ignore
      action: null,
      isListLoading: false,
      isRequestingToClose: false,
      affectedDetectors: [],
      affectedMonitors: {},
    }
  );
  const [listActionsState, setListActionsState] = useState<ListActionsState>({
    isDisabled: true,
    isStartDisabled: false,
    isStopDisabled: false,
  });

  // Getting all initial indices
  const [indexQuery, setIndexQuery] = useState('');
  useEffect(() => {
    const getInitialIndices = async () => {
      await dispatch(getIndices(indexQuery));
    };
    getInitialIndices();
  }, []);

  // Getting all initial monitors
  useEffect(() => {
    const getInitialMonitors = async () => {
      dispatch(searchMonitors());
    };
    getInitialMonitors();
  }, []);

  useEffect(() => {
    if (
      errorGettingDetectors &&
      !errorGettingDetectors.includes(SINGLE_DETECTOR_NOT_FOUND_MSG)
    ) {
      console.error(errorGettingDetectors);
      core.notifications.toasts.addDanger(
        typeof errorGettingDetectors === 'string' &&
          errorGettingDetectors.includes(NO_PERMISSIONS_KEY_WORD)
          ? prettifyErrorMessage(errorGettingDetectors)
          : 'Unable to get all detectors'
      );
      setIsLoadingFinalDetectors(false);
    }
  }, [errorGettingDetectors]);

  // Updating displayed indices (initializing to first 20 for now)
  const visibleIndices = get(opensearchState, 'indices', []) as CatIndex[];
  const visibleAliases = get(opensearchState, 'aliases', []) as IndexAlias[];
  const indexOptions = getVisibleOptions(visibleIndices, visibleAliases);

  const [state, setState] = useState<ListState>({
    page: 0,
    queryParams: getURLQueryParams(props.location),
    selectedDetectorStates: ALL_DETECTOR_STATES,
    selectedIndices: ALL_INDICES,
  });

  // Set breadcrumbs on page initialization
  useEffect(() => {
    core.chrome.setBreadcrumbs([
      BREADCRUMBS.ANOMALY_DETECTOR,
      BREADCRUMBS.DETECTORS,
    ]);
  }, []);

  // Refresh data if user change any parameters / filter / sort
  useEffect(() => {
    const { history, location } = props;
    const updatedParams = {
      ...state.queryParams,
      indices: state.selectedIndices.join(' '),
      from: state.page * state.queryParams.size,
    };

    history.replace({
      ...location,
      search: queryString.stringify(updatedParams),
    });

    setIsLoadingFinalDetectors(true);
    getUpdatedDetectors();
  }, [
    state.page,
    state.queryParams,
    state.selectedDetectorStates,
    state.selectedIndices,
  ]);

  // Handle all filtering / sorting of detectors
  useEffect(() => {
    const curSelectedDetectors = filterAndSortDetectors(
      Object.values(allDetectors),
      state.queryParams.search,
      state.selectedIndices,
      state.selectedDetectorStates,
      state.queryParams.sortField,
      state.queryParams.sortDirection
    );
    setSelectedDetectors(curSelectedDetectors);

    const curDetectorsToDisplay = getDetectorsToDisplay(
      curSelectedDetectors,
      state.page,
      state.queryParams.size
    );
    setDetectorsToDisplay(curDetectorsToDisplay);

    setIsLoadingFinalDetectors(false);
  }, [allDetectors]);

  // Update modal state if user decides to close
  useEffect(() => {
    if (confirmModalState.isRequestingToClose) {
      if (isLoading) {
        setConfirmModalState({
          ...confirmModalState,
          isListLoading: true,
        });
      } else {
        setConfirmModalState({
          ...confirmModalState,
          isOpen: false,
          isListLoading: false,
          isRequestingToClose: false,
        });
      }
    }
  }, [confirmModalState.isRequestingToClose, isLoading]);

  const getUpdatedDetectors = async () => {
    dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS));
  };

  const handlePageChange = (pageNumber: number) => {
    setState({ ...state, page: pageNumber });
  };

  const handleTableChange = ({ page: tablePage = {}, sort = {} }: any) => {
    const { index: page, size } = tablePage;
    const { field: sortField, direction: sortDirection } = sort;
    setState({
      ...state,
      page,
      queryParams: {
        ...state.queryParams,
        size,
        sortField,
        sortDirection,
      },
    });
  };

  // Refresh data is user is typing in the search bar
  const handleSearchDetectorChange = (
    e: React.ChangeEvent<HTMLInputElement>
  ): void => {
    const searchText = e.target.value;
    setState({
      ...state,
      page: 0,
      queryParams: {
        ...state.queryParams,
        search: searchText,
      },
    });
  };

  // Refresh data if user is typing in the index filter
  const handleSearchIndexChange = debounce(async (searchValue: string) => {
    if (searchValue !== indexQuery) {
      const sanitizedQuery = sanitizeSearchText(searchValue);
      setIndexQuery(sanitizedQuery);
      await dispatch(getPrioritizedIndices(sanitizedQuery));
      setState((state) => ({
        ...state,
        page: 0,
      }));
    }
  }, 300);

  // Refresh data if user is selecting a detector state filter
  const handleDetectorStateChange = (
    options: EuiComboBoxOptionProps[]
  ): void => {
    let states: DETECTOR_STATE[];
    states =
      options.length == 0
        ? ALL_DETECTOR_STATES
        : options.map((option) => option.label as DETECTOR_STATE);
    setState((state) => ({
      ...state,
      page: 0,
      selectedDetectorStates: states,
    }));
  };

  // Refresh data if user is selecting an index filter
  const handleIndexChange = (options: EuiComboBoxOptionProps[]): void => {
    let indices: string[];
    indices =
      options.length == 0
        ? ALL_INDICES
        : options.map((option) => option.label).slice(0, MAX_SELECTED_INDICES);

    setState({
      ...state,
      page: 0,
      selectedIndices: indices,
    });
  };

  const handleResetFilter = () => {
    setState((state) => ({
      ...state,
      queryParams: {
        ...state.queryParams,
        search: '',
        indices: '',
      },
      selectedDetectorStates: ALL_DETECTOR_STATES,
      selectedIndices: ALL_INDICES,
    }));
  };

  const handleSelectionChange = (currentSelected: DetectorListItem[]) => {
    setSelectedDetectorsForAction(currentSelected);
    setListActionsState({
      ...listActionsState,
      isDisabled: isEmpty(currentSelected),
      isStartDisabled: isEmpty(
        getDetectorsForAction(currentSelected, DETECTOR_ACTION.START)
      ),
      isStopDisabled: isEmpty(
        getDetectorsForAction(currentSelected, DETECTOR_ACTION.STOP)
      ),
    });
  };

  const handleStartDetectorsAction = () => {
    const validDetectors = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.START
    );
    if (!isEmpty(validDetectors)) {
      setConfirmModalState({
        isOpen: true,
        action: DETECTOR_ACTION.START,
        isListLoading: false,
        isRequestingToClose: false,
        affectedDetectors: validDetectors,
        affectedMonitors: {},
      });
    } else {
      core.notifications.toasts.addWarning(
        'All selected detectors are unable to start. Make sure selected \
          detectors have features and are not already running'
      );
    }
  };

  const handleStopDetectorsAction = () => {
    const validDetectors = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.STOP
    );
    if (!isEmpty(validDetectors)) {
      const validMonitors = getMonitorsForAction(validDetectors, allMonitors);
      setConfirmModalState({
        isOpen: true,
        action: DETECTOR_ACTION.STOP,
        isListLoading: false,
        isRequestingToClose: false,
        affectedDetectors: validDetectors,
        affectedMonitors: validMonitors,
      });
    } else {
      core.notifications.toasts.addWarning(
        'All selected detectors are unable to stop. Make sure selected \
          detectors are already running'
      );
    }
  };

  const handleDeleteDetectorsAction = async () => {
    const validDetectors = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.DELETE
    );
    if (!isEmpty(validDetectors)) {
      const validMonitors = getMonitorsForAction(validDetectors, allMonitors);
      setConfirmModalState({
        isOpen: true,
        action: DETECTOR_ACTION.DELETE,
        isListLoading: false,
        isRequestingToClose: false,
        affectedDetectors: validDetectors,
        affectedMonitors: validMonitors,
      });
    } else {
      core.notifications.toasts.addWarning(
        'No detectors selected. Please select detectors to delete'
      );
    }
  };

  const handleStartDetectorJobs = async () => {
    setIsLoadingFinalDetectors(true);
    const validIds = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.START
    ).map((detector) => detector.id);
    const promises = validIds.map(async (id: string) => {
      return dispatch(startDetector(id));
    });
    await Promise.all(promises)
      .then(() => {
        core.notifications.toasts.addSuccess(
          'Successfully started all selected detectors'
        );
      })
      .catch((error) => {
        core.notifications.toasts.addDanger(
          prettifyErrorMessage(
            `Error starting all selected detectors: ${error}`
          )
        );
      })
      .finally(() => {
        getUpdatedDetectors();
      });
  };

  const handleStopDetectorJobs = async (listener?: Listener) => {
    setIsLoadingFinalDetectors(true);
    const validIds = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.STOP
    ).map((detector) => detector.id);
    const promises = validIds.map(async (id: string) => {
      return dispatch(stopDetector(id));
    });
    await Promise.all(promises)
      .then(() => {
        core.notifications.toasts.addSuccess(
          'Successfully stopped all selected detectors'
        );
        if (listener) listener.onSuccess();
      })
      .catch((error) => {
        core.notifications.toasts.addDanger(
          prettifyErrorMessage(
            `Error stopping all selected detectors: ${error}`
          )
        );
        if (listener) listener.onException();
      })
      .finally(() => {
        // only need to get updated list if we're just stopping (no need if deleting also)
        if (confirmModalState.action === DETECTOR_ACTION.STOP) {
          getUpdatedDetectors();
        }
      });
  };

  const handleDeleteDetectorJobs = async () => {
    setIsLoadingFinalDetectors(true);
    const validIds = getDetectorsForAction(
      selectedDetectorsForAction,
      DETECTOR_ACTION.DELETE
    ).map((detector) => detector.id);
    const promises = validIds.map(async (id: string) => {
      return dispatch(deleteDetector(id));
    });
    await Promise.all(promises)
      .then(() => {
        core.notifications.toasts.addSuccess(
          'Successfully deleted all selected detectors'
        );
      })
      .catch((error) => {
        core.notifications.toasts.addDanger(
          prettifyErrorMessage(
            `Error deleting all selected detectors: ${error}`
          )
        );
      })
      .finally(() => {
        getUpdatedDetectors();
      });
  };

  const getItemId = (item: any) => {
    return `${item.id}-${item.currentTime}`;
  };

  const handleHideModal = () => {
    setConfirmModalState({
      ...confirmModalState,
      isOpen: false,
    });
  };

  const handleConfirmModal = () => {
    setConfirmModalState({
      ...confirmModalState,
      isRequestingToClose: true,
    });
  };

  const getConfirmModal = () => {
    if (confirmModalState.isOpen) {
      //@ts-ignore
      switch (confirmModalState.action) {
        case DETECTOR_ACTION.START: {
          return (
            <ConfirmStartDetectorsModal
              detectors={confirmModalState.affectedDetectors}
              onStartDetectors={handleStartDetectorJobs}
              onHide={handleHideModal}
              onConfirm={handleConfirmModal}
              isListLoading={isLoading}
            />
          );
        }
        case DETECTOR_ACTION.STOP: {
          return (
            <ConfirmStopDetectorsModal
              detectors={confirmModalState.affectedDetectors}
              monitors={confirmModalState.affectedMonitors}
              onStopDetectors={handleStopDetectorJobs}
              onHide={handleHideModal}
              onConfirm={handleConfirmModal}
              isListLoading={isLoading}
            />
          );
        }
        case DETECTOR_ACTION.DELETE: {
          return (
            <ConfirmDeleteDetectorsModal
              detectors={confirmModalState.affectedDetectors}
              monitors={confirmModalState.affectedMonitors}
              onStopDetectors={handleStopDetectorJobs}
              onDeleteDetectors={handleDeleteDetectorJobs}
              onHide={handleHideModal}
              onConfirm={handleConfirmModal}
              isListLoading={isLoading}
            />
          );
        }
        default: {
          return null;
        }
      }
    } else {
      return null;
    }
  };

  const sorting = {
    sort: {
      direction: state.queryParams.sortDirection,
      field: state.queryParams.sortField,
    },
  };

  const selection = {
    onSelectionChange: handleSelectionChange,
  };

  const isFilterApplied =
    !isEmpty(state.queryParams.search) ||
    !isEmpty(state.selectedDetectorStates) ||
    !isEmpty(state.selectedIndices);

  const pagination = {
    pageIndex: state.page,
    pageSize: state.queryParams.size,
    totalItemCount: Math.min(MAX_DETECTORS, selectedDetectors.length),
    pageSizeOptions: [5, 10, 20, 50],
  };

  const confirmModal = getConfirmModal();

  return (
    <EuiPage>
      <EuiPageBody>
        <ContentPanel
          title={
            isLoading
              ? getTitleWithCount('Detectors', '...')
              : getTitleWithCount('Detectors', selectedDetectors.length)
          }
          titleDataTestSubj="detectorListHeader"
          actions={[
            <ListActions
              onStartDetectors={handleStartDetectorsAction}
              onStopDetectors={handleStopDetectorsAction}
              onDeleteDetectors={handleDeleteDetectorsAction}
              isActionsDisabled={listActionsState.isDisabled}
              isStartDisabled={listActionsState.isStartDisabled}
              isStopDisabled={listActionsState.isStopDisabled}
            />,
            <EuiButton
              data-test-subj="createDetectorButton"
              fill
              href={`${PLUGIN_NAME}#${APP_PATH.CREATE_DETECTOR}`}
            >
              Create detector
            </EuiButton>,
          ]}
        >
          {confirmModal}
          <ListFilters
            activePage={state.page}
            pageCount={
              isLoading
                ? 0
                : Math.ceil(
                    selectedDetectors.length / state.queryParams.size
                  ) || 1
            }
            search={state.queryParams.search}
            selectedDetectorStates={state.selectedDetectorStates}
            selectedIndices={state.selectedIndices}
            indexOptions={indexOptions}
            onDetectorStateChange={handleDetectorStateChange}
            onIndexChange={handleIndexChange}
            onSearchDetectorChange={handleSearchDetectorChange}
            onSearchIndexChange={handleSearchIndexChange}
            onPageClick={handlePageChange}
          />
          <EuiSpacer size="m" />
          <EuiBasicTable<any>
            data-test-subj="detectorListTable"
            items={isLoading ? [] : detectorsToDisplay}
            /*
              itemId here is used to keep track of the selected detectors and render appropriately.
              Because the item id is dependent on the current time (see getItemID() above), all selected
              detectors will be deselected once new detectors are retrieved because the page will
              re-render with a new timestamp. This logic is borrowed from Alerting Dashboards plugin's
              monitors list page.
            */
            itemId={getItemId}
            columns={staticColumn}
            onChange={handleTableChange}
            isSelectable={true}
            selection={selection}
            sorting={sorting}
            pagination={pagination}
            noItemsMessage={
              isLoading ? (
                'Loading detectors...'
              ) : (
                <EmptyDetectorMessage
                  isFilterApplied={isFilterApplied}
                  onResetFilters={handleResetFilter}
                />
              )
            }
          />
        </ContentPanel>
      </EuiPageBody>
    </EuiPage>
  );
};