import { Dispatch } from 'redux';

import { logger } from 'tachyon-logger';
import { RootState } from 'mweb/common/reducers/root';
import { ALL_CHANNELS } from 'mweb/common/reducers/data/channels';
import {
  ChannelDataPayload,
  ChannelsDataPayload,
  FetchChannelOpts,
  fetchChannel,
  fetchChannels,
} from 'mweb/common/fetch/channels';
import { GQLError } from 'mweb/common/fetch/fetchGQL';
import { getGameNameForGameAlias } from 'mweb/common/selectors/data/games';
import {
  doesChannelDataNeedsReinitForGame,
  areAllChannelsLoadedForGame,
} from 'mweb/common/selectors/data/channels';

export const UNEXPECTED_404_ERROR_MESSAGES = [
  // Error when the channel doesn't exist because GQL doesn't handle our query properly.
  'missing userID',
  // Part of the error message graphql responds with when the channel contains invalid
  // characters, and thus couldn't exist.
  'Error (400):',
  // Don't even know how this isn't 404
  'unable to identify user',
  // New error messaged introduced with InlineIdentifierValidation: true in visage
  'Users.GetUsers: No login names, emails, IDs or display names in request',
  // Happens when a channel has been renamed and the sub product doesn't exist.
  '301 response missing Location header',
];

export const INVALID_LOGIN_CHARACTERS = /[-.]/;

export const CHANNELS_DATA_REINITIALIZED_GAME_ACTION_TYPE =
  'CHANNELS_DATA_REINITIALIZED_GAME_ACTION_TYPE';
export interface ChannelsDataReinitializedGameAction {
  type: typeof CHANNELS_DATA_REINITIALIZED_GAME_ACTION_TYPE;
  payload: {
    game: string;
  };
}

export const CHANNELS_DATA_PAGE_LOADED_ACTION_TYPE =
  'CHANNELS_DATA_PAGE_LOADED_ACTION_TYPE';
export interface ChannelsDataPageLoadedAction {
  type: typeof CHANNELS_DATA_PAGE_LOADED_ACTION_TYPE;
  payload: ChannelsDataPayload;
}

export const CHANNELS_DATA_CHANNEL_LOADED_ACTION_TYPE =
  'CHANNELS_DATA_CHANNEL_LOADED_ACTION_TYPE';

export interface ChannelsDataChannelLoadedAction {
  type: typeof CHANNELS_DATA_CHANNEL_LOADED_ACTION_TYPE;
  payload: ChannelDataPayload;
}

export const CHANNELS_DATA_PAGE_FAILED_ACTION_TYPE =
  'CHANNELS_DATA_PAGE_FAILED_ACTION_TYPE';
export interface ChannelsDataPageFailedAction {
  type: typeof CHANNELS_DATA_PAGE_FAILED_ACTION_TYPE;
  payload: Response;
}

export const CHANNELS_DATA_CHANNEL_FAILED_ACTION_TYPE =
  'CHANNELS_DATA_CHANNEL_FAILED_ACTION_TYPE';
export interface ChannelsDataChannelFailedAction {
  type: typeof CHANNELS_DATA_CHANNEL_FAILED_ACTION_TYPE;
  payload: GQLError;
}

export const CHANNELS_DATA_CHANNEL_HOSTING_STATUS_UPDATED_ACTION_TYPE =
  'CHANNELS_DATA_CHANNEL_HOSTING_STATUS_UPDATED_ACTION_TYPE';
export interface ChannelsDataChannelHostingStatusUpdatedAction {
  type: typeof CHANNELS_DATA_CHANNEL_HOSTING_STATUS_UPDATED_ACTION_TYPE;
  payload: {
    channel: string;
    hostedChannel: string;
  };
}

export type ChannelsDataAction =
  | ChannelsDataReinitializedGameAction
  | ChannelsDataPageLoadedAction
  | ChannelsDataChannelLoadedAction
  | ChannelsDataPageFailedAction
  | ChannelsDataChannelFailedAction
  | ChannelsDataChannelHostingStatusUpdatedAction;

interface ChannelsDataFetchNextPageOpts {
  isOnePageEnough?: boolean;
}

export function channelsDataGetPage(
  gameAlias: string,
  opts: ChannelsDataFetchNextPageOpts = {},
): (dispatch: Dispatch<RootState>, getState: () => RootState) => Promise<void> {
  return async (dispatch, getState) => {
    const state = getState();
    let gameName: string | undefined;
    let cursor: string | null = null;

    if (gameAlias === ALL_CHANNELS) {
      gameName = ALL_CHANNELS;
    } else {
      gameName = getGameNameForGameAlias(state, gameAlias);
    }

    if (!gameName) {
      /**
       * we need to load the first page of data because
       * we haven't seen this alias before
       */
      logger.info(`Init ${gameAlias} channels data and loading first page`);
    } else if (doesChannelDataNeedsReinitForGame(state, gameName)) {
      /**
       * we need to load the first page of data because either:
       * - we haven't loaded channels for this game before
       * - we didn't find any channels the last time we looked
       * - we have loaded channels but that data is considered stale
       * - we're looking for ALL_CHANNELS and haven't loaded it before
       *                 (similar to first case)
       */
      logger.info(`Init ${gameName} channels data and loading first page`);
      dispatch(channelsDataReinitializeForGame(gameName));
    } else if (opts.isOnePageEnough) {
      /**
       * we have loaded channels for this game before and
       * the data is fresh enough and we only need one page
       */
      logger.info(
        `Skipping ${gameName} channels data request as initial data already loaded`,
      );
      return;
    } else if (areAllChannelsLoadedForGame(state, gameName)) {
      /**
       * we have loaded all the channels for the game
       */
      logger.info(
        `Skipping ${gameName} channels data request as all channels already loaded`,
      );
      return;
    } else {
      /**
       * we need to load the next "page" of channels for the game
       */
      logger.info(`Attempting to fetch next page of ${gameName} channels data`);
      cursor =
        state.data.channels.channelsByGameLoadStatus[gameName]
          .lastChannelCursor;
    }

    return dispatch(channelsDataFetchPage(gameName || gameAlias, cursor));
  };
}

