import { combineReducers } from 'redux';
import { intlReducer, IntlState } from 'react-intl-redux';
import values from 'lodash-es/values';
import sortBy from 'lodash-es/sortBy';

import { Action } from 'mweb/common/actions/root';
import {
  app,
  AppState,
  Platform,
  Location,
  MobileOptOutState,
} from 'mweb/common/reducers/app';
import { device, DeviceState, OS } from 'mweb/common/reducers/device';
import {
  ExperimentsState,
  buildExperimentsReducer,
} from 'mweb/common/reducers/experiments';
import { tracking, TrackingState } from 'mweb/common/reducers/tracking';
import { chat, ChatState } from 'mweb/chat/chatReducer';
import {
  buildDataRootReducer,
  DataRootState,
} from 'mweb/common/reducers/data/root';
import { GameDetails } from 'mweb/common/reducers/data/games';
import {
  buildPagesRootReducer,
  PagesRootState,
} from 'mweb/common/reducers/pages/root';
import { ChannelDetails } from 'mweb/common/reducers/data/channels';
import { VODDetails } from 'mweb/common/reducers/data/vods';
import { ClipDetails } from 'mweb/common/reducers/data/clips';
import { EventModel } from 'mweb/common/reducers/data/events';
import { getChannelDetails } from 'mweb/common/selectors/data/channels';
import { VideoType } from 'mweb/common/reducers/data/baseVideoDetails';

export enum StatusCode {
  SUCCESS = 200,
  MOVED_PERMANENTLY = 301,
  FOUND = 302,
  TEMPORARY_REDIRECT = 307,
  PERMANENT_REDIRECT = 308,
  BAD_REQUEST = 400,
  NOT_FOUND = 404,
  UNPROCESSABLE = 422,
  INTERNAL_SERVER_ERROR = 500,
}

export interface RootState {
  readonly app: AppState;
  readonly data: DataRootState;
  readonly device: DeviceState;
  readonly experiments: ExperimentsState;
  readonly pages: PagesRootState;
  readonly tracking: TrackingState;
  readonly chat: ChatState;
  readonly intl: IntlState;
}

export function buildRootReducer(): (
  state: RootState | undefined,
  action: Action,
) => RootState {
  return combineReducers<RootState>({
    app,
    data: buildDataRootReducer(),
    device,
    experiments: buildExperimentsReducer(),
    pages: buildPagesRootReducer(),
    tracking,
    chat,
    intl: intlReducer,
  });
}

export function getIsOptedOut(state: RootState): MobileOptOutState {
  return state.app.mobileOptOut;
}

// selectors/pages/gameDirectory
export function gameDirectoryIsLoading(state: RootState): boolean {
  return Object.keys(state.data.games.gameDetails).length === 0;
}

export function isSupportedMobileOS(state: RootState): boolean {
  return ![OS.Microsoft, OS.Other, OS.Unknown].includes(state.device.os);
}
export function isOnClient(state: RootState): boolean {
  return state.app.platform === Platform.Client;
}

export function statusNotFound(state: RootState): boolean {
  const status = state.app.status;
  return (
    status === StatusCode.NOT_FOUND ||
    status === StatusCode.UNPROCESSABLE ||
    status === StatusCode.BAD_REQUEST
  );
}

export function statusSuccess(state: RootState): boolean {
  return !!state.app.status && 199 < state.app.status && state.app.status < 300;
}

export function statusInternalError(state: RootState): boolean {
  return !statusNotFound(state) && !statusSuccess(state);
}

function unknownLocation(location: never): never;
function unknownLocation(_: Location): StatusCode {
  return StatusCode.INTERNAL_SERVER_ERROR;
}

/**
 * Selector for handling return status when served from Lambda.
 *
 * NOTE: The cyclomatic complexity of this seems pretty unreasonable. It might make more sense to have the different
 * pages report their own statuses into the app, I think that's the next step in this refactor.
 */

export function returnStatus(state: RootState): StatusCode {
  switch (state.app.location) {
    case Location.DirectoryGame:
    case Location.DirectoryMainGame:
    case Location.Channel:
    case Location.VOD:
    case Location.ChatEmbed:
    case Location.ChannelProfile:
    case Location.EventDetails:
      if (statusInternalError(state)) {
        return StatusCode.INTERNAL_SERVER_ERROR;
      } else if (statusNotFound(state)) {
        return StatusCode.NOT_FOUND;
      }
      return StatusCode.SUCCESS;
    // We always serve these pages regardless since they can render without any data.
    case Location.Unknown:
    case Location.Upsell:
      return StatusCode.SUCCESS;
    default:
      // Make sure we handle new pages properly
      return unknownLocation(state.app.location);
  }
}

export function chatTargetDetails(
  state: RootState,
): { name: string; id: string | undefined } {
  const channelName =
    state.pages.channelViewer.currentChannel ||
    state.pages.chatEmbed.currentChannel;
  const channel = getChannelDetails(state, channelName);
  return {
    name: channelName,
    id: channel && channel.id,
  };
}

export function isChatReady(state: RootState): boolean {
  return state.chat.messages.length > 0;
}

export function isFixedTopNav(state: RootState): boolean {
  return ![Location.Channel, Location.VOD].includes(state.app.location);
}

// selectors/pages/gameDirectory
export function gameList(state: RootState): GameDetails[] {
  return sortBy(
    values(state.data.games.gameDetails),
    details => -details.viewersCount,
  );
}

