import type { NetlifyConfig } from '@netlify/build'
import destr from 'destr'
import { readJSON, writeJSON } from 'fs-extra'
import type { Header } from 'next/dist/lib/load-custom-routes'
import type { NextConfigComplete } from 'next/dist/server/config-shared'
import { join, dirname, relative } from 'pathe'
import slash from 'slash'

import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'

import type { RoutesManifest } from './types'

const ROUTES_MANIFEST_FILE = 'routes-manifest.json'

type NetlifyHeaders = NetlifyConfig['headers']

export interface RequiredServerFiles {
  version?: number
  config?: NextConfigComplete
  appDir?: string
  files?: string[]
  ignore?: string[]
}

export type NextConfig = Pick<RequiredServerFiles, 'appDir' | 'ignore'> &
  NextConfigComplete & {
    routesManifest?: RoutesManifest
  }

const defaultFailBuild = (message: string, { error }): never => {
  throw new Error(`${message}\n${error && error.stack}`)
}

export const getNextConfig = async function getNextConfig({
  publish,
  failBuild = defaultFailBuild,
}): Promise<NextConfig> {
  try {
    const { config, appDir, ignore }: RequiredServerFiles = await readJSON(join(publish, 'required-server-files.json'))
    if (!config) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      return failBuild('Error loading your Next config')
    }

    const routesManifest: RoutesManifest = await readJSON(join(publish, ROUTES_MANIFEST_FILE))

    // If you need access to other manifest files, you can add them here as well
    return { ...config, appDir, ignore, routesManifest }
  } catch (error: unknown) {
    return failBuild('Error loading your Next config', { error })
  }
}

/**
 * Returns all of the NextJS configuration stored within 'required-server-files.json'
 * To update the configuration within this file, use the 'updateRequiredServerFiles' method.
 */
export const getRequiredServerFiles = async (publish: string): Promise<RequiredServerFiles> => {
  const configFile = join(publish, 'required-server-files.json')
  return await readJSON(configFile)
}

/**
 * Writes a modified configuration object to 'required-server-files.json'.
 * To get the full configuration, use the 'getRequiredServerFiles' method.
 */
export const updateRequiredServerFiles = async (publish: string, modifiedConfig: RequiredServerFiles) => {
  const configFile = join(publish, 'required-server-files.json')
  await writeJSON(configFile, modifiedConfig)
}

const resolveModuleRoot = (moduleName) => {
  try {
    return dirname(relative(process.cwd(), require.resolve(`${moduleName}/package.json`, { paths: [process.cwd()] })))
  } catch {
    return null
  }
}

const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron']

export const hasManuallyAddedModule = ({
  netlifyConfig,
  moduleName,
}: {
  netlifyConfig: NetlifyConfig
  moduleName: string
}) =>
  /* eslint-disable camelcase */
  Object.values(netlifyConfig.functions).some(({ included_files = [] }) =>
    included_files.some((inc) => inc.includes(`node_modules/${moduleName}`)),
  )
/* eslint-enable camelcase */

