import fs from 'node:fs'
import path from 'node:path'
import { createRequire } from 'node:module'
import { pathToFileURL } from 'node:url'
import { transformWithEsbuild } from 'vite'
import type { Plugin } from 'vite'
import { getProjectRoot } from './projectRoot'

const UMO_CJS_SPECS = [
  'dom-to-image-more',
  'hotkeys-js',
  'jsbarcode',
  'file-saver',
  'pretty-bytes',
  'smooth-signature',
  'pure-svg-code',
  'nzh/cn',
] as const

const UMO_CJS_NAMED_EXPORTS: Record<string, string[]> = {
  'file-saver': ['saveAs'],
  'pure-svg-code': ['qrcode', 'barcode', 'svg2url'],
}

const VIRTUAL_UMO_CJS_PREFIX = '\0virtual:umo-cjs:'

function requireFromProjectRoot() {
  return createRequire(path.join(getProjectRoot(), 'package.json'))
}

function resolvePackageMain(pkgDir: string): string | null {
  const pkgJsonPath = path.join(pkgDir, 'package.json')
  if (!fs.existsSync(pkgJsonPath)) return null

  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) as {
    main?: string
    module?: string
  }
  const main = pkgJson.module || pkgJson.main || 'index.js'
  const entry = path.join(pkgDir, main)
  return fs.existsSync(entry) ? entry : null
}

function findUmoPackageDir(pkgName: string): string | null {
  const projectRoot = getProjectRoot()
  const hoisted = path.join(projectRoot, 'node_modules', pkgName)
  if (fs.existsSync(path.join(hoisted, 'package.json'))) return hoisted

  const pnpmDir = path.join(projectRoot, 'node_modules/.pnpm')
  if (!fs.existsSync(pnpmDir)) return null

  const pnpmPrefix = pkgName.replace('/', '+')
  const pkgDir = fs.readdirSync(pnpmDir).find((name) => name.startsWith(`${pnpmPrefix}@`))
  if (!pkgDir) return null

  const resolved = path.join(pnpmDir, pkgDir, 'node_modules', pkgName)
  return fs.existsSync(path.join(resolved, 'package.json')) ? resolved : null
}

function findUmoPackageEntryForSpec(spec: string): string | null {
  const [pkgName, ...subParts] = spec.split('/')
  const pkgDir = findUmoPackageDir(pkgName)
  if (!pkgDir) return null

  if (subParts.length === 0) {
    return resolvePackageMain(pkgDir)
  }

  const sub = subParts.join('/')
  const candidates = [
    path.join(pkgDir, `${sub}.js`),
    path.join(pkgDir, sub, 'index.js'),
    path.join(pkgDir, sub),
  ]
  for (const candidate of candidates) {
    if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate
  }

  return null
}

function matchesUmoCjsFile(source: string, spec: string): boolean {
  const [pkg, ...sub] = spec.split('/')
  if (!source.includes(`/${pkg}/`)) return false
  if (sub.length === 0) {
    return source.endsWith('.js') || source.includes('.js?')
  }
  const subPath = sub.join('/')
  return source.includes(`/${pkg}/${subPath}.js`) || source.includes(`/${pkg}/${subPath}?`)
}

function augmentUmoCjsNamedExports(code: string, spec: string): string {
  const named =
    UMO_CJS_NAMED_EXPORTS[spec] ?? UMO_CJS_NAMED_EXPORTS[spec.split('/')[0] ?? '']
  if (!named?.length) return code

  const match = code.match(/export default (require_[A-Za-z0-9_]+\(\));?\s*$/)
  if (!match) return code

  const call = match[1]
  const namedLines = named
    .map((n) => `export const ${n} = __umoCjs.${n} ?? __umoCjs.default?.${n};`)
    .join('\n')

  return code.replace(
    /export default require_[A-Za-z0-9_]+\(\);?\s*$/,
    `const __umoCjs = ${call};\nexport default __umoCjs;\n${namedLines}\n`,
  )
}

type EsbuildModule = typeof import('esbuild')

let cachedEsbuild: EsbuildModule | null | undefined

async function getEsbuild(): Promise<EsbuildModule> {
  if (cachedEsbuild) return cachedEsbuild

  try {
    cachedEsbuild = requireFromProjectRoot()('esbuild') as EsbuildModule
    return cachedEsbuild
  } catch {
    // ignore
  }

  const pnpmDir = path.join(getProjectRoot(), 'node_modules/.pnpm')
  const esDir = fs.existsSync(pnpmDir)
    ? fs.readdirSync(pnpmDir).find((name) => name.startsWith('esbuild@'))
    : undefined

  if (esDir) {
    const esMain = path.join(pnpmDir, esDir, 'node_modules/esbuild/lib/main.js')
    if (fs.existsSync(esMain)) {
      const mod = (await import(pathToFileURL(esMain).href)) as EsbuildModule & { default?: EsbuildModule }
      cachedEsbuild = mod.default ?? mod
      return cachedEsbuild
    }
  }

  throw new Error('[umo-cjs-virtual] 未找到 esbuild，请在宿主项目执行: pnpm add -D esbuild')
}

async function bundleUmoCjsToEsm(entry: string): Promise<string> {
  const esbuild = await getEsbuild()
  const result = await esbuild.build({
    entryPoints: [entry],
    bundle: true,
    format: 'esm',
    platform: 'browser',
    write: false,
    logLevel: 'silent',
  })

  const code = result.outputFiles[0]?.text ?? ''
  if (code) return code

  const fallback = await transformWithEsbuild(fs.readFileSync(entry, 'utf-8'), entry, {
    loader: 'js',
    format: 'esm',
    platform: 'browser',
  })
  return fallback.code
}

export function umoCjsVirtual(): Plugin {
  return {
    name: 'umo-cjs-virtual',
    enforce: 'pre',
    resolveId(source) {
      if ((UMO_CJS_SPECS as readonly string[]).includes(source)) {
        return `${VIRTUAL_UMO_CJS_PREFIX}${source}`
      }

      for (const spec of UMO_CJS_SPECS) {
        if (matchesUmoCjsFile(source, spec)) {
          return `${VIRTUAL_UMO_CJS_PREFIX}${spec}`
        }
      }

      return null
    },
    async load(id) {
      if (!id.startsWith(VIRTUAL_UMO_CJS_PREFIX)) return

      const spec = id.slice(VIRTUAL_UMO_CJS_PREFIX.length)
      const entry = findUmoPackageEntryForSpec(spec)
      if (!entry) return null

      const code = augmentUmoCjsNamedExports(await bundleUmoCjsToEsm(entry), spec)
      return { code, map: null }
    },
  }
}