// selectors/pages/gameDirectory
export function doesGameDirectoryDataNeedReinitialization(
  state: RootState,
  cacheTTL: number,
): boolean {
  const gamesDataState = state.data.games;
  return (
    Object.keys(gamesDataState.gameDetails).length === 0 ||
    gamesDataState.gamesLoadStatus.lastInitTime <
      new Date().valueOf() - cacheTTL
  );
}

// selectors/pages/gameDirectory
export function areAllGamesLoaded(state: RootState): boolean {
  return (
    Object.keys(state.data.games.gameDetails).length > 0 &&
    !state.data.games.gamesLoadStatus.lastGameCursor
  );
}

export function getCurrentChannelForChannelViewer(
  state: RootState,
): ChannelDetails | undefined {
  return getChannelDetails(state, state.pages.channelViewer.currentChannel);
}

export function getCurrentChannelForVODViewer(
  state: RootState,
): ChannelDetails | undefined {
  const vod = getCurrentVODForVODViewer(state);
  return vod && getChannelDetails(state, vod.channel);
}

export function getCurrentChannelForProfile(
  state: RootState,
): ChannelDetails | undefined {
  return getChannelDetails(state, state.pages.channelProfile.currentChannel);
}

export function getCurrentChannelForChatEmbed(
  state: RootState,
): ChannelDetails | undefined {
  return getChannelDetails(state, state.pages.chatEmbed.currentChannel);
}

function unknownLocationForChannel(location: never): never;
function unknownLocationForChannel(_: Location): undefined {
  return undefined;
}

/**
 * The getCurrentChannel selector returns currently viewed channel based on app
 * location. If the current location does not have a channel, returns undefined.
 * @param state
 */
export function getCurrentChannel(
  state: RootState,
): ChannelDetails | undefined {
  switch (state.app.location) {
    case Location.Channel:
      return getCurrentChannelForChannelViewer(state);
    case Location.ChannelProfile:
      return getCurrentChannelForProfile(state);
    case Location.ChatEmbed:
      return getCurrentChannelForChatEmbed(state);
    case Location.VOD:
      return getCurrentChannelForVODViewer(state);
    case Location.Upsell:
    case Location.DirectoryMainGame:
    case Location.DirectoryGame:
    case Location.EventDetails:
    case Location.Upsell:
    case Location.Unknown:
      return undefined;
    default:
      return unknownLocationForChannel(state.app.location);
  }
}

export function getCurrentVODForVODViewer(
  state: RootState,
): VODDetails | undefined {
  return getVODDetails(state, state.pages.vodViewer.currentVODid);
}

export enum VODSortTarget {
  Date = 'date',
  ViewCount = 'viewCount',
}

interface GetVODsForChannelsOpts {
  channelName: string;
  count: number;
  videoType: VideoType;
  sortTarget?: VODSortTarget;
}

export function getVODsForChannel(
  state: RootState,
  { channelName, count, sortTarget, videoType }: GetVODsForChannelsOpts,
): VODDetails[] {
  const vodDetails = state.data.vods.vodDetails;
  return Object.keys(vodDetails)
    .reduce(
      (agg, vodID) => {
        const vod = vodDetails[vodID];
        if (vod.channel === channelName && vod.videoType === videoType) {
          agg.push(vod);
        }
        return agg;
      },
      [] as VODDetails[],
    )
    .sort((a, b) => {
      switch (sortTarget) {
        case VODSortTarget.Date:
          return b.date - a.date;
        case VODSortTarget.ViewCount:
          return b.viewCount - a.viewCount;
        default:
          return 0;
      }
    })
    .slice(0, count);
}

interface GetClipsForChannelsOpts {
  channelName: string;
  count: number;
  filterForLastWeek: boolean;
}

export function getClipsForChannel(
  state: RootState,
  { channelName, count, filterForLastWeek }: GetClipsForChannelsOpts,
): ClipDetails[] {
  let weekAgo = new Date();
  weekAgo.setDate(weekAgo.getDate() - 7);

  const clipDetails = state.data.clips.clipDetails;
  return Object.keys(clipDetails)
    .reduce(
      (agg, clipID) => {
        const clip = clipDetails[clipID];
        if (clip.channel === channelName) {
          agg.push(clip);
        }
        return agg;
      },
      [] as ClipDetails[],
    )
    .filter(a => !filterForLastWeek || a.date >= weekAgo.valueOf())
    .sort((a, b) => b.viewCount - a.viewCount)
    .slice(0, count);
}

// selectors/data/vods
export function getVODDetails(
  state: RootState,
  vodID: string,
): VODDetails | undefined {
  return state.data.vods.vodDetails[vodID];
}

export function getCurrentEventForEventDetails(
  state: RootState,
): EventModel | undefined {
  return state.data.events.eventDetails[
    state.pages.eventDetails.currentEventID
  ];
}

/**
 * Determine if the "Open in App" upsells should be present.
 *
 * The "Open in App" style upsells should not be visible when:
 * * On pages that are not supported in the application.
 * * On unsupported operating systems.
 *
 * @param state
 * @returns  A boolean determining if the "Open in App" upsells should be shown.
 */
export function showOpenInApp(state: RootState): boolean {
  // Event Detail page is not supported in native mobile apps.
  const isEventDetail = state.app.location === Location.EventDetails;
  return isSupportedMobileOS(state) && !isEventDetail;
}