export const configureHandlerFunctions = async ({
  netlifyConfig,
  publish,
  ignore = [],
}: {
  netlifyConfig: NetlifyConfig
  publish: string
  ignore: Array<string>
}) => {
  const config = await getRequiredServerFiles(publish)
  const files = config.files || []
  const cssFilesToInclude = files.filter((f) => f.startsWith(`${publish}/static/css/`))

  /* eslint-disable no-underscore-dangle */
  if (!destr(process.env.DISABLE_IPX)) {
    netlifyConfig.functions._ipx ||= {}
    netlifyConfig.functions._ipx.node_bundler = 'nft'
  }

  // If the user has manually added the module to included_files, then don't exclude it
  const excludedModules = DEFAULT_EXCLUDED_MODULES.filter(
    (moduleName) => !hasManuallyAddedModule({ netlifyConfig, moduleName }),
  )

  /* eslint-enable no-underscore-dangle */
  ;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, '_api_*'].forEach((functionName) => {
    netlifyConfig.functions[functionName] ||= { included_files: [], external_node_modules: [] }
    netlifyConfig.functions[functionName].node_bundler = 'nft'
    netlifyConfig.functions[functionName].included_files ||= []
    netlifyConfig.functions[functionName].included_files.push(
      '.env',
      '.env.local',
      '.env.production',
      '.env.production.local',
      './public/locales/**',
      './next-i18next.config.js',
      `${publish}/server/**`,
      `${publish}/serverless/**`,
      `${publish}/*.json`,
      `${publish}/BUILD_ID`,
      `${publish}/static/chunks/webpack-middleware*.js`,
      `!${publish}/server/**/*.js.nft.json`,
      `!${publish}/server/**/*.map`,
      '!**/node_modules/@next/swc*/**/*',
      ...cssFilesToInclude,
      ...ignore.map((path) => `!${slash(path)}`),
    )

    const nextRoot = resolveModuleRoot('next')
    if (nextRoot) {
      netlifyConfig.functions[functionName].included_files.push(
        `!${nextRoot}/dist/server/lib/squoosh/**/*.wasm`,
        `!${nextRoot}/dist/next-server/server/lib/squoosh/**/*.wasm`,
        `!${nextRoot}/dist/compiled/webpack/bundle4.js`,
        `!${nextRoot}/dist/compiled/webpack/bundle5.js`,
      )
    }

    excludedModules.forEach((moduleName) => {
      const moduleRoot = resolveModuleRoot(moduleName)
      if (moduleRoot) {
        netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`)
      }
    })
  })
}

interface BuildHeaderParams {
  path: string
  headers: Header['headers']
  locale?: string
}

const buildHeader = (buildHeaderParams: BuildHeaderParams) => {
  const { path, headers } = buildHeaderParams

  return {
    for: path,
    values: headers.reduce((builtHeaders, { key, value }) => {
      builtHeaders[key] = value

      return builtHeaders
    }, {}),
  }
}

// Replace the pattern :path* at the end of a path with * since it's a named splat which the Netlify
// configuration does not support.
const sanitizePath = (path: string) => path.replace(/:[^*/]+\*$/, '*')

/**
 * Persist Next.js custom headers to the Netlify configuration so the headers work with static files
 * See {@link https://nextjs.org/docs/api-reference/next.config.js/headers} for more information on custom
 * headers in Next.js
 *
 * @param nextConfig - The Next.js configuration
 * @param netlifyHeaders - Existing headers that are already configured in the Netlify configuration
 */
export const generateCustomHeaders = (nextConfig: NextConfig, netlifyHeaders: NetlifyHeaders = []) => {
  // The routesManifest is the contents of the routes-manifest.json file which will already contain the generated
  // header paths which take locales and base path into account since this runs after the build. The routes-manifest.json
  // file is located at demos/default/.next/routes-manifest.json once you've build the demo site.
  const {
    routesManifest: { headers: customHeaders = [] },
    i18n,
  } = nextConfig

  // Skip `has` based custom headers as they have more complex dynamic conditional header logic
  // that currently isn't supported by the Netlify configuration.
  // Also, this type of dynamic header logic is most likely not for SSG pages.
  for (const { source, headers, locale: localeEnabled } of customHeaders.filter((customHeader) => !customHeader.has)) {
    // Explicitly checking false to make the check simpler.
    // Locale specific paths are excluded only if localeEnabled is false. There is no true value for localeEnabled. It's either
    // false or undefined, where undefined means it's true.
    //
    // Again, the routesManifest has already been generated taking locales into account, but the check is required
    // so  the paths can be properly set in the Netlify configuration.
    const useLocale = i18n?.locales?.length > 0 && localeEnabled !== false

    if (useLocale) {
      const { locales } = i18n
      const joinedLocales = locales.join('|')

      /**
       *  converts e.g.
       *  /:nextInternalLocale(en|fr)/some-path
       *  to a path for each locale
       *  /en/some-path and /fr/some-path as well as /some-path (default locale)
       */
      const defaultLocalePath = sanitizePath(source).replace(`/:nextInternalLocale(${joinedLocales})`, '')

      netlifyHeaders.push(buildHeader({ path: defaultLocalePath, headers }))

      for (const locale of locales) {
        const path = sanitizePath(source).replace(`:nextInternalLocale(${joinedLocales})`, locale)

        netlifyHeaders.push(buildHeader({ path, headers }))
      }
    } else {
      const path = sanitizePath(source)

      netlifyHeaders.push(buildHeader({ path, headers }))
    }
  }
}
