import { mkdir, readFile, writeFile } from 'node:fs/promises'
import type { Buffer } from 'node:buffer'
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'
import type { VitePluginPWAAPI } from 'vite-plugin-pwa'
import { VitePWA } from 'vite-plugin-pwa'
import type { Plugin } from 'vite'
import { join, resolve } from 'pathe'
import type { VitePWANuxtOptions } from './types'
import { configurePWAOptions } from './config'
import { type LocalizedWebManifest, createI18n, pwaLocales } from './i18n'

export * from './types'

interface PwaDevIcon {
  src: string
  type: string
}

interface ResolvedPwaDevIcon extends PwaDevIcon {
  data: Promise<Buffer>
}

export default defineNuxtModule<VitePWANuxtOptions>({
  meta: {
    name: 'elk-pwa',
    configKey: 'pwa',
  },
  defaults: nuxt => ({
    base: nuxt.options.app.baseURL,
    scope: nuxt.options.app.baseURL,
  }),
  async setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)

    let vitePwaClientPlugin: Plugin | undefined
    const resolveVitePluginPWAAPI = (): VitePluginPWAAPI | undefined => {
      return vitePwaClientPlugin?.api
    }
    let webmanifests: LocalizedWebManifest | undefined

    nuxt.options.appConfig = nuxt.options.appConfig || {}
    nuxt.options.appConfig.pwaEnabled = !options.disable

    nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || []
    const manifestDir = join(nuxt.options.buildDir, 'manifests')
    nuxt.options.nitro.publicAssets.push({
      dir: manifestDir,
      baseURL: '/',
      maxAge: 0,
    })
    if (options.disable) {
      addPlugin({ src: resolver.resolve('./runtime/pwa-plugin-stub.client') })
    }
    else {
      // Register PWA types
      nuxt.hook('prepare:types', ({ references }) => {
        references.push({ types: 'vite-plugin-pwa/info' })
        references.push({ types: 'vite-plugin-pwa/client' })
      })
      // Inject $pwa helper throughout app
      addPlugin({ src: resolver.resolve('./runtime/pwa-plugin.client') })
    }

    // TODO: combine with configurePWAOptions?
    nuxt.hook('nitro:init', (nitro) => {
      options.outDir = nitro.options.output.publicDir
      options.injectManifest = options.injectManifest || {}
      options.injectManifest.globDirectory = nitro.options.output.publicDir
    })
    nuxt.hook('vite:extend', ({ config }) => {
      const plugin = config.plugins?.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa')
      if (plugin)
        throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')
    })
    nuxt.hook('vite:extendConfig', async (viteInlineConfig, { isClient }) => {
      viteInlineConfig.plugins = viteInlineConfig.plugins || []
      const plugin = viteInlineConfig.plugins.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa')
      if (plugin)
        throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')

      webmanifests = await createI18n()
      const generateManifest = (entry: string) => {
        const manifest = webmanifests![entry]
        if (!manifest)
          throw new Error(`No webmanifest found for locale/theme ${entry}`)
        return JSON.stringify(manifest)
      }
      if (isClient) {
        viteInlineConfig.plugins.push({
          name: 'elk:pwa:locales:build',
          apply: 'build',
          async writeBundle(_options, bundle) {
            if (options.disable || !bundle)
              return
            await mkdir(manifestDir, { recursive: true })
            for (const wm in webmanifests)
              await writeFile(join(manifestDir, `manifest-${wm}.webmanifest`), generateManifest(wm))
          },
        })
      }
      viteInlineConfig.plugins.push({
        name: 'elk:pwa:dev',
        apply: 'serve',
        configureServer(server) {
          const icons: PwaDevIcon[] = webmanifests?.['en-US']?.icons as any
          const mappedIcons: PwaDevIcon[] = [
            ...icons.map(({ src, type }) => ({ src, type })),
            { src: 'favicon.ico', type: 'image/x-icon' },
            { src: 'apple-touch-icon.png', type: 'image/png' },
            { src: 'logo.svg', type: 'image/svg+xml' },
          ]

          const folder = dirname(fileURLToPath(import.meta.url))
          const useIcons = mappedIcons.reduce((acc, icon) => {
            icon.src = `${nuxt.options.app.baseURL}${icon.src}`
            acc[icon.src] = {
              ...icon,
              data: readFile(resolve(join(folder, '../../public-dev', icon.src))),
            }
            return acc
          }, <Record<string, ResolvedPwaDevIcon>>{})
          const localeMatcher = new RegExp(`^${nuxt.options.app.baseURL}manifest-(.*).webmanifest$`)
          server.middlewares.use(async (req, res, next) => {
            const url = req.url
            if (!url)
              return next()

            const match = url.match(localeMatcher)
            const entry = match && webmanifests![match[1]]
            if (entry) {
              res.statusCode = 200
              res.setHeader('Content-Type', 'application/manifest+json')
              res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate')
              res.write(JSON.stringify(entry), 'utf-8')
              res.end()
              return
            }

            const icon = useIcons[url]
            if (!icon)
              return next()

            res.statusCode = 200
            res.setHeader('Content-Type', icon.type)
            res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate')
            res.write(await icon.data)
            res.end()
          })
        },
      })

      configurePWAOptions(options, nuxt)
      const plugins = VitePWA(options)
      viteInlineConfig.plugins.push(plugins)
      if (isClient)
        vitePwaClientPlugin = plugins.find(p => p.name === 'vite-plugin-pwa') as Plugin
    })

    if (nuxt.options.dev) {
      const webManifest = `${nuxt.options.app.baseURL}${options.devOptions?.webManifestUrl ?? options.manifestFilename ?? 'manifest.webmanifest'}`
      const devSw = `${nuxt.options.app.baseURL}dev-sw.js?dev-sw`
      const workbox = `${nuxt.options.app.baseURL}workbox-`
      // @ts-expect-error just ignore
      const emptyHandle = (_req, _res, next) => {
        next()
      }
      nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
        if (isServer)
          return

        viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle })
        if (webmanifests) {
          Object.keys(webmanifests).forEach((wm) => {
            viteServer.middlewares.stack.push({
              route: `${nuxt.options.app.baseURL}manifest-${wm}.webmanifest`,
              handle: emptyHandle,
            })
          })
        }
        viteServer.middlewares.stack.push({ route: devSw, handle: emptyHandle })
      })

      if (!options.strategies || options.strategies === 'generateSW') {
        nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
          if (isServer)
            return

          viteServer.middlewares.stack.push({ route: workbox, handle: emptyHandle })
        })
        nuxt.hook('close', async () => {
          // todo: cleanup dev-dist folder
        })
      }
    }
    else {
      nuxt.hook('nitro:config', async (nitroConfig) => {
        nitroConfig.routeRules = nitroConfig.routeRules || {}
        nitroConfig.routeRules!['/sw.js'] = {
          headers: {
            'Cache-Control': 'public, max-age=0, must-revalidate',
          },
        }
        for (const locale of pwaLocales) {
          nitroConfig.routeRules![`/manifest-${locale.code}.webmanifest`] = {
            headers: {
              'Content-Type': 'application/manifest+json',
              'Cache-Control': 'public, max-age=0, must-revalidate',
            },
          }
          nitroConfig.routeRules![`/manifest-${locale.code}-dark.webmanifest`] = {
            headers: {
              'Content-Type': 'application/manifest+json',
              'Cache-Control': 'public, max-age=0, must-revalidate',
            },
          }
        }
      })
      nuxt.hook('nitro:init', (nitro) => {
        nitro.hooks.hook('rollup:before', async () => {
          await resolveVitePluginPWAAPI()?.generateSW()
        })
      })
    }
  },
})
