import { useMemo } from 'react';
import NextError from 'next/error';
import Debug from 'debug';

import type { AppError, SerializedAppError } from '~/lib/errors/types';
import type { HttpMethods } from '~/lib/songwhipApi/types';
import type { NextPageContext } from 'next';

import ApiError from '~/lib/errors/ApiError';
import { resolveError, serializeError } from '~/lib/errors/utils';
import Box from '~/src/components/Box';
import Button from '~/src/components/Button';
import ErrorText from '~/src/components/ErrorText';
import LogoSongwhip from '~/src/components/Logos/LogoSongwhip';
import Page from '~/src/components/Page';
import PageMetadata from '~/src/components/PageMetadata';
import { reportError } from '~/src/lib/sentry';
import { reportErrorClient } from '~/src/lib/sentry/client';

const debug = Debug('songwhip/NextErrorPage');

/**
 * The way errors are caught and rendered are complex,
 * this module is based on:
 * https://github.com/zeit/next.js/blob/canary/examples/with-sentry-simple/pages/_error.js
 */
const NextErrorPage = ({
  serializedError,
  hasReportedError,

  // this param is injected via <App>
  err: error,
}: {
  serializedError: SerializedAppError;
  hasReportedError: boolean;
  err: Error | ApiError | undefined;
}) => {
  debug('render');

  // eslint-disable-next-line no-console
  console.log('_error', {
    serializedError,
    hasReportedError,
    error,
  });

  useMemo(() => {
    // only report the error if wasn't already reported in getInitialProps
    if (hasReportedError) return;

    if (error) {
      reportErrorClient({ error });
    }
  }, []);

  // 'resolved' errors have a `type` which means we can render a predefined message
  const appError = resolveError(serializedError);

  return (
    <Page
      testId="errorPage"
      renderHeaderContent={() => <LogoSongwhip size="2.8rem" />}
    >
      <PageMetadata title="Error" noIndex />
      <Box padding="0 2rem 3.2rem" centerContent flexColumn coverParent>
        <ErrorText
          testId="errorText"
          centered
          isBold
          size="4.6rem"
          lineHeight="1.2em"
          error={appError}
          hyphenate={false}
        />
        <Button
          margin="3rem 0 0"
          isCentered
          href="/contact"
          text="Chat to us for help"
          height="6.6rem"
        />
      </Box>
    </Page>
  );
};

interface ErrorPageContext extends NextPageContext {
  err?: AppError;
}

/**
 * Running on the server, the response object (`res`) is available.
 * Next.js will pass an err on the server if a page's data fetching methods
 * threw or returned a Promise that rejected
 *
 * Running on the client (browser), Next.js will provide an err if:
 *
 * - A. Page threw or return rejected Promise in `getInitialProps`
 * - B. An exception was thrown somewhere in the React lifecycle (render,
 *   componentDidMount, etc) that was caught by Next's React Error
 *   Boundary. Read more about what types of exceptions are caught by Error
 *   Boundaries: https://reactjs.org/docs/error-boundaries.html
 */
NextErrorPage.getInitialProps = async (ctx: ErrorPageContext) => {
  const { err } = ctx;

  const initialProps = await NextError.getInitialProps(ctx);
  let hasReportedError = false;

  debug('get initial props', err);

  // If there's an `err` object, then it means this page was
  // rendered by Next and not from the withErrorHandler() HOC.
  // In that case we need to report the error to sentry as
  // this is normally done within withErrorHandler().
  // skip 404s (too noisy)
  if (err && err['status'] !== 404) {
    await reportError({
      error: err,
      nextCtx: ctx,

      extras: {
        caughtAt: 'NextErrorPage.getInitialProps()',
      },
    });

    hasReportedError = true;
  }

  return {
    ...initialProps,

    // error passing between server/client boundary so must be plain object
    serializedError: serializeError(toError(ctx)),

    hasReportedError,
  };
};

const toError = ({ req, res, err }: NextPageContext) => {
  if (err) return err;

  if (res?.statusCode) {
    return new ApiError({
      status: res.statusCode,
      method: req!.method! as HttpMethods,
      url: req!.url!,
    });
  }

  // this line shouldn't really get hit
  return new Error('unknown error caught at pages/_error');
};

export default NextErrorPage;
