import {
  BodyScrollEvent,
  CellEditRequestEvent,
  ColDef,
  FillOperationParams,
  GetRowIdParams,
  PasteEndEvent,
  RowDataUpdatedEvent,
  RowNode,
  RowSelectedEvent,
  SuppressKeyboardEventParams,
} from 'ag-grid-community';
import 'ag-grid-community/dist/styles/ag-grid.css?raw';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css?raw';
import 'ag-grid-enterprise';
import { AgGridReact } from 'ag-grid-react';
import { CampaignStatuses, Id, SubmitApprovalCampaign, Taxonomy } from 'backend-api/models';
import { getProjectName } from 'common-v2/transducers';
import { removeToasts } from 'common/actions';
import { showToast, ToastType } from 'common/components/toast';
import { activeProjectSelector } from 'common/selectors';
import { LoadingState } from 'common/types';
import { usePrevious } from 'hooks';
import { throttle } from 'lodash';
import {
  addCampaignForApproval,
  deselectCampaign,
  selectCampaign,
  setDisableRequestApprovalButton,
} from 'media-plan/actions';
import { useShowRequestApprovalButton } from 'media-plan/hooks/use-show-request-approval-button';
import {
  confirmModalSelector,
  mediaPlanSelector,
  selectedCampaignsIdsSelector,
  spreadsheetValidationModelSelector,
} from 'media-plan/selectors';
import { MediaPlanMode } from 'media-plan/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, useConfirm, useContextMenu, useCopyPaste, useHotKeys, useSync, useUndo } from './hooks';
import { SSpreadsheet } from './s-spreadsheet';
import {
  exportMediaPlanToExcel,
  getExcelStyles,
  handleRemovingValue,
  hasCampaignsStatuses,
  initialGroupOrderComparator,
  isCheckboxShown,
  isReadOnlyMode,
  isRedoKey,
  isUndoKey,
  onSortChanged,
  postSortRows,
  prepareCellDataForClipboard,
  processPlacements,
  sendToClipboard,
} from './transducers';
import {
  GroupRowRendererParams,
  MediaPlanAccessMode,
  MediaPlanColumnId,
  SpreadsheetContext,
  SpreadsheetRowData,
} from './types';

interface Props {
  projectId: Id;
  mediaPlanId: Id;
  data: SpreadsheetRowData[];
  reviewedCampaigns: SubmitApprovalCampaign[];
  columnsDefinitions?: ColDef[];
  accessMode: MediaPlanAccessMode;
  mediaPlanMode: MediaPlanMode;
  taxonomy?: Taxonomy;
  isLoading?: boolean;
  isFilterEnabled?: boolean;
  haseError?: boolean;
  currencyCode: string;
}

export interface SpreadsheetRef {
  export(): void;
}

