import pify from 'pify'
import type { NodeMiddleware } from 'h3'
import { resolve } from 'pathe'
import { defineEventHandler, fromNodeMiddleware } from 'h3'
import type { MultiWatching } from 'webpack-dev-middleware'
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import type { Compiler, Stats, Watching } from 'webpack'
import { defu } from 'defu'
import type { NuxtBuilder } from '@nuxt/schema'
import { joinURL } from 'ufo'
import { logger, useNitro, useNuxt } from '@nuxt/kit'
import type { InputPluginOption } from 'rollup'

import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys'
import { DynamicBasePlugin } from './plugins/dynamic-base'
import { ChunkErrorPlugin } from './plugins/chunk'
import { createMFS } from './utils/mfs'
import { client, server } from './configs'
import { applyPresets, createWebpackConfigContext, getWebpackConfig } from './utils/config'
import { dynamicRequire } from './nitro/plugins/dynamic-require'

import { builder, webpack } from '#builder'

// TODO: Support plugins
// const plugins: string[] = []

export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
  const webpackConfigs = await Promise.all([client, ...nuxt.options.ssr ? [server] : []].map(async (preset) => {
    const ctx = createWebpackConfigContext(nuxt)
    ctx.userConfig = defu(nuxt.options.webpack[`$${preset.name as 'client' | 'server'}`], ctx.userConfig)
    await applyPresets(ctx, preset)
    return getWebpackConfig(ctx)
  }))

  /** Inject rollup plugin for Nitro to handle dynamic imports from webpack chunks */
  if (!nuxt.options.dev) {
    const nitro = useNitro()
    const dynamicRequirePlugin = dynamicRequire({
      dir: resolve(nuxt.options.buildDir, 'dist/server'),
      inline:
      nitro.options.node === false || nitro.options.inlineDynamicImports,
      ignore: [
        'client.manifest.mjs',
        'server.js',
        'server.cjs',
        'server.mjs',
        'server.manifest.mjs',
      ],
    })
    const prerenderRollupPlugins = nitro.options._config.rollupConfig!.plugins as InputPluginOption[]
    const rollupPlugins = nitro.options.rollupConfig!.plugins as InputPluginOption[]

    prerenderRollupPlugins.push(dynamicRequirePlugin)
    rollupPlugins.push(dynamicRequirePlugin)
  }

  await nuxt.callHook(`${builder}:config`, webpackConfigs)

  // Initialize shared MFS for dev
  const mfs = nuxt.options.dev ? createMFS() : null

  for (const config of webpackConfigs) {
    config.plugins!.push(DynamicBasePlugin.webpack({
      sourcemap: !!nuxt.options.sourcemap[config.name as 'client' | 'server'],
    }))
    // Emit chunk errors if the user has opted in to `experimental.emitRouteChunkError`
    if (config.name === 'client' && nuxt.options.experimental.emitRouteChunkError && nuxt.options.builder !== '@nuxt/rspack-builder') {
      config.plugins!.push(new ChunkErrorPlugin())
    }
    config.plugins!.push(composableKeysPlugin.webpack({
      sourcemap: !!nuxt.options.sourcemap[config.name as 'client' | 'server'],
      rootDir: nuxt.options.rootDir,
      composables: nuxt.options.optimization.keyedComposables,
    }))
  }

  await nuxt.callHook(`${builder}:configResolved`, webpackConfigs)

  // Configure compilers
  const compilers = webpackConfigs.map((config) => {
    // Create compiler
    const compiler = webpack(config)

    // In dev, write files in memory FS
    if (nuxt.options.dev) {
      compiler.outputFileSystem = mfs! as unknown as Compiler['outputFileSystem']
    }

    return compiler
  })

  nuxt.hook('close', async () => {
    for (const compiler of compilers) {
      await new Promise(resolve => compiler.close(resolve))
    }
  })

  // Start Builds
  if (nuxt.options.dev) {
    await Promise.all(compilers.map(c => compile(c)))
    return
  }

  for (const c of compilers) {
    await compile(c)
  }
}

async function createDevMiddleware (compiler: Compiler) {
  const nuxt = useNuxt()

  logger.debug('Creating webpack middleware...')

  // Create webpack dev middleware
  const devMiddleware = webpackDevMiddleware(compiler, {
    publicPath: joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir),
    outputFileSystem: compiler.outputFileSystem as any,
    stats: 'none',
    ...nuxt.options.webpack.devMiddleware,
  })

  // @ts-expect-error need better types for `pify`
  nuxt.hook('close', () => pify(devMiddleware.close.bind(devMiddleware))())

  const { client: _client, ...hotMiddlewareOptions } = nuxt.options.webpack.hotMiddleware || {}
  const hotMiddleware = webpackHotMiddleware(compiler, {
    log: false,
    heartbeat: 10000,
    path: joinURL(nuxt.options.app.baseURL, '__webpack_hmr', compiler.options.name!),
    ...hotMiddlewareOptions,
  })

  // Register devMiddleware on server
  const devHandler = fromNodeMiddleware(devMiddleware as NodeMiddleware)
  const hotHandler = fromNodeMiddleware(hotMiddleware as NodeMiddleware)
  await nuxt.callHook('server:devHandler', defineEventHandler(async (event) => {
    await devHandler(event)
    await hotHandler(event)
  }))

  return devMiddleware
}

async function compile (compiler: Compiler) {
  const nuxt = useNuxt()

  await nuxt.callHook(`${builder}:compile`, { name: compiler.options.name!, compiler })

  // Load renderer resources after build
  compiler.hooks.done.tap('load-resources', async (stats) => {
    await nuxt.callHook(`${builder}:compiled`, { name: compiler.options.name!, compiler, stats })
  })

  // --- Dev Build ---
  if (nuxt.options.dev) {
    const compilersWatching: Array<Watching | MultiWatching> = []

    nuxt.hook('close', async () => {
      await Promise.all(compilersWatching.map(watching => pify(watching.close.bind(watching))()))
    })

    // Client build
    if (compiler.options.name === 'client') {
      return new Promise((resolve, reject) => {
        compiler.hooks.done.tap('nuxt-dev', () => { resolve(null) })
        compiler.hooks.failed.tap('nuxt-errorlog', (err) => { reject(err) })
        // Start watch
        createDevMiddleware(compiler).then((devMiddleware) => {
          if (devMiddleware.context.watching) {
            compilersWatching.push(devMiddleware.context.watching)
          }
        })
      })
    }

    // Server, build and watch for changes
    return new Promise((resolve, reject) => {
      const watching = compiler.watch(nuxt.options.watchers.webpack, (err) => {
        if (err) { return reject(err) }
        resolve(null)
      })

      compilersWatching.push(watching)
    })
  }

  // --- Production Build ---
  const stats = await new Promise<Stats>((resolve, reject) => compiler.run((err, stats) => err ? reject(err) : resolve(stats!)))

  if (stats.hasErrors()) {
    const error = new Error('Nuxt build error')
    error.stack = stats.toString('errors-only')
    throw error
  }
}
