import { fetchGQL } from 'mweb/common/fetch/fetchGQL';
import { VideoGQL, mapVideoGQLToVODDetails } from 'mweb/common/fetch/vods';
import { GameGQL, parseGameGQL } from 'mweb/common/fetch/games';
import {
  ALL_CHANNELS,
  ChannelDetails,
  ChannelDetailsMap,
  ChannelOnlineStatus,
  MutableChannelDetailsMap,
} from 'mweb/common/reducers/data/channels';
import {
  ClipDetailsMap,
  MutableClipDetailsMap,
} from 'mweb/common/reducers/data/clips';
import {
  MutableVODDetailsMap,
  VODDetailsMap,
} from 'mweb/common/reducers/data/vods';
import { GameDetails } from 'mweb/common/reducers/data/games';
import { VideoType } from 'mweb/common/reducers/data/baseVideoDetails';
import { ChannelSubscribableStatus } from 'mweb/common/reducers/data/channels';
import { formatVideoLength } from 'mweb/common/utils/formatVideoLength';

const CoreChannelFragment: string = require('mweb/common/fetch/fragments/coreChannel');
const ChannelVideosFragment: string = require('mweb/common/fetch/fragments/channelVideos');
const ChannelSubscriptionFragment: string = require('mweb/common/fetch/fragments/channelSubscription');
const GameFragment: string = require('mweb/common/fetch/fragments/game');
const StreamFragment: string = require('mweb/common/fetch/fragments/stream');
const VideoFragment: string = require('mweb/common/fetch/fragments/video');
const ClipFragment: string = require('mweb/common/fetch/fragments/clip');
const ChannelQuery: string = require('mweb/common/fetch/queries/channel');
const ChannelsQuery: string = require('mweb/common/fetch/queries/channels');

export const CHANNEL_PAGE_SIZE = 12;

interface ClipGQL {
  id: string;
  title: string;
  thumbnailURL: string;
  url: string;
  createdAt: string;
  viewCount: number;
  durationSeconds: number;
  game: GameGQL | null;
}

interface ClipNodeGQL {
  node: ClipGQL;
}

interface StreamGQL {
  title: string | null;
  viewersCount: number | null;
  previewImageURL: string | null;
  id: string;
  game: GameGQL | null;
}

interface VideoNodeGQL {
  node: VideoGQL;
}

interface SubscriptionProductGQL {
  id: string;
}

export interface ChannelGQL {
  displayName: string | null;
  hosting?: ChannelGQL | null;
  stream: StreamGQL | null;
  bannerImageURL: string | null;
  id: string | null;
  login: string | null;
  profileViewCount: number | null;
  profileImageURL: string | null;
  description: string | null;
  followers: {
    totalCount: number;
  } | null;
  archives?: {
    edges: VideoNodeGQL[];
  } | null;
  recentClips?: {
    edges: ClipNodeGQL[];
  } | null;
  topClips?: {
    edges: ClipNodeGQL[];
  } | null;
  recentHighlights?: {
    edges: VideoNodeGQL[];
  } | null;
  topHighlights?: {
    edges: VideoNodeGQL[];
  } | null;
  recentPremieres?: {
    edges: VideoNodeGQL[];
  } | null;
  recentUploads?: {
    edges: VideoNodeGQL[];
  } | null;
  subscriptionProducts?: SubscriptionProductGQL[] | null;
}

interface EdgeGQL {
  cursor: string;
  node: {
    broadcaster: ChannelGQL;
  };
}

interface GameChannelsGQL extends GameGQL {
  streams: {
    edges: EdgeGQL[];
    pageInfo: {
      hasNextPage: boolean;
    };
  };
}

interface AllChannelsGQL {
  edges: EdgeGQL[];
  pageInfo: {
    hasNextPage: boolean;
  };
}

export interface FetchChannelsGQL {
  data: {
    gameChannels?: GameChannelsGQL | null;
    allChannels?: AllChannelsGQL;
  };
}

export interface FetchChannelGQL {
  data: {
    user: ChannelGQL | null;
  };
}

export interface ChannelDataPayload {
  channel: ChannelDetails;
  hostedChannel: ChannelDetails | undefined;
  videos: VODDetailsMap | undefined;
  clips: ClipDetailsMap | undefined;
}

export interface ChannelsDataPayload {
  channelDetails: ChannelDetailsMap;
  cursor: string | null;
  game: GameDetails | undefined;
  gameAliasUsed: string;
}

