import { isAbsolute, relative } from 'pathe'
import { genDynamicImport } from 'knitwork'
import type { Component, Nuxt, NuxtApp, NuxtPluginTemplate, NuxtTemplate } from 'nuxt/schema'

interface ComponentsTemplateContext {
  app: NuxtApp
  nuxt: Nuxt
  options: {
    getComponents: (mode?: 'client' | 'server' | 'all') => Component[]
    mode?: 'client' | 'server'
  }
}

type ImportMagicCommentsOptions = {
  chunkName: string
  prefetch?: boolean | number
  preload?: boolean | number
}

const createImportMagicComments = (options: ImportMagicCommentsOptions) => {
  const { chunkName, prefetch, preload } = options
  return [
    `webpackChunkName: "${chunkName}"`,
    prefetch === true || typeof prefetch === 'number' ? `webpackPrefetch: ${prefetch}` : false,
    preload === true || typeof preload === 'number' ? `webpackPreload: ${preload}` : false
  ].filter(Boolean).join(', ')
}

const emptyComponentsPlugin = `
import { defineNuxtPlugin } from '#app/nuxt'
export default defineNuxtPlugin({
  name: 'nuxt:global-components',
})
`

export const componentsPluginTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
  filename: 'components.plugin.mjs',
  getContents ({ app }) {
    const globalComponents = new Set<string>()
    for (const component of app.components) {
      if (component.global) {
        globalComponents.add(component.pascalName)
      }
    }
    if (!globalComponents.size) { return emptyComponentsPlugin }

    const components = [...globalComponents]

    return `import { defineNuxtPlugin } from '#app/nuxt'
import { ${components.map(c => 'Lazy' + c).join(', ')} } from '#components'
const lazyGlobalComponents = [
  ${components.map(c => `["${c}", Lazy${c}]`).join(',\n')}
]

export default defineNuxtPlugin({
  name: 'nuxt:global-components',
  setup (nuxtApp) {
    for (const [name, component] of lazyGlobalComponents) {
      nuxtApp.vueApp.component(name, component)
      nuxtApp.vueApp.component('Lazy' + name, component)
    }
  }
})
`
  }
}

export const componentNamesTemplate: NuxtPluginTemplate<ComponentsTemplateContext> = {
  filename: 'component-names.mjs',
  getContents ({ app }) {
    return `export const componentNames = ${JSON.stringify(app.components.filter(c => !c.island).map(c => c.pascalName))}`
  }
}

export const componentsIslandsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
  // components.islands.mjs'
  getContents ({ app }) {
    const components = app.components
    const islands = components.filter(component =>
      component.island ||
      // .server components without a corresponding .client component will need to be rendered as an island
      (component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client'))
    )
    return ['import { defineAsyncComponent } from \'vue\'', ...islands.map(
      (c) => {
        const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
        const comment = createImportMagicComments(c)
        return `export const ${c.pascalName} = /* #__PURE__ */ defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
      }
    )].join('\n')
  }
}

export const componentsTypeTemplate: NuxtTemplate<ComponentsTemplateContext> = {
  filename: 'components.d.ts',
  getContents: ({ app, nuxt }) => {
    const buildDir = nuxt.options.buildDir
    const componentTypes = app.components.filter(c => !c.island).map(c => [
      c.pascalName,
      `typeof ${genDynamicImport(isAbsolute(c.filePath)
        ? relative(buildDir, c.filePath).replace(/(?<=\w)\.(?!vue)\w+$/g, '')
        : c.filePath.replace(/(?<=\w)\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
    ])

    return `// Generated by components discovery
declare module 'vue' {
  export interface GlobalComponents {
${componentTypes.map(([pascalName, type]) => `    '${pascalName}': ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => `    'Lazy${pascalName}': ${type}`).join('\n')}
  }
}

${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join('\n')}

export const componentNames: string[]
`
  }
}
