import { logger } from 'tachyon-logger';
import fs from 'fs';
import path from 'path';
import pathToRegexp from 'path-to-regexp';
import url from 'url';

import { ROUTES } from 'mweb/common/routes';
import { parseCookie } from 'mweb/common/utils/cookie';
import { buildChannelPath } from 'mweb/common/utils/pathBuilders';
import {
  CHANNEL_OFFLINE_ERROR_NAME,
  OFFLINE_CHANNEL_REDIRECT_VALUE,
  Render,
  Renderer,
  RenderLocation,
} from 'mweb/server/renderer';
import { respondThroughCallback } from 'mweb/server/responder';
import { LambdaRedirectManager } from 'mweb/lambda/lambdaRedirectManager';
import { redirectForLiveToVOD } from 'mweb/lambda/redirectForLiveToVOD';
import { getLoggingCallback } from 'mweb/lambda/callback-decorator';
import { parseExperimentOverrides } from 'mweb/lambda/experiment-override';
import { StatusCode } from 'mweb/common/reducers/root';
import {
  PageExpiry,
  buildCacheControlHeader,
} from 'mweb/server/selectors/cache';

const INDEX_TEMPLATE = String(
  fs.readFileSync(path.join(__dirname, './index.html')),
);
const CHAT_EMBED_TEMPLATE = String(
  fs.readFileSync(path.join(__dirname, './chatEmbed.html')),
);

export type EventLocation =
  | 'directory_game'
  | 'directory_main_game'
  | 'mobile_upsell'
  | 'not_found'
  | 'event_details'
  | 'chat_embed'
  | 'live_to_vod'
  | 'channel_profile'
  | 'vod'
  | '';

// TODO: Make this a union of discriminated types
type ChannelEvent = {
  channel: string;
  vod_id: string;
  id: string; // this should go away once APIG changes are deployed in the future
  game: string;
  cookie: string;
  language: string;
  eventID: string; // this should go away once APIG changes are deployed in the future
  event_id: string;
  // TODO: Consider dispatching off of this instead of inferring from set values.
  location: EventLocation;
  pathParts: ReadonlyArray<string>;
  redirectFromDesktop: string;
  tt_content: string;
  tt_medium: string;
  theme: string;
  fontSize: string;
  channel_id: string;
  stream_id: string;
  bucket_id: string;
  experiment_overrides: string;
};

const CONSOLE_LOG_FULL_RUN_TIMER = 'COMPLETE_RESPONSE';
const DEFAULT_LOCALE = 'en-us';

export interface RedirectResponse {
  statusCode:
    | StatusCode.MOVED_PERMANENTLY
    | StatusCode.FOUND
    | StatusCode.PERMANENT_REDIRECT
    | StatusCode.TEMPORARY_REDIRECT;
  headers: {
    location: string;
    cacheControl: string;
  };
}

// Polyfill intl since Lambda's Node 4.3.2 doesn't provide it fully.
global.Intl = require('intl');

