import {
  BodyScrollEvent,
  CellEditRequestEvent,
  ColDef,
  FillOperationParams,
  GetRowIdParams,
  PasteEndEvent,
  RowDataUpdatedEvent,
  RowNode,
  RowSelectedEvent,
  CellEditingStartedEvent,
  SuppressKeyboardEventParams,
} from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css?raw';
import 'ag-grid-community/styles/ag-theme-alpine.css?raw';
import 'ag-grid-enterprise';
import { AgGridReact } from 'ag-grid-react';
import { CampaignStatuses, Id, Taxonomy } from 'backend-api/models';
import { getProjectName } from 'common-v2/transducers';
import { removeToasts } from 'common/actions';
import { LoadingState } from 'common/types';
import { usePrevious } from 'hooks';
import { throttle } from 'lodash';
import {
  addCampaignForApproval,
  deselectCampaign,
  selectCampaign,
  setDisableRequestApprovalButton,
} from 'media-plan-v2/actions';
import { useShowRequestApprovalButton } from 'media-plan-v2/hooks/use-show-request-approval-button';
import {
  confirmModalSelector,
  mediaPlanSelector,
  selectedCampaignsIdsSelector,
  spreadsheetValidationModelSelector,
} from 'media-plan-v2/selectors';
import { MediaPlanMode } from 'media-plan-v2/types';
import React, { Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Prompt } from 'react-router';
import { useClickAway } from 'react-use';
import { AnalyticsEvents, trackEvent } from 'utils/analytic';
import { normalizeIndex } from 'utils/array';
import { ConfirmationModal } from '../confirmation-modal';
import { OverlayType, PhaseRow, SpreadsheetOverlay } from './components';
import {
  ASYNC_TRANSACTION_WAIT,
  DEFAULT_COLUMNS_CONFIG,
  SPREADSHEET_HEADER_HEIGHT,
  SPREADSHEET_ROW_HEIGHT,
  SYNC_IN_PROGRESS_WARNING_MESSAGE,
  UNDO_REDO_LIMITS,
} from './constants';
import {
  useAutogeneratedNames,
  useColumnsVisibility,
  useConfirm,
  useContextMenu,
  useCopyPaste,
  useHotKeys,
  useSync,
  useUndo,
  useData,
} from './hooks';
import { SSpreadsheet } from './s-spreadsheet';
import {
  exportMediaPlanToExcel,
  getExcelStyles,
  handleRemovingValue,
  hasCampaignsStatuses,
  initialGroupOrderComparator,
  isReadOnlyMode,
  isRedoKey,
  isUndoKey,
  onSortChanged,
  postSortRows,
  prepareCellDataForClipboard,
  sendToClipboard,
} from './transducers';
import { isMediaPlanColumnId, processPlacements } from 'media-plan-v2/containers/spreadsheet/transducers';
import { GroupRowRendererParams } from './types';
import {
  MediaPlanColumnId,
  SpreadsheetRowData,
  SpreadsheetContext,
  MediaPlanAccessMode,
} from 'media-plan-v2/containers/spreadsheet/types';
import { projectDetailsSelector } from 'project-v2/selectors';
import { THEME, TOAST_TYPE, useManageToasts } from 'gdb-web-shared-components';

interface Props {
  projectId: Id;
  mediaPlanId: Id;
  columnsDefinitions?: ColDef[];
  accessMode: MediaPlanAccessMode;
  mediaPlanMode: MediaPlanMode;
  taxonomy?: Taxonomy;
  isLoading: boolean;
  onRefresh(mediaPlanId: number): void;
  hasError: boolean;
  currencyCode: string;
}

export interface SpreadsheetRef {
  export(): void;
}

