import { MaybePromise, Undefinable } from 'tsdef';

import { FileActionData, FileActionState } from '../../types/action-handler.types';
import { FileAction } from '../../types/action.types';
import { ChonkyDispatch, ChonkyThunk } from '../../types/redux.types';
import { Logger } from '../../util/logger';
import { reduxActions } from '../reducers';
import {
  selectContextMenuTriggerFile,
  selectExternalFileActionHandler,
  selectFileActionMap,
  selectInstanceId,
  selectSelectedFiles,
} from '../selectors';
import { thunkActivateSortAction, thunkApplySelectionTransform } from './file-actions.thunks';

/**
 * Thunk that dispatches actions to the external (user-provided) action handler.
 */
export const thunkDispatchFileAction =
  (data: FileActionData<FileAction>): ChonkyThunk =>
  (_dispatch, getState) => {
    Logger.debug(`FILE ACTION DISPATCH: [${data.id}]`, 'data:', data);
    const state = getState();
    const action = selectFileActionMap(state)[data.id];
    const externalFileActionHandler = selectExternalFileActionHandler(state);
    if (action) {
      if (externalFileActionHandler) {
        Promise.resolve(externalFileActionHandler(data)).catch((error) =>
          Logger.error(`User-defined file action handler threw an error: ${error.message}`),
        );
      }
    } else {
      Logger.warn(
        `Internal components dispatched the "${data.id}" file action, but such ` + `action was not registered.`,
      );
    }
  };

/**
 * Thunk that is used by internal components (and potentially the user) to "request"
 * actions. When action is requested, Chonky "prepares" the action data by extracting it
 * from Redux state. Once action data is ready, Chonky executes some side effect and/or
 * dispatches the action to the external action handler.
 */
export const thunkRequestFileAction =
  <Action extends FileAction>(action: Action, payload: Action['__payloadType']): ChonkyThunk =>
  (dispatch, getState) => {
    Logger.debug(`FILE ACTION REQUEST: [${action.id}]`, 'action:', action, 'payload:', payload);
    const state = getState();
    const instanceId = selectInstanceId(state);

    if (!selectFileActionMap(state)[action.id]) {
      Logger.warn(
        `The action "${action.id}" was requested, but it is not registered. The ` +
          `action will still be dispatched, but this might indicate a bug in ` +
          `the code. Please register your actions by passing them to ` +
          `"fileActions" prop.`,
      );
    }

    // Determine files for the action if action requires selection
    const selectedFiles = selectSelectedFiles(state);
    const selectedFilesForAction = action.fileFilter ? selectedFiles.filter(action.fileFilter) : selectedFiles;
    if (action.requiresSelection && selectedFilesForAction.length === 0) {
      Logger.warn(
        `Internal components requested the "${action.id}" file ` +
          `action, but the selection for this action was empty. This ` +
          `might a bug in the code of the presentational components.`,
      );
      return;
    }

    const contextMenuTriggerFile = selectContextMenuTriggerFile(state);
    const actionState: FileActionState<{}> = {
      instanceId,
      selectedFiles,
      selectedFilesForAction,
      contextMenuTriggerFile,
    };

    // === Update sort state if necessary
    const sortKeySelector = action.sortKeySelector;
    if (sortKeySelector) dispatch(thunkActivateSortAction(action.id));

    // === Update file view state if necessary
    const fileViewConfig = action.fileViewConfig;
    if (fileViewConfig) dispatch(reduxActions.setFileViewConfig(fileViewConfig));

    // === Update option state if necessary
    const option = action.option;
    if (option) dispatch(reduxActions.toggleOption(option.id));

    // === Apply selection transform if necessary
    const selectionTransform = action.selectionTransform;
    if (selectionTransform) dispatch(thunkApplySelectionTransform(action));

    // Apply the effect
    const effect = action.effect;
    let maybeEffectPromise: MaybePromise<boolean | undefined> = undefined;
    if (effect) {
      try {
        maybeEffectPromise = effect({
          action,
          payload,
          state: actionState,
          reduxDispatch: dispatch,
          getReduxState: getState,
        }) as MaybePromise<boolean | undefined>;
      } catch (err) {
        const error = err as Error;
        Logger.error(`User-defined effect function for action ${action.id} threw an ` + `error: ${error.message}`);
      }
    }

    // Dispatch the action to user code. Deliberately call it after all other
    // operations are over.
    return Promise.resolve(maybeEffectPromise)
      .then((effectResult) => {
        const data: FileActionData<Action> = {
          id: action.id,
          action,
          payload,
          state: actionState,
        };
        triggerDispatchAfterEffect(dispatch, data, effectResult);
      })
      .catch((error) => {
        Logger.error(
          `User-defined effect function for action ${action.id} returned a ` +
            `promise that was rejected: ${error.message}`,
        );
        const data: FileActionData<Action> = {
          id: action.id,
          action,
          payload,
          state: actionState,
        };
        triggerDispatchAfterEffect(dispatch, data, undefined);
      });
  };

export const triggerDispatchAfterEffect = <Action extends FileAction>(
  dispatch: ChonkyDispatch,
  data: FileActionData<Action>,
  effectResult: Undefinable<boolean>,
) => {
  const preventDispatch = effectResult === true;
  if (!preventDispatch) dispatch(thunkDispatchFileAction(data));
};