export async function channelHandler(
  event: ChannelEvent,
  _: AWS.Lambda.Context<void>,
  callback: AWS.Lambda.Callback<any>,
): Promise<void> {
  logger.info('Starting to process request:');
  const cookies = parseCookie(event.cookie);
  const locale = cookies.language || event.language || DEFAULT_LOCALE;
  logger.info({
    ...event,
    locale,
  });
  console.time(CONSOLE_LOG_FULL_RUN_TIMER);
  let renderPromise: Promise<Render> | undefined;
  const targetPath = `/${event.pathParts.join('/')}`;
  const experimentOverrides = parseExperimentOverrides(
    event.experiment_overrides,
  );
  logger.debug({ message: 'Experiment Overrides', experimentOverrides });
  const renderer = new Renderer({
    appTemplate: INDEX_TEMPLATE,
    chatEmbedTemplate: CHAT_EMBED_TEMPLATE,
    acceptLanguageHeader: locale,
    recordedPath: targetPath,
    bucketId: event.bucket_id,
    experimentOverrides,
  });
  // Right now API Gateway is URL encoding the URL encoded title. The result is that
  // "League of Legends" is "League%25%20%20of%25%20%20Legends".
  const gameAlias = decodeURIComponent(decodeURIComponent(event.game));
  const eventLocation = event.location;
  const channel = (event.channel || '').toLowerCase();
  const redirectManager = new LambdaRedirectManager(
    event.location,
    event.pathParts,
    cookies,
    event.redirectFromDesktop === 'true',
    event.tt_medium,
    event.tt_content,
  );
  const targetRoute = ROUTES.map(({ route, location }) => ({
    route: pathToRegexp(route),
    location,
  })).find(({ route }) => route.test(targetPath));
  const targetLocation = targetRoute ? targetRoute.location : '';
  const loggingCallback = getLoggingCallback(callback);
  const responseHandler = buildHandler(callback);

  if (redirectManager.redirect()) {
    loggingCallback(
      JSON.stringify({
        statusCode: StatusCode.FOUND,
        headers: {
          location: redirectManager.redirectTo(),
          cacheControl: buildCacheControlHeader(PageExpiry.DEFAULT_REDIRECT),
        },
      } as RedirectResponse),
      null,
    );
    return;
  }

  if (eventLocation === 'live_to_vod') {
    /* Success cannot be called through the common response handler because of
     * limitations with the APIG. Any metadata for these events must be handled
     * inside the APIG directly.
     */
    try {
      const redirectResponse = await redirectForLiveToVOD(
        event.channel_id,
        event.stream_id,
      );
      loggingCallback(JSON.stringify(redirectResponse), null);
    } catch (e) {
      logger.error(e);
      responseHandler(await renderer.renderNotFound());
    }
    return;
  }

  if (targetLocation) {
    renderPromise = renderer.render({
      location: targetLocation,
      theme: event.theme,
      fontSize: event.fontSize,
      eventID: event.event_id || event.eventID,
      vodID: event.vod_id || event.id || event.pathParts.slice(-1)[0],
      redirectedFrom: event.redirectFromDesktop,
      channel,
      gameAlias,
    } as RenderLocation);
  } else {
    renderPromise = renderer.renderNotFound();
  }

  try {
    responseHandler(await renderPromise);
  } catch (error) {
    if (error.name === CHANNEL_OFFLINE_ERROR_NAME) {
      const queryParams: {
        tt_content?: string;
        tt_medium?: string;
        'desktop-redirect': string;
      } = {
        'desktop-redirect': OFFLINE_CHANNEL_REDIRECT_VALUE,
      };

      if (event.tt_content) {
        queryParams.tt_content = event.tt_content;
      }

      if (event.tt_medium) {
        queryParams.tt_medium = event.tt_medium;
      }

      loggingCallback(
        JSON.stringify({
          statusCode: StatusCode.TEMPORARY_REDIRECT,
          headers: {
            location: url.format({
              pathname: `${buildChannelPath(channel)}/profile`,
              query: queryParams,
            }),
            cacheControl: buildCacheControlHeader(
              PageExpiry.CHANNEL_OFFLINE_TO_PROFILE,
            ),
          },
        } as RedirectResponse),
        null,
      );
      logger.info({
        message:
          'Rejected channel request due to offline, redirecting to profile page.',
        location: event.location,
        channel,
      });
    } else {
      logger.error({
        message: 'Unexpected error in rendering',
        error,
        event,
      });
      throw error;
    }
  }
}

function buildHandler(
  callback: AWS.Lambda.Callback<any>,
): (render: Render) => void {
  return function handler(render: Render): void {
    const loggingCallback = getLoggingCallback(callback, render.state);
    respondThroughCallback(loggingCallback, render);
    console.timeEnd(CONSOLE_LOG_FULL_RUN_TIMER);
  };
}