export const Spreadsheet = React.forwardRef(
  (
    {
      projectId,
      mediaPlanId,
      columnsDefinitions,
      accessMode,
      taxonomy,
      isLoading,
      onRefresh,
      hasError,
      mediaPlanMode,
      currencyCode,
    }: Props,
    ref: Ref<SpreadsheetRef>
  ) => {
    const gridRef = useRef<AgGridReact | null>();
    const gridContainer = useRef<HTMLDivElement>(null);
    const overlayTimeout = useRef<NodeJS.Timeout | null>(null);

    const dispatch = useDispatch();
    const { openToast } = useManageToasts(THEME.light);
    const { isCheckboxColumnVisible } = useColumnsVisibility(gridRef.current?.columnApi);
    const { data, reviewedData, dataFilter, isDataFiltered } = useData(gridRef.current?.api);

    const mediaPlan = useSelector(mediaPlanSelector);
    const confirmModal = useSelector(confirmModalSelector);
    const { data: project } = useSelector(projectDetailsSelector);
    const spreadsheetValidationModel = useSelector(spreadsheetValidationModelSelector);
    const selectedCampaignIds = useSelector(selectedCampaignsIdsSelector);

    const context = useMemo<SpreadsheetContext>(
      () => ({
        taxonomy,
        accessMode,
        mediaPlanMode,
        projectId,
      }),
      [taxonomy, accessMode, mediaPlanMode, projectId]
    );

    useClickAway(gridContainer, () => {
      gridRef.current?.api.clearRangeSelection();
      gridRef.current?.api.clearFocusedCell();
    });

    useAutogeneratedNames(gridRef);

    const groupRendererParams = useMemo<GroupRowRendererParams>(
      () => ({
        projectId,
        mediaPlanId,
        suppressCount: true,
        suppressDoubleClickExpand: true,
        suppressEnterExpand: true,
        checkbox: true,
        innerRenderer: PhaseRow,
        showDisabledCheckboxes: true,
      }),
      [projectId, mediaPlanId]
    );

    const previousAccessMode = usePrevious(accessMode);
    const previousMediaPlanMode = usePrevious(mediaPlanMode);

    const { withConfirm, confirmAction } = useConfirm();

    const { syncData, isDataSyncing } = useSync(mediaPlanId, projectId, gridRef.current?.api, mediaPlanMode);
    const undo = useUndo(gridRef.current?.api);
    const { copiedData, addCopiedData, resetCopiedData } = useCopyPaste(projectId, undo, context, gridRef.current);
    const { getContextMenuItems } = useContextMenu(projectId, mediaPlanId, withConfirm, resetCopiedData, undo, context);
    useHotKeys(projectId, mediaPlanId, withConfirm, undo.sanitizeUndoRedoStacks, resetCopiedData, gridRef);

    const shouldShowRequestApprovalButton = useShowRequestApprovalButton(projectId, mediaPlanMode);

    const shouldInterruptPageReload = isDataSyncing || reviewedData.length > 0;

    const handleBeforeUnloadEvent = useCallback(
      event => {
        if (shouldInterruptPageReload) {
          event.preventDefault();
          event.returnValue = '';
        }
      },
      [shouldInterruptPageReload]
    );

    useEffect(() => {
      window.addEventListener('beforeunload', handleBeforeUnloadEvent);

      return () => {
        window.removeEventListener('beforeunload', handleBeforeUnloadEvent);
      };
    }, [handleBeforeUnloadEvent]);

    const getRowId = useCallback((params: GetRowIdParams) => params.data.uuid.value, []);

    const onBodyScroll = useMemo(
      () =>
        throttle(({ api }: BodyScrollEvent) => {
          api.stopEditing();
        }, 300),
      []
    );

    const onExportClick = useCallback(() => {
      if (project && gridRef.current) {
        exportMediaPlanToExcel(gridRef.current?.api, getProjectName(project));
        trackEvent(AnalyticsEvents.EXPORT_TO_EXCEL, { projectId });
      }
    }, [project, projectId]);

    useImperativeHandle(ref, () => ({ export: onExportClick }), [onExportClick]);

    const onPasteEnd = useCallback(
      (event: PasteEndEvent) => {
        const focusedCell = event.api.getFocusedCell();
        const column = focusedCell?.column;
        const isEditable = column?.getColDef().editable || column?.getColId() === MediaPlanColumnId.ACTIONS;
        const isShowToast = isReadOnlyMode(accessMode) || (focusedCell && !isEditable);

        if (isShowToast) {
          openToast({
            id: 'media-plan-read-only',
            type: TOAST_TYPE.ERROR,
            message: "We're sorry, you cannot update read-only data.",
            preventDuplicate: true,
          });
        }
      },
      [accessMode, openToast]
    );

    const shouldShowOverlay = isLoading || hasError;

    const rowData = useMemo(() => {
      if (shouldShowOverlay) {
        return undefined;
      } else {
        return data;
      }
    }, [shouldShowOverlay, data]);

    const overlayContentProps = useMemo(() => {
      let type = OverlayType.NONE;

      if (isLoading) {
        type = OverlayType.LOADING;
      } else if (hasError) {
        type = OverlayType.ERROR;
      }

      return {
        type,
        refresh: () => onRefresh(mediaPlanId),
      };
    }, [hasError, isLoading, mediaPlanId, onRefresh]);

    useEffect(() => {
      if (shouldShowOverlay) {
        gridRef.current?.api?.hideOverlay();

        // NOTE: we have to use setTimeout because there is a bug of ag-grid showing no-row view on refetch data instead of load view (https://github.com/ag-grid/ag-grid/issues/3849)
        // and for the proper refreshing between overlay states
        overlayTimeout.current = setTimeout(() => {
          gridRef.current?.api?.showLoadingOverlay();
        }, 0);
      } else {
        if (overlayTimeout.current !== null) {
          clearTimeout(overlayTimeout.current);
        }

        gridRef.current?.api?.hideOverlay();
      }
    }, [overlayContentProps, shouldShowOverlay]);

    const shouldSuppressKeyboardEvent = useCallback(
      (params: SuppressKeyboardEventParams) => {
        const { event, editing, api, node, column } = params;
        const isEditable = column.isCellEditable(node);

        if (isUndoKey(event) || isRedoKey(event)) {
          return true;
        }

        if (isEditable && event.key === 'Backspace' && !editing && !node.group && !node.footer) {
          const cell = api.getFocusedCell();

          handleRemovingValue(event, api, undo.updateUndoStack, openToast);

          if (cell) {
            api.setFocusedCell(cell.rowIndex, cell.column);
          }

          return true;
        }

        return !!DEFAULT_COLUMNS_CONFIG.suppressKeyboardEvent?.(params);
      },
      [openToast, undo.updateUndoStack]
    );

    const defaultColumnsConfig: ColDef = useMemo(
      () => ({
        ...DEFAULT_COLUMNS_CONFIG,
        suppressKeyboardEvent: shouldSuppressKeyboardEvent,
      }),
      [shouldSuppressKeyboardEvent]
    );

    const onCellValueChanged = useCallback(
      ({ data, colDef, api, newValue, oldValue }: CellEditRequestEvent<SpreadsheetRowData>) => {
        if (!colDef.field || !isMediaPlanColumnId(colDef.field)) return;

        const prevData = { ...data };
        const nextData = { ...data };

        const isStatusDuringApproval =
          colDef.field === MediaPlanColumnId.STATUS && mediaPlanMode === MediaPlanMode.APPROVAL;

        prevData[colDef.field] = oldValue;
        nextData[colDef.field] = newValue;

        undo.updateUndoStack({ field: colDef.field, data: prevData });

        if (colDef.field && !isStatusDuringApproval) {
          api.applyTransactionAsync({
            update: [nextData],
          });
        } else if (
          (nextData.status?.value === CampaignStatuses.APPROVED ||
            nextData.status?.value === CampaignStatuses.DISAPPROVED) &&
          isStatusDuringApproval
        ) {
          dispatch(
            addCampaignForApproval({ campaignUuid: nextData.uuid.value, workflowStatus: nextData.status.value })
          );
        }
      },
      [dispatch, mediaPlanMode, undo]
    );

    const fillOperation = useCallback(
      (event: FillOperationParams) => {
        const groupsCount = event.values.filter(item => item === 'group').length;
        const normalizedIndex = normalizeIndex(
          Math.max(event.currentIndex - groupsCount, 0),
          event.initialValues.length
        );

        if (event.rowNode.group || event.rowNode.footer) {
          return 'group';
        }

        const columnId = event.column.getColId();
        const value = event.initialValues[normalizedIndex];

        if (columnId === MediaPlanColumnId.PLACEMENTS) {
          const currentPlacements = event.api.getValue(columnId, event.rowNode);
          const platforms = event.api.getValue(MediaPlanColumnId.PLATFORMS, event.rowNode);
          const processedPlacements = processPlacements(
            platforms,
            event.context.taxonomy,
            value?.value?.map(({ name }) => name).join(', ')
          );

          if (!processedPlacements) {
            openToast({
              id: 'media-plan-placements-error',
              type: TOAST_TYPE.ERROR,
              message: "We're sorry, Placements inconsistent with Platforms were pasted into the table.",
              preventDuplicate: true,
            });

            return currentPlacements;
          }

          return {
            ...value,
            value: processedPlacements,
          };
        }

        return value;
      },
      [openToast]
    );

    useEffect(() => {
      if (accessMode !== previousAccessMode && mediaPlanMode === previousMediaPlanMode) {
        gridRef.current?.api?.refreshCells({ force: true });
      }
    }, [accessMode, mediaPlanMode, previousAccessMode, previousMediaPlanMode]);

    const onCellEditingStarted = useCallback(
      (event: CellEditingStartedEvent) => {
        dispatch(removeToasts());

        const rowIndex = event.rowIndex;

        if (rowIndex == null) return;

        const editingRowList = document.querySelectorAll(`[row-index="${rowIndex}"]`);
        Array.from(editingRowList).forEach(rowElement => rowElement.classList.remove('ag-row-hover'));
      },
      [dispatch]
    );

    const onRowSelected = useCallback(
      (event: RowSelectedEvent) => {
        if (event.node.group) return;
        if (event.node.isSelected()) {
          dispatch(selectCampaign(event.data?.uuid.value));
        } else {
          dispatch(deselectCampaign(event.data?.uuid.value));
        }
      },
      [dispatch]
    );

    useEffect(() => {
      if (selectedCampaignIds?.length === 0) gridRef.current?.api?.deselectAllFiltered();
    }, [selectedCampaignIds.length]);

    const isRowSelectable = useCallback((row: RowNode) => {
      if (row.group) {
        return row.allLeafChildren.some(child => child.selectable);
      }
      return (
        row.data?.status.value === CampaignStatuses.DRAFT || row.data?.status.value === CampaignStatuses.DISAPPROVED
      );
    }, []);

    const onRowDataUpdated = useCallback(
      ({ api }: RowDataUpdatedEvent) => {
        if (shouldShowRequestApprovalButton) {
          const isDisabled = !hasCampaignsStatuses(api, [CampaignStatuses.DRAFT, CampaignStatuses.DISAPPROVED]);
          dispatch(setDisableRequestApprovalButton(isDisabled));
        }
      },
      [shouldShowRequestApprovalButton, dispatch]
    );

    useEffect(() => {
      const api = gridRef.current?.api;

      if (shouldShowRequestApprovalButton && api && mediaPlan.loading !== LoadingState.Failed) {
        const isDisabled = !hasCampaignsStatuses(api, [CampaignStatuses.DRAFT, CampaignStatuses.DISAPPROVED]);
        dispatch(setDisableRequestApprovalButton(isDisabled));
      }
    }, [dispatch, shouldShowRequestApprovalButton, mediaPlan.loading]);

    useEffect(() => {
      if (mediaPlan.loading === LoadingState.Started) {
        undo.clearUndoStack();
        undo.clearRedoStack();
      }
    }, [mediaPlan.loading, undo]);

    return (
      <SSpreadsheet accessMode={accessMode} shouldShowGroupCheckbox={isCheckboxColumnVisible}>
        <div ref={gridContainer} className="ag-theme-alpine" data-selector="media-plan-spreadsheet">
          <AgGridReact
            ref={ref => (gridRef.current = ref)}
            rowData={rowData}
            columnDefs={columnsDefinitions}
            defaultColDef={defaultColumnsConfig}
            enableRangeSelection
            suppressRowVirtualisation
            suppressColumnVirtualisation
            suppressMultiRangeSelection
            rowSelection="multiple"
            suppressRowClickSelection={!isCheckboxColumnVisible}
            groupSelectsChildren={isCheckboxColumnVisible}
            rowMultiSelectWithClick={isCheckboxColumnVisible}
            undoRedoCellEditing
            enableFillHandle
            fillHandleDirection="y"
            onRowSelected={onRowSelected}
            undoRedoCellEditingLimit={UNDO_REDO_LIMITS}
            headerHeight={SPREADSHEET_HEADER_HEIGHT}
            rowHeight={SPREADSHEET_ROW_HEIGHT}
            showOpenedGroup
            groupDisplayType="groupRows"
            groupDefaultExpanded={-1}
            groupRowRendererParams={groupRendererParams}
            getRowId={getRowId}
            groupIncludeFooter
            suppressAggFuncInHeader
            initialGroupOrderComparator={initialGroupOrderComparator}
            loadingOverlayComponent={SpreadsheetOverlay}
            loadingOverlayComponentParams={overlayContentProps}
            onBodyScroll={onBodyScroll}
            processCellForClipboard={params =>
              spreadsheetValidationModel &&
              prepareCellDataForClipboard(params, spreadsheetValidationModel, addCopiedData)
            }
            processDataFromClipboard={() => null}
            getContextMenuItems={getContextMenuItems}
            onPasteEnd={onPasteEnd}
            postSortRows={postSortRows}
            onSortChanged={onSortChanged}
            onAsyncTransactionsFlushed={syncData}
            onCellValueChanged={onCellValueChanged}
            suppressClearOnFillReduction
            fillOperation={fillOperation}
            context={context}
            onCellEditingStarted={onCellEditingStarted}
            isRowSelectable={isRowSelectable}
            onRowDataUpdated={onRowDataUpdated}
            asyncTransactionWaitMillis={ASYNC_TRANSACTION_WAIT}
            excelStyles={getExcelStyles(currencyCode)}
            sendToClipboard={sendToClipboard(copiedData, resetCopiedData)}
            isExternalFilterPresent={() => isDataFiltered}
            doesExternalFilterPass={dataFilter}
          />
        </div>

        <ConfirmationModal text={confirmModal.text} action={confirmAction} isVisible={confirmModal.isVisible} />

        <Prompt when={shouldInterruptPageReload} message={SYNC_IN_PROGRESS_WARNING_MESSAGE} />
      </SSpreadsheet>
    );
  }
);