export function errorsContain404(jsonErrors: any): boolean {
  return jsonErrors.some && jsonErrors.some(errorIsUnexpected404);
}

export function channelsDataFetchPage(
  gameAlias: string,
  cursor: string | null,
): (dispatch: Dispatch<RootState>) => Promise<void> {
  return async dispatch => {
    logger.info(
      `Fetching channels for ${gameAlias} from ${cursor || 'initial page'}`,
    );
    try {
      const data = await fetchChannels(
        { gameAlias, cursor },
        (errors: any) => !errorsContain404(errors),
      );
      dispatch(channelsDataLoadPage(data));
    } catch (channelsDataFetchPageError) {
      logger.warn({ channelsDataFetchPageError });
      dispatch(channelsDataFailPage(channelsDataFetchPageError));
    }
  };
}

export function channelsDataFetchChannel(
  channel: string,
  opts: FetchChannelOpts = {},
): (dispatch: Dispatch<RootState>) => Promise<void> {
  return async dispatch => {
    if (channel.match(INVALID_LOGIN_CHARACTERS)) {
      logger.info(
        `Shortcutting to 404 due to invalid character in ${channel}.`,
      );
      dispatch(channelsDataFailChannel({ status: 404 }));
      return;
    }
    logger.info(`Attempting to load channel data for ${channel}`);
    try {
      const data = await fetchChannel(
        channel,
        opts,
        (errors: any) => !errorsContain404(errors),
      );
      if (data) {
        dispatch(channelsDataLoadChannel(data));
      } else {
        dispatch(channelsDataFailChannel({ status: 404 }));
      }
    } catch (channelsDataFetchChannelError) {
      logger.warn({ channelsDataFetchChannelError });
      if (
        channelsDataFetchChannelError.errors &&
        errorsContain404(channelsDataFetchChannelError.errors)
      ) {
        logger.error('unexpected 404');
        dispatch(channelsDataFailChannel({ status: 404 }));
      } else {
        dispatch(channelsDataFailChannel(channelsDataFetchChannelError));
      }
    }
  };
}

function errorIsUnexpected404(
  error: { message: string } = { message: '' },
): boolean {
  return UNEXPECTED_404_ERROR_MESSAGES.some(possible404message =>
    error.message.includes(possible404message),
  );
}

export function channelsDataLoadPage(
  payload: ChannelsDataPayload,
): ChannelsDataPageLoadedAction {
  return {
    type: CHANNELS_DATA_PAGE_LOADED_ACTION_TYPE,
    payload,
  };
}

export function channelsDataLoadChannel(
  payload: ChannelDataPayload,
): ChannelsDataChannelLoadedAction {
  return {
    type: CHANNELS_DATA_CHANNEL_LOADED_ACTION_TYPE,
    payload,
  };
}

export function channelsDataFailPage(
  error: Response,
): ChannelsDataPageFailedAction {
  return {
    type: CHANNELS_DATA_PAGE_FAILED_ACTION_TYPE,
    payload: error,
  };
}

export function channelsDataFailChannel(
  error: GQLError,
): ChannelsDataChannelFailedAction {
  return {
    type: CHANNELS_DATA_CHANNEL_FAILED_ACTION_TYPE,
    payload: error,
  };
}

export function channelsDataReinitializeForGame(
  game: string,
): ChannelsDataReinitializedGameAction {
  return {
    type: CHANNELS_DATA_REINITIALIZED_GAME_ACTION_TYPE,
    payload: {
      game,
    },
  };
}

export function channelsDataUpdateChannelHostingStatus(
  channel: string,
  hostedChannel: string = '',
): (dispatch: Dispatch<RootState>) => Promise<void> {
  return async dispatch => {
    if (hostedChannel) {
      await dispatch(channelsDataFetchChannel(hostedChannel));
    }
    dispatch(
      channelsDataBuildUpdateChannelHostingStatusAction(channel, hostedChannel),
    );
  };
}

export function channelsDataBuildUpdateChannelHostingStatusAction(
  channel: string,
  hostedChannel: string,
): ChannelsDataChannelHostingStatusUpdatedAction {
  return {
    type: CHANNELS_DATA_CHANNEL_HOSTING_STATUS_UPDATED_ACTION_TYPE,
    payload: {
      channel,
      hostedChannel,
    },
  };
}

// Tracked event

export interface ChannelsDataPageLoadedEventPayload {
  channelsLoaded: number;
  game: string;
}

export const CHANNELS_DATA_PAGE_LOADED_EVENT_TYPE =
  'CHANNELS_DATA_PAGE_LOADED_EVENT_TYPE';
export interface ChannelsDataPageLoadedEvent {
  type: typeof CHANNELS_DATA_PAGE_LOADED_EVENT_TYPE;
  payload: ChannelsDataPageLoadedEventPayload;
}
