import { HandlerContext, HandlerEvent } from '@netlify/functions'
import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge'
// Aliasing like this means the editor may be able to syntax-highlight the string
import { outdent as javascript } from 'outdent'

import type { NextConfig } from '../helpers/config'

import type { NextServerType } from './handlerUtils'

/* eslint-disable @typescript-eslint/no-var-requires */

const { promises } = require('fs')
const { Server } = require('http')
const path = require('path')
// eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params
const { URLSearchParams, URL } = require('url')

const { Bridge } = require('@vercel/node-bridge/bridge')

const {
  augmentFsModule,
  getMaxAge,
  getMultiValueHeaders,
  getPrefetchResponse,
  getNextServer,
} = require('./handlerUtils')
/* eslint-enable @typescript-eslint/no-var-requires */

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}

// We return a function and then call `toString()` on it to serialise it as the launcher function
// eslint-disable-next-line max-params
const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => {
  // Change working directory into the site root, unless using Nx, which moves the
  // dist directory and handles this itself
  const dir = path.resolve(__dirname, app)
  if (pageRoot.startsWith(dir)) {
    process.chdir(dir)
  }

  // This is just so nft knows about the page entrypoints. It's not actually used
  try {
    // eslint-disable-next-line n/no-missing-require
    require.resolve('./pages.js')
  } catch {}

  const ONE_YEAR_IN_SECONDS = 31536000

  // React assumes you want development mode if NODE_ENV is unset.
  ;(process.env as Mutable<NodeJS.ProcessEnv>).NODE_ENV ||= 'production'

  // We don't want to write ISR files to disk in the lambda environment
  conf.experimental.isrFlushToDisk = false
  // This is our flag that we use when patching the source
  // eslint-disable-next-line no-underscore-dangle
  process.env._REVALIDATE_SSG = 'true'
  for (const [key, value] of Object.entries(conf.env)) {
    process.env[key] = String(value)
  }
  // Set during the request as it needs to get it from the request URL. Defaults to the URL env var
  let base = process.env.URL

  augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base })

  // We memoize this because it can be shared between requests, but don't instantiate it until
  // the first request because we need the host and port.
  let bridge: NodeBridge
  const getBridge = (event: HandlerEvent): NodeBridge => {
    if (bridge) {
      return bridge
    }
    const url = new URL(event.rawUrl)
    const port = Number.parseInt(url.port) || 80
    base = url.origin

    const NextServer: NextServerType = getNextServer()
    const nextServer = new NextServer({
      conf,
      dir,
      customServer: false,
      hostname: url.hostname,
      port,
    })
    const requestHandler = nextServer.getRequestHandler()
    const server = new Server(async (req, res) => {
      try {
        await requestHandler(req, res)
      } catch (error) {
        console.error(error)
        throw new Error('Error handling request. See function logs for details.')
      }
    })
    bridge = new Bridge(server)
    bridge.listen()
    return bridge
  }

  return async function handler(event: HandlerEvent, context: HandlerContext) {
    let requestMode = mode
    const prefetchResponse = getPrefetchResponse(event, mode)
    if (prefetchResponse) {
      return prefetchResponse
    }
    // Ensure that paths are encoded - but don't double-encode them
    event.path = new URL(event.rawUrl).pathname
    // Next expects to be able to parse the query from the URL
    const query = new URLSearchParams(event.queryStringParameters).toString()
    event.path = query ? `${event.path}?${query}` : event.path

    const graphToken = event.netlifyGraphToken
    if (graphToken && requestMode !== 'ssr') {
      // Prefix with underscore to help us determine the origin of the token
      // allows us to write better error messages
      // eslint-disable-next-line no-underscore-dangle
      process.env._NETLIFY_GRAPH_TOKEN = graphToken
    }

    const { headers, ...result } = await getBridge(event).launcher(event, context)

    // Convert all headers to multiValueHeaders

    const multiValueHeaders = getMultiValueHeaders(headers)

    if (event.headers['x-next-debug-logging']) {
      const response = {
        headers: multiValueHeaders,
        statusCode: result.statusCode,
      }
      console.log('Origin response:', JSON.stringify(response, null, 2))
    }

    if (multiValueHeaders['set-cookie']?.[0]?.includes('__prerender_bypass')) {
      delete multiValueHeaders.etag
      multiValueHeaders['cache-control'] = ['no-cache']
    }

    // Sending SWR headers causes undefined behaviour with the Netlify CDN
    const cacheHeader = multiValueHeaders['cache-control']?.[0]

    if (cacheHeader?.includes('stale-while-revalidate')) {
      if (requestMode === 'odb') {
        const ttl = getMaxAge(cacheHeader)
        // Long-expiry TTL is basically no TTL, so we'll skip it
        if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) {
          // ODBs currently have a minimum TTL of 60 seconds
          result.ttl = Math.max(ttl, 60)
        }
        const ephemeralCodes = [301, 302, 307, 308, 404]
        if (ttl === ONE_YEAR_IN_SECONDS && ephemeralCodes.includes(result.statusCode)) {
          // Only cache for 60s if default TTL provided
          result.ttl = 60
        }
        if (result.ttl > 0) {
          requestMode = `odb ttl=${result.ttl}`
        }
      }
      multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate']
    }
    multiValueHeaders['x-nf-render-mode'] = [requestMode]

    console.log(`[${event.httpMethod}] ${event.path} (${requestMode?.toUpperCase()})`)

    return {
      ...result,
      multiValueHeaders,
      isBase64Encoded: result.encoding === 'base64',
    }
  }
}

export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }): string =>
  // This is a string, but if you have the right editor plugin it should format as js
  javascript/* javascript */ `
  const { Server } = require("http");
  const { promises } = require("fs");
  // We copy the file here rather than requiring from the node module
  const { Bridge } = require("./bridge");
  const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer } = require('./handlerUtils')

  ${isODB ? `const { builder } = require("@netlify/functions")` : ''}
  const { config }  = require("${publishDir}/required-server-files.json")
  let staticManifest
  try {
    staticManifest = require("${publishDir}/static-manifest.json")
  } catch {}
  const path = require("path");
  const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server"));
  exports.handler = ${
    isODB
      ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));`
      : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`
  }
`