export function compileFetchChannelsOperation(): string {
  return `${ChannelsQuery}
  ${GameFragment}
  ${CoreChannelFragment}
  ${StreamFragment}
  `;
}

interface FetchChannelsOpts {
  gameAlias: string;
  cursor: string | null;
  count?: number;
}

export async function fetchChannels(
  opts: FetchChannelsOpts,
  shouldLogErrors?: (jsonError: any) => boolean,
): Promise<ChannelsDataPayload> {
  const cleanedOpts = { ...opts };
  if (opts.gameAlias === ALL_CHANNELS) {
    delete cleanedOpts.gameAlias;
  }
  const json = await fetchGQL<FetchChannelsGQL>(
    compileFetchChannelsOperation(),
    {
      count: CHANNEL_PAGE_SIZE,
      hasGame: !!cleanedOpts.gameAlias,
      ...cleanedOpts,
    },
    'ChannelsQuery',
    shouldLogErrors,
  );
  return parseChannelsGQL(json, opts.gameAlias);
}

function parseChannelsGQL(
  channelsGQL: FetchChannelsGQL,
  gameAlias: string,
): ChannelsDataPayload {
  const gameData = channelsGQL.data.gameChannels;
  let game: GameDetails | undefined;

  if (gameData) {
    game = parseGameGQL(gameData);
  }

  const streamData = gameData ? gameData.streams : channelsGQL.data.allChannels;

  if (!streamData) {
    return {
      channelDetails: {},
      cursor: null,
      gameAliasUsed: gameAlias,
      game,
    };
  }

  const channelDetails = streamData.edges.reduce(
    (acc, { node: { broadcaster } }) => {
      const details = mapChannelGQLtoChannelDetails(broadcaster);
      if (details) {
        acc[details.name] = details;
      }
      return acc;
    },
    {} as MutableChannelDetailsMap,
  );
  return {
    channelDetails,
    game,
    gameAliasUsed: gameAlias,
    cursor: streamData.pageInfo.hasNextPage
      ? streamData.edges[streamData.edges.length - 1].cursor
      : null,
  };
}

export function compileFetchChannelOperation(): string {
  return `${ChannelQuery}
  ${CoreChannelFragment}
  ${ChannelVideosFragment}
  ${ChannelSubscriptionFragment}
  ${StreamFragment}
  ${VideoFragment}
  ${ClipFragment}
  ${GameFragment}
  `;
}

export interface FetchChannelOpts {
  archiveCount?: number;
  recentClipCount?: number;
  topClipCount?: number;
  recentHighlightCount?: number;
  topHighlightCount?: number;
  recentPremiereCount?: number;
  recentUploadCount?: number;
}

export async function fetchChannel(
  login: string,
  {
    archiveCount,
    recentClipCount,
    topClipCount,
    recentHighlightCount,
    topHighlightCount,
    recentPremiereCount,
    recentUploadCount,
  }: FetchChannelOpts = {},
  shouldLogErrors?: (jsonError: any) => boolean,
): Promise<ChannelDataPayload | undefined> {
  const channelGQL = await fetchGQL<FetchChannelGQL>(
    compileFetchChannelOperation(),
    {
      withArchives: !!archiveCount,
      withRecentClips: !!recentClipCount,
      withTopClips: !!topClipCount,
      withRecentHighlights: !!recentHighlightCount,
      withTopHighlights: !!topHighlightCount,
      withRecentPremieres: !!recentPremiereCount,
      withRecentUploads: !!recentUploadCount,
      login,
      archiveCount,
      recentClipCount,
      topClipCount,
      recentHighlightCount,
      topHighlightCount,
      recentPremiereCount,
      recentUploadCount,
    },
    'ChannelQuery',
    shouldLogErrors,
  );
  return parseChannelGQL(channelGQL);
}

function parseChannelGQL({
  data: { user: channel },
}: FetchChannelGQL): ChannelDataPayload | undefined {
  if (!channel) {
    return undefined;
  }

  const channelDetails = mapChannelGQLtoChannelDetailsWithHosted(channel);
  if (!channelDetails.length) {
    return undefined;
  }
  return {
    channel: channelDetails[0],
    hostedChannel: channelDetails[1],
    videos: mapChannelGQLtoVODDetailsMap(channel),
    clips: mapChannelGQLtoClipDetailsMap(channel),
  };
}