export const Spreadsheet = React.forwardRef(
  (
    {
      projectId,
      mediaPlanId,
      data,
      reviewedCampaigns,
      columnsDefinitions,
      accessMode,
      taxonomy,
      isLoading,
      haseError,
      mediaPlanMode,
      isFilterEnabled,
      currencyCode,
    }: Props,
    ref: Ref<SpreadsheetRef>
  ) => {
    const gridRef = useRef<AgGridReact | null>();
    const gridContainer = useRef<HTMLDivElement>(null);

    const dispatch = useDispatch();

    const mediaPlan = useSelector(mediaPlanSelector);
    const confirmModal = useSelector(confirmModalSelector);
    const project = useSelector(activeProjectSelector);
    const spreadsheetValidationModel = useSelector(spreadsheetValidationModelSelector);
    const selectedCampaignIds = useSelector(selectedCampaignsIdsSelector);

    const shouldShowCheckboxes = useMemo(() => isCheckboxShown(mediaPlanMode), [mediaPlanMode]);

    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 previousFilterEnabled = usePrevious(isFilterEnabled);
    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 || reviewedCampaigns.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),
      []
    );

    useEffect(() => {
      gridRef.current?.api?.refreshCells({ force: true });
      const columnApi = gridRef.current?.columnApi;
      if (!columnApi) return;
      if (shouldShowCheckboxes) {
        columnApi.applyColumnState({
          state: [
            { colId: MediaPlanColumnId.ACTIONS, hide: true },
            { colId: MediaPlanColumnId.CHECKBOX, hide: false },
          ],
        });
      } else {
        columnApi.applyColumnState({
          state: [
            { colId: MediaPlanColumnId.ACTIONS, hide: false },
            { colId: MediaPlanColumnId.CHECKBOX, hide: true },
          ],
        });
      }
    }, [shouldShowCheckboxes]);

    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) {
          showToast({
            id: 'media-plan-paste',
            type: ToastType.Error,
            message: "Error Occurred! We're sorry, you cannot update read-only data.",
            replaceable: true,
          });
        }
      },
      [accessMode]
    );

    const shouldShowOverlay = isLoading || haseError || !columnsDefinitions;

    useEffect(() => {
      if (shouldShowOverlay) {
        gridRef.current?.api?.showLoadingOverlay();
      } else {
        gridRef.current?.api?.hideOverlay();
      }
    }, [shouldShowOverlay]);

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

    const overlayContentProps = useMemo(() => {
      if (isLoading || !columnsDefinitions) {
        return { type: OverlayType.LOADING };
      } else if (haseError) {
        return { type: OverlayType.ERROR };
      }

      return {
        type: OverlayType.NONE,
      };
    }, [columnsDefinitions, haseError, isLoading]);

    useEffect(() => {
      if (!shouldShowOverlay) return;
      gridRef.current?.api?.hideOverlay();
      gridRef.current?.api?.showLoadingOverlay();
    }, [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);

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

          return true;
        }

        return !!DEFAULT_COLUMNS_CONFIG.suppressKeyboardEvent?.(params);
      },
      [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) 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 as MediaPlanColumnId, 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) {
          showToast({
            id: 'media-plan-paste',
            type: ToastType.Error,
            message: "Error Occurred! We're sorry, Placements inconsistent with Platforms were pasted into the table.",
            replaceable: true,
          });

          return currentPlacements;
        }

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

      return value;
    }, []);

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

    const hideToasts = useCallback(() => {
      dispatch(removeToasts());
    }, [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(() => {
      if (isFilterEnabled !== previousFilterEnabled) {
        gridRef.current?.api?.onFilterChanged();
      }
    }, [isFilterEnabled, previousFilterEnabled]);

    const isExternalFilterPresent = useCallback(() => !!isFilterEnabled, [isFilterEnabled]);

    const doesExternalFilterPass = useCallback(
      ({ data }: RowNode) => {
        if (!data || mediaPlanMode !== MediaPlanMode.APPROVAL) return true;
        return data.editableInApproval.value;
      },
      [mediaPlanMode]
    );

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

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

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

    return (
      <SSpreadsheet accessMode={accessMode} shouldShowGroupCheckbox={shouldShowCheckboxes}>
        <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={!shouldShowCheckboxes}
            groupSelectsChildren={shouldShowCheckboxes}
            rowMultiSelectWithClick={shouldShowCheckboxes}
            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={hideToasts}
            isRowSelectable={isRowSelectable}
            onRowDataUpdated={onRowDataUpdated}
            isExternalFilterPresent={isExternalFilterPresent}
            doesExternalFilterPass={doesExternalFilterPass}
            asyncTransactionWaitMillis={ASYNC_TRANSACTION_WAIT}
            excelStyles={getExcelStyles(currencyCode)}
            sendToClipboard={sendToClipboard(copiedData, resetCopiedData)}
          />
        </div>
        <ConfirmationModal text={confirmModal.text} action={confirmAction} isVisible={confirmModal.isVisible} />
        <Prompt when={shouldInterruptPageReload} message={SYNC_IN_PROGRESS_WARNING_MESSAGE} />
      </SSpreadsheet>
    );
  }
);
