/// <reference types="node" />

import { join, basename } from 'node:path'
import { readdir, lstat, writeFile } from 'node:fs/promises'

import type {
  MetaCheckerOptions,
  EventMeta,
  PropertyMeta,
  ExposeMeta,
  SlotMeta,
} from 'vue-component-meta'
import { createCheckerByJson } from 'vue-component-meta'

const checkerOptions: MetaCheckerOptions = {
  forceUseTs: true,
  schema: false,
  printer: { newLine: 1 },
}

type Checker = ReturnType<typeof createCheckerByJson>
async function main() {
  const root = process.cwd()
  const tsconfig = join(root, './tsconfig.json')
  const componentDir = join(root, './src/components/')
  const out = join(root, './src/data/documentation/components-meta.ts')

  const checker = createCheckerByJson(root, {
    extends: tsconfig,
  }, checkerOptions)

  const components: Record<string, any> = {}
  const directories = ['base', 'base-addons', 'layouts']
  await Promise.all(
    directories.map(dir => walk(components, checker, join(componentDir, dir))),
  )

  const sorted: Record<string, any> = {}
  Object.keys(components).sort().forEach((key) => {
    sorted[key] = components[key]
  })

  const content = [
    `/* eslint-disable */`,
    `// This file is auto generated by scripts/generate-component-meta.ts`,
    `import type { ComponentMeta } from 'vue-component-meta'`,
    ``,
    `export const components = ${JSON.stringify(
      Object.keys(sorted),
    )} as const`,
    ``,
    ...Object.entries(sorted).map(([name, meta]) => {
      return `export const ${name}Meta: ComponentMeta = ${JSON.stringify(
        meta,
      )} as const`
    }),
  ].join('\n')

  await writeFile(out, content)
}

async function walk(
  components: Record<string, any>,
  checker: Checker,
  dir: string,
) {
  const files = await readdir(dir)
  for (const file of files) {
    const path = join(dir, file)
    const stat = await lstat(path)
    if (stat.isDirectory()) {
      await walk(components, checker, path) // recursive
    }

    const isVueFile = path.endsWith('.vue')
    if (!isVueFile) continue

    const name = basename(path, '.vue')
    const meta = await extractMeta(checker, path)
    components[name] = meta
  }
}

async function extractMeta(checker: Checker, path: string) {
  const metaFields = checker.getComponentMeta(path)
  const meta = {
    type: metaFields.type,
    props: metaFields.props.filter(field => !field.global),
    events: metaFields.events,
    exposed: metaFields.exposed.filter((field) => {
      const isProps
        = metaFields.props?.findIndex(prop => prop.name === field.name) >= 0
      const isEvent
        = metaFields.events?.findIndex(
          (event: any) =>
            `on${event.name}`.toLowerCase() === field.name?.toLowerCase(),
        ) >= 0
      const isExcluded = field.name?.startsWith('$')
      const isModel = field.name === 'modelValue'

      return !(isProps || isEvent || isExcluded || isModel)
    }),
    slots: metaFields.slots,
  }

  meta.props.forEach(field => cleanMeta(field))
  meta.events.forEach(field => cleanMeta(field))
  meta.slots.forEach(field => cleanMeta(field))
  meta.exposed.forEach(field => cleanMeta(field))

  meta.props.sort((a, b) => {
    // sort required properties first
    if (!a.required && b.required) {
      return 1
    }
    if (a.required && !b.required) {
      return -1
    }
    // then ensure boolean properties are sorted last
    if (a.type === 'boolean' && b.type !== 'boolean') {
      return 1
    }
    if (a.type !== 'boolean' && b.type === 'boolean') {
      return -1
    }

    return a.name.localeCompare(b.name)
  })
  meta.events.sort((a, b) => a.name.localeCompare(b.name))
  meta.slots.sort((a, b) => a.name.localeCompare(b.name))
  meta.exposed.sort((a, b) => a.name.localeCompare(b.name))

  return meta
}

function cleanMeta(field: EventMeta | PropertyMeta | ExposeMeta | SlotMeta) {
  if ('schema' in field) {
    // @ts-expect-error can't be undefined
    delete field.schema

    if ('signature' in field) {
      field.schema = []
    }
    else {
      field.schema = ''
    }
  }

  if ('declarations' in field) {
    // @ts-expect-error can't be undefined
    delete field.declarations

    field.declarations = []
  }
}

main().catch(console.error)