export function mapChannelGQLtoChannelDetailsWithHosted(
  channel: ChannelGQL,
): ChannelDetails[] {
  const details: ChannelDetails[] = [];
  const channelDetails = mapChannelGQLtoChannelDetails(channel);
  if (channelDetails) {
    details.push(channelDetails);
    if (channelDetails.hostedChannel && channel.hosting) {
      const hostedChannelDetails = mapChannelGQLtoChannelDetails(
        channel.hosting,
      );
      if (hostedChannelDetails) {
        details.push(hostedChannelDetails);
      }
    }
  }
  return details;
}

export function mapChannelGQLtoChannelDetails(
  channel: ChannelGQL | undefined,
): ChannelDetails | undefined {
  if (!channel || !channel.login || !channel.id) {
    return undefined;
  }

  const bannerImageURL =
    channel.bannerImageURL && /https:\/\//.test(channel.bannerImageURL)
      ? channel.bannerImageURL
      : undefined;

  let subscribableStatus: ChannelSubscribableStatus =
    ChannelSubscribableStatus.Unknown;
  if (channel.subscriptionProducts) {
    subscribableStatus =
      channel.subscriptionProducts.length > 0
        ? ChannelSubscribableStatus.CanSubscribe
        : ChannelSubscribableStatus.CannotSubscribe;
  }

  return {
    id: channel.id,
    name: channel.login,
    displayName: channel.displayName || channel.login,
    game:
      channel.stream && channel.stream.game
        ? channel.stream.game.name
        : undefined,
    logoURL: channel.profileImageURL || undefined,
    status: (channel.stream && channel.stream.title) || undefined,
    preview: (channel.stream && channel.stream.previewImageURL) || undefined,
    viewerCount: (channel.stream && channel.stream.viewersCount) || 0,
    description: channel.description || undefined,
    followerCount: channel.followers ? channel.followers.totalCount : 0,
    hostedChannel: (channel.hosting && channel.hosting.login) || undefined,
    lifetimeViewerCount: channel.profileViewCount || 0,
    onlineStatus: buildOnlineStatus(channel),
    bannerImageURL,
    subscribableStatus,
  };
}

function buildOnlineStatus(channel: ChannelGQL): ChannelOnlineStatus {
  if (
    (channel.stream && channel.stream.id) ||
    (channel.hosting && channel.hosting.stream && channel.hosting.stream.id)
  ) {
    return ChannelOnlineStatus.Online;
  }
  return ChannelOnlineStatus.Offline;
}

function mapChannelGQLtoVODDetailsMap(
  channelGQL: ChannelGQL,
): VODDetailsMap | undefined {
  const {
    login,
    archives,
    recentHighlights,
    topHighlights,
    recentUploads,
    recentPremieres,
  } = channelGQL;
  if (
    !archives &&
    !recentHighlights &&
    !topHighlights &&
    !recentUploads &&
    !recentPremieres
  ) {
    return undefined;
  }

  const archiveArr = archives ? archives.edges : [];
  const recentHighlightsArr = recentHighlights ? recentHighlights.edges : [];
  const topHighlightsArr = topHighlights ? topHighlights.edges : [];
  const recentUploadsArr = recentUploads ? recentUploads.edges : [];
  const recentPremieresArr = recentPremieres ? recentPremieres.edges : [];
  return archiveArr
    .concat(recentHighlightsArr)
    .concat(topHighlightsArr)
    .concat(recentUploadsArr)
    .concat(recentPremieresArr)
    .reduce(
      (agg, { node: video }) => {
        agg[video.id] = mapVideoGQLToVODDetails(video, login || undefined);
        return agg;
      },
      {} as MutableVODDetailsMap,
    );
}

function mapChannelGQLtoClipDetailsMap(
  channelGQL: ChannelGQL,
): ClipDetailsMap | undefined {
  const { login: channel, recentClips, topClips } = channelGQL;
  if (!recentClips && !topClips) {
    return undefined;
  }
  const recentClipsArr = recentClips ? recentClips.edges : [];
  const topClipsArr = topClips ? topClips.edges : [];
  return recentClipsArr.concat(topClipsArr).reduce((agg, { node: clip }) => {
    agg[clip.id] = {
      channel: channel || undefined,
      id: clip.id,
      title: clip.title,
      thumbnailURL: clip.thumbnailURL,
      url: clip.url,
      date: new Date(clip.createdAt).valueOf(),
      viewCount: clip.viewCount,
      length: clip.durationSeconds,
      formattedLength: formatVideoLength(clip.durationSeconds),
      game: clip.game ? clip.game.name : undefined,
      videoType: VideoType.Clip,
    };
    return agg;
  },
  {} as MutableClipDetailsMap);
}
