import { IncomingMessage, ServerResponse } from 'http'
import { parse as parseUrl, format as formatUrl, UrlWithParsedQuery } from 'url'
import { isResSent } from '../../../../next-server/lib/utils'
import { sendPayload } from '../../../../next-server/server/send-payload'
import { getUtils, vercelHeader, ServerlessHandlerCtx } from './utils'

import { renderToHTML } from '../../../../next-server/server/render'
import { tryGetPreviewData } from '../../../../next-server/server/api-utils'
import { denormalizePagePath } from '../../../../next-server/server/denormalize-page-path'
import {
  setLazyProp,
  getCookieParser,
} from '../../../../next-server/server/api-utils'
import { getRedirectStatus } from '../../../../lib/load-custom-routes'
import getRouteNoAssetPath from '../../../../next-server/lib/router/utils/get-route-from-asset-path'
import { PERMANENT_REDIRECT_STATUS } from '../../../../next-server/lib/constants'

export function getPageHandler(ctx: ServerlessHandlerCtx) {
  const {
    page,

    pageComponent,
    pageConfig,
    pageGetStaticProps,
    pageGetStaticPaths,
    pageGetServerSideProps,

    appModule,
    documentModule,
    errorModule,
    notFoundModule,

    encodedPreviewProps,
    pageIsDynamic,
    generateEtags,
    poweredByHeader,

    runtimeConfig,
    buildManifest,
    reactLoadableManifest,

    i18n,
    buildId,
    basePath,
    assetPrefix,
    canonicalBase,
    escapedBuildId,

    experimental: { initServer, onError },
  } = ctx
  const {
    handleLocale,
    handleRewrites,
    handleBasePath,
    defaultRouteRegex,
    dynamicRouteMatcher,
    interpolateDynamicPath,
    getParamsFromRouteMatches,
    normalizeDynamicRouteParams,
    normalizeVercelUrl,
  } = getUtils(ctx)

  async function renderReqToHTML(
    req: IncomingMessage,
    res: ServerResponse,
    renderMode?: 'export' | 'passthrough' | true,
    _renderOpts?: any,
    _params?: any
  ) {
    let Component
    let App
    let config
    let Document
    let Error
    let notFoundMod
    let getStaticProps
    let getStaticPaths
    let getServerSideProps
    ;[
      getStaticProps,
      getServerSideProps,
      getStaticPaths,
      Component,
      App,
      config,
      { default: Document },
      { default: Error },
      notFoundMod,
    ] = await Promise.all([
      pageGetStaticProps,
      pageGetServerSideProps,
      pageGetStaticPaths,
      pageComponent,
      appModule,
      pageConfig,
      documentModule,
      errorModule,
      notFoundModule,
    ])

    const fromExport = renderMode === 'export' || renderMode === true
    const nextStartMode = renderMode === 'passthrough'

    let hasValidParams = true

    setLazyProp({ req: req as any }, 'cookies', getCookieParser(req))

    const options = {
      App,
      Document,
      buildManifest,
      getStaticProps,
      getServerSideProps,
      getStaticPaths,
      reactLoadableManifest,
      canonicalBase,
      buildId,
      assetPrefix,
      runtimeConfig: (runtimeConfig || {}).publicRuntimeConfig || {},
      previewProps: encodedPreviewProps,
      env: process.env,
      basePath,
      ..._renderOpts,
    }
    let _nextData = false
    let defaultLocale = i18n?.defaultLocale
    let detectedLocale = i18n?.defaultLocale
    let parsedUrl: UrlWithParsedQuery

    try {
      // We need to trust the dynamic route params from the proxy
      // to ensure we are using the correct values
      const trustQuery = !getStaticProps && req.headers[vercelHeader]
      parsedUrl = parseUrl(req.url!, true)
      let routeNoAssetPath = parsedUrl.pathname!

      if (basePath) {
        routeNoAssetPath =
          routeNoAssetPath.replace(new RegExp(`^${basePath}`), '') || '/'
      }
      const origQuery = Object.assign({}, parsedUrl.query)

      parsedUrl = handleRewrites(parsedUrl)
      handleBasePath(req, parsedUrl)

      // remove ?amp=1 from request URL if rendering for export
      if (fromExport && parsedUrl.query.amp) {
        const queryNoAmp = Object.assign({}, origQuery)
        delete queryNoAmp.amp

        req.url = formatUrl({
          ...parsedUrl,
          search: undefined,
          query: queryNoAmp,
        })
      }

      if (parsedUrl.pathname!.match(/_next\/data/)) {
        _nextData = page !== '/_error'
        parsedUrl.pathname = getRouteNoAssetPath(
          parsedUrl.pathname!.replace(
            new RegExp(`/_next/data/${escapedBuildId}/`),
            '/'
          ),
          '.json'
        )
        routeNoAssetPath = parsedUrl.pathname
      }

      const localeResult = handleLocale(
        req,
        res,
        parsedUrl,
        routeNoAssetPath,
        fromExport || nextStartMode
      )
      defaultLocale = localeResult?.defaultLocale || defaultLocale
      detectedLocale = localeResult?.detectedLocale || detectedLocale
      routeNoAssetPath = localeResult?.routeNoAssetPath || routeNoAssetPath

      if (parsedUrl.query.nextInternalLocale) {
        detectedLocale = parsedUrl.query.nextInternalLocale as string
        delete parsedUrl.query.nextInternalLocale
      }

      const renderOpts = Object.assign(
        {
          Component,
          pageConfig: config,
          nextExport: fromExport,
          isDataReq: _nextData,
          locales: i18n?.locales,
          locale: detectedLocale,
          defaultLocale,
          domainLocales: i18n?.domains,
        },
        options
      )

      if (page === '/_error' && !res.statusCode) {
        res.statusCode = 404
      }

      let params = {}

      if (!fromExport && pageIsDynamic) {
        const result = normalizeDynamicRouteParams(
          trustQuery
            ? parsedUrl.query
            : (dynamicRouteMatcher!(parsedUrl.pathname) as Record<
                string,
                string | string[]
              >)
        )

        hasValidParams = result.hasValidParams
        params = result.params
      }

      let nowParams = null

      if (
        pageIsDynamic &&
        !hasValidParams &&
        req.headers?.['x-now-route-matches']
      ) {
        nowParams = getParamsFromRouteMatches(req, renderOpts, detectedLocale)
      }

      // make sure to set renderOpts to the correct params e.g. _params
      // if provided from worker or params if we're parsing them here
      renderOpts.params = _params || params

      normalizeVercelUrl(req, !!trustQuery)

      // normalize request URL/asPath for fallback/revalidate pages since the
      // proxy sets the request URL to the output's path for fallback pages
      if (pageIsDynamic && nowParams && defaultRouteRegex) {
        const _parsedUrl = parseUrl(req.url!)

        _parsedUrl.pathname = interpolateDynamicPath(
          _parsedUrl.pathname!,
          nowParams
        )
        parsedUrl.pathname = _parsedUrl.pathname
        req.url = formatUrl(_parsedUrl)
      }

      // make sure to normalize asPath for revalidate and _next/data requests
      // since the asPath should match what is shown on the client
      if (!fromExport && (getStaticProps || getServerSideProps)) {
        // don't include dynamic route params in query while normalizing
        // asPath
        if (pageIsDynamic && trustQuery && defaultRouteRegex) {
          delete (parsedUrl as any).search

          for (const param of Object.keys(defaultRouteRegex.groups)) {
            delete origQuery[param]
          }
        }

        parsedUrl.pathname = denormalizePagePath(parsedUrl.pathname!)
        renderOpts.resolvedUrl = formatUrl({
          ...parsedUrl,
          query: origQuery,
        })

        // For getServerSideProps we need to ensure we use the original URL
        // and not the resolved URL to prevent a hydration mismatch on asPath
        renderOpts.resolvedAsPath = getServerSideProps
          ? formatUrl({
              ...parsedUrl,
              pathname: routeNoAssetPath,
              query: origQuery,
            })
          : renderOpts.resolvedUrl
      }

      const isFallback = parsedUrl.query.__nextFallback

      const previewData = tryGetPreviewData(req, res, options.previewProps)
      const isPreviewMode = previewData !== false

      if (process.env.__NEXT_OPTIMIZE_FONTS) {
        renderOpts.optimizeFonts = true
        /**
         * __webpack_require__.__NEXT_FONT_MANIFEST__ is added by
         * font-stylesheet-gathering-plugin
         */
        // @ts-ignore
        renderOpts.fontManifest = __webpack_require__.__NEXT_FONT_MANIFEST__
      }

      let result = await renderToHTML(
        req,
        res,
        page,
        Object.assign(
          {},
          getStaticProps
            ? { ...(parsedUrl.query.amp ? { amp: '1' } : {}) }
            : parsedUrl.query,
          nowParams ? nowParams : params,
          _params,
          isFallback ? { __nextFallback: 'true' } : {}
        ),
        renderOpts
      )

      if (!renderMode) {
        if (_nextData || getStaticProps || getServerSideProps) {
          if (renderOpts.isNotFound) {
            res.statusCode = 404

            if (_nextData) {
              res.end('{"notFound":true}')
              return null
            }

            const NotFoundComponent = notFoundMod ? notFoundMod.default : Error
            const errPathname = notFoundMod ? '/404' : '/_error'

            const result2 = await renderToHTML(
              req,
              res,
              errPathname,
              parsedUrl.query,
              Object.assign({}, options, {
                getStaticProps: notFoundMod
                  ? notFoundMod.getStaticProps
                  : undefined,
                getStaticPaths: undefined,
                getServerSideProps: undefined,
                Component: NotFoundComponent,
                err: undefined,
                locale: detectedLocale,
                locales: i18n?.locales,
                defaultLocale: i18n?.defaultLocale,
              })
            )

            sendPayload(
              req,
              res,
              result2,
              'html',
              {
                generateEtags,
                poweredByHeader,
              },
              {
                private: isPreviewMode,
                stateful: !!getServerSideProps,
                revalidate: renderOpts.revalidate,
              }
            )
            return null
          } else if (renderOpts.isRedirect && !_nextData) {
            const redirect = {
              destination: renderOpts.pageData.pageProps.__N_REDIRECT,
              statusCode: renderOpts.pageData.pageProps.__N_REDIRECT_STATUS,
              basePath: renderOpts.pageData.pageProps.__N_REDIRECT_BASE_PATH,
            }
            const statusCode = getRedirectStatus(redirect)

            if (basePath && redirect.basePath !== false) {
              redirect.destination = `${basePath}${redirect.destination}`
            }

            if (statusCode === PERMANENT_REDIRECT_STATUS) {
              res.setHeader('Refresh', `0;url=${redirect.destination}`)
            }

            res.statusCode = statusCode
            res.setHeader('Location', redirect.destination)
            res.end()
            return null
          } else {
            sendPayload(
              req,
              res,
              _nextData ? JSON.stringify(renderOpts.pageData) : result,
              _nextData ? 'json' : 'html',
              {
                generateEtags,
                poweredByHeader,
              },
              {
                private: isPreviewMode,
                stateful: !!getServerSideProps,
                revalidate: renderOpts.revalidate,
              }
            )
            return null
          }
        }
      } else if (isPreviewMode) {
        res.setHeader(
          'Cache-Control',
          'private, no-cache, no-store, max-age=0, must-revalidate'
        )
      }

      if (renderMode) return { html: result, renderOpts }
      return result
    } catch (err) {
      if (!parsedUrl!) {
        parsedUrl = parseUrl(req.url!, true)
      }

      if (err.code === 'ENOENT') {
        res.statusCode = 404
      } else if (err.code === 'DECODE_FAILED') {
        // TODO: better error?
        res.statusCode = 400
      } else {
        console.error('Unhandled error during request:', err)

        // Backwards compat (call getInitialProps in custom error):
        try {
          await renderToHTML(
            req,
            res,
            '/_error',
            parsedUrl!.query,
            Object.assign({}, options, {
              getStaticProps: undefined,
              getStaticPaths: undefined,
              getServerSideProps: undefined,
              Component: Error,
              err: err,
              // Short-circuit rendering:
              isDataReq: true,
            })
          )
        } catch (underErrorErr) {
          console.error(
            'Failed call /_error subroutine, continuing to crash function:',
            underErrorErr
          )
        }

        // Throw the error to crash the serverless function
        if (isResSent(res)) {
          console.error('!!! WARNING !!!')
          console.error(
            'Your function crashed, but closed the response before allowing the function to exit.\\n' +
              'This may cause unexpected behavior for the next request.'
          )
          console.error('!!! WARNING !!!')
        }
        throw err
      }

      const result2 = await renderToHTML(
        req,
        res,
        '/_error',
        parsedUrl!.query,
        Object.assign({}, options, {
          getStaticProps: undefined,
          getStaticPaths: undefined,
          getServerSideProps: undefined,
          Component: Error,
          err: res.statusCode === 404 ? undefined : err,
        })
      )
      return result2
    }
  }

  return {
    renderReqToHTML,
    render: async function render(req: IncomingMessage, res: ServerResponse) {
      try {
        await initServer()
        const html = await renderReqToHTML(req, res)
        if (html) {
          sendPayload(req, res, html, 'html', {
            generateEtags,
            poweredByHeader,
          })
        }
      } catch (err) {
        console.error(err)
        await onError(err)
        // Throw the error to crash the serverless function
        throw err
      }
    },
  }
}
