import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
import fg from 'fast-glob'
import _ from 'lodash'
import path from 'path'
import fs from 'fs-extra'
import { fileResolveByExtension, fileReader } from './utils'
import { parse as cjsParse } from 'cjs-esm-exports'
import { init as esInit, parse as esParse } from 'es-module-lexer'
import { FILE_EXCLUDES, FILE_EXTENSIONS, FILES_IGNORE } from './constants'
import { recursionExportsValues } from './recursion'

type Recordable = Record<string, any>

const DEFAULT_ENTRY = 'index.js'

const IGNORE_KEYS = ['default', 'types', 'typings', 'development', 'browser']

const JS_RE = /\.[m]?js$/

export async function resolvePackage(cachePath: string) {
  const pkg = await readPackageJSON(cachePath)
  // @ts-ignore
  if (pkg.__ESMD__ === true) {
    return pkg
  }

  const pkgExports = await resolveExports(pkg, cachePath)

  const files = await resolveFiles(pkg, cachePath, pkgExports)
  pkg.exports = pkgExports
  pkg.files = files

  // pkg.imports
  Object.assign(pkg, await resolveImports(pkg))

  // @ts-ignore
  pkg.__ESMD__ = true
  return pkg
}

export async function resolveImports(pkg: Recordable) {
  const { browser } = pkg
  if (!browser || !_.isObject(browser)) {
    return {}
  }

  const pkgBrowser: any = browser

  const resultImport = pkgBrowser?.import ?? {}

  for (const [key, value] of Object.entries(pkgBrowser)) {
    const importKey = `#${key.replace(/^((\.+)?\/?)/, '')}`
    if (resultImport[importKey]) {
      continue
    }
    Object.assign(resultImport, {
      [importKey]: {
        browser: normalizeExport(value as string),
        default: normalizeExport(key),
      },
    })
  }
  return { imports: resultImport }
}

export async function resolveExports(pkg: PackageJson, root: string) {
  let {
    exports: pkgExports,
    module: pkgModule,
    main: pkgMain,
    browser: pkgBrowser,
    files: pkgFiles = [],
  } = pkg as any
  const cjsMainFiles: string[] = []

  const unStandard = !pkgExports && !pkgMain && !pkgModule

  // exports exits
  if (pkgExports !== undefined && pkgExports !== null) {
    // export include {"./*":"./*"}
    if (_.isObject(pkgExports)) {
      if (pkgExports['./*'] === './*') {
        const result = await addCjsFiledToExports({
          root,
          pkgExports,
          cjsMainFiles,
        })
        return await developmentifyExports(result, root)
      }
      pkgExports['./package.json'] = './package.json.js'
    }
    if (_.isString(pkgExports)) {
      pkgExports = { '.': pkgExports }
    }

    for (const [key, pattern] of Object.entries(pkgExports)) {
      if (
        (key.includes('*') && _.isString(pattern) && pattern.includes('*')) ||
        (key.endsWith('/') && _.isString(pattern) && pattern.endsWith('/'))
      ) {
        await addMatchFileToExports(pattern, pkgExports, root)
        continue
      }
      if (_.isObject(pattern)) {
        await handleObjectPattern({
          root,
          pkgExports,
          key,
          pattern: pattern as Recordable,
        })
      }
    }

    const result = await addCjsFiledToExports({
      root,
      pkgExports,
      cjsMainFiles,
    })
    return await developmentifyExports(result, root)
  } else if (!pkgExports) {
    // const cjsManiFiles: string = []

    // no exports exist
    const resultExports: Recordable = {}

    // module and main do not exist
    if (!pkgModule && !pkgMain) {
      const normalizePkgMain = normalizeExport(guessEntryFile(root, pkgFiles))
      const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
      cjsMainFiles.push(...addonCjsMainFiles)

      Object.assign(resultExports, {
        '.': normalizePkgMain,
      })
    }
    // module and main is exits
    else if (pkgModule && pkgMain) {
      const normalizePkgMain = normalizeExport(pkgMain)
      const normalizepkgModule = normalizeExport(pkgModule)
      const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
      cjsMainFiles.push(...addonCjsMainFiles)

      Object.assign(resultExports, {
        '.': await resolveMainAndModule(
          root,
          normalizePkgMain,
          normalizepkgModule,
          pkgBrowser,
        ),
      })

      if (!pkgBrowser) {
        Object.assign(resultExports, await joinFilesToExports(root, pkgFiles))
      }

      resultExports['./package.json'] = './package.json.js'

      Object.assign(resultExports, {
        [normalizePkgMain]: await resolveMain(root, normalizePkgMain),
        [normalizepkgModule]: normalizepkgModule,
      })
    } else if (!pkgModule && pkgMain) {
      const normalizePkgMain = normalizeExport(pkgMain)
      const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
      cjsMainFiles.push(...addonCjsMainFiles)

      Object.assign(resultExports, {
        '.': await resolveMain(root, normalizePkgMain),
        [normalizePkgMain]: await resolveMain(root, normalizePkgMain),
      })

      resultExports['./package.json'] = './package.json.js'

      Object.assign(resultExports, await joinFilesToExports(root, pkgFiles))
    }

    pkgExports = resultExports
  }

  if (unStandard) {
    pkgExports['./package.json'] = './package.json.js'
  }

  let result = await addCjsFiledToExports({ root, pkgExports, cjsMainFiles })

  result = await handlerPkgBrowser(pkgBrowser as any, result)

  return await developmentifyExports(result, root)
}

export async function resolveFiles(
  pkg: PackageJson,
  root: string,
  pkgExports: Record<string, any>,
) {
  let { files = [] } = pkg
  files = []

  // add package.json、package.json.js to files
  files.push(...['', '.js'].map((item) => `package.json${item}`))

  // exports file
  const values = recursionExportsValues(pkgExports)

  files.push(
    ...values.map((item) => {
      return item.replace(/^\.?\//, '')
    }),
  )

  // Add .map to js file
  files.forEach((item) => {
    if (/\.[m|c]?js$/.test(item) && !item.includes('*')) {
      files.push(`${item}.map`)
    }
  })

  // add license and readme.md to files
  const matchFiles = await fg(['license', 'readme.md', 'changelog.md'], {
    cwd: root,
    caseSensitiveMatch: false,
  })

  const matchTsFiles = await fg('./*.d.ts', {
    cwd: root,
    caseSensitiveMatch: false,
  })

  files.push(...matchFiles, ...matchTsFiles)
  files = files.filter((item) => !['*', '.', './', './*'].includes(item))
  return Array.from(new Set(files)).sort()
}

async function developmentifyExports(pkgExports: Recordable, root: string) {
  for (const [key, value] of Object.entries(pkgExports)) {
    const ext = path.extname(key)

    if (
      !ext ||
      key.endsWith('.prod.cjs') ||
      !['.js', '.cjs'].includes(ext) ||
      !_.isString(value) ||
      key === './index.js'
    ) {
      continue
    }

    const filepath = path.join(root, key)
    if (!fs.existsSync(filepath)) {
      continue
    }

    const { isEsm } = await getFileType(root, key)
    if (isEsm) {
      continue
    }

    const content = fileReader(filepath)
    const dynamicEntry = await isDynamicEntry(content)
    if (dynamicEntry) {
      const devKeys = await resolveDevFilename(key)
      const itemExports = {
        default: key,
        development: devKeys,
      }
      pkgExports[key] = itemExports
      const unExtKey = key.substring(0, key.length - ext.length)
      if (pkgExports[unExtKey]) {
        pkgExports[unExtKey] = itemExports
      }
    }
  }

  return pkgExports
}

async function joinFilesToExports(root: string, pkgFiles: string[]) {
  const resultExports: Recordable = {}
  const getFiles = await findPkgFiles(root, pkgFiles)
  getFiles.forEach((file) => {
    const fileKey = normalizeExport(file)
    if (!fileKey.endsWith('.cjs.js')) {
      resultExports[fileKey] = fileKey
      if (JS_RE.test(fileKey)) {
        resultExports[removeFileExt(fileKey)] = fileKey
      }
    }
  })
  return resultExports
}

async function handlerPkgBrowser(
  pkgBrowser: Recordable | undefined,
  pkgExports: Recordable,
) {
  if (!pkgBrowser || _.isString(pkgBrowser)) {
    return pkgExports
  }

  const resultExports = pkgExports
  for (const [key, value] of Object.entries(pkgBrowser)) {
    if (!resultExports[key] || _.isString(resultExports[key])) {
      resultExports[removeFileExt(key)] = resultExports[key] = {
        default: normalizeExport(key),
        browser: normalizeExport(value),
      }
    }
  }
  return resultExports
}

async function createCjsMainFiles(root: string, pkgMain: string) {
  const { isCjs } = await getFileType(root, pkgMain)
  //   const isDynamic = await getIsDynamic(root, pkgMain)
  const cjsMainFiles: string[] = []
  if (isCjs) {
    cjsMainFiles.push(...[pkgMain])
  }
  return cjsMainFiles
}

async function findPkgFiles(root: string, pkgFiles: string[] = []) {
  const pattern =
    pkgFiles.length === 0
      ? '**/**'
      : pkgFiles.map((item) => {
          if (item.includes('*') || path.extname(item)) {
            return item
          } else {
            const filepath = path.join(root, item)
            if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
              return item
            }
            return `${item}${item.endsWith('/') ? '' : '/'}**/**.js`
          }
        })
  const files = await fg(pattern, { cwd: root, ignore: FILES_IGNORE })
  const pkg = await readPackageJSON(root)
  return files.filter((item) => {
    const ext = path.extname(item)
    if (!ext || ext === '.ts' || excludeFiles(pkg, item)) {
      return false
    }
    return FILE_EXTENSIONS.includes(ext)
  })
}

function excludeFiles(pkg: Recordable, file: string) {
  return (
    // test files
    FILE_EXCLUDES.some((item) => file.includes(item)) ||
    // min files
    file.endsWith('.min.js') ||
    file.endsWith('.prod.js') ||
    // dev files
    file.endsWith('.development.js') ||
    file.endsWith('.dev.js') ||
    // global files
    file.endsWith('.global.js')
  )
}

// function filterFiles() {}

async function handleObjectPattern({
  root,
  pkgExports,
  key,
  pattern,
}: {
  root: string
  pkgExports: Recordable
  key: string
  pattern: Recordable
}) {
  for (const [pkey, pval] of Object.entries(pattern)) {
    if (IGNORE_KEYS.includes(pkey)) {
      continue
    }
    if (_.isString(pval)) {
      if (
        pattern.import &&
        pattern.require &&
        _.isString(pattern.import) &&
        _.isString(pattern.require) &&
        pattern.require !== pattern.import
      ) {
        continue
      }

      pkgExports[key][pkey] = await resolveMain(root, pval)
    } else if (_.isObject(pval)) {
      for (const [ik, iv] of Object.entries(pval)) {
        if (IGNORE_KEYS.includes(ik)) {
          continue
        }
        if (_.isString(iv)) {
          pkgExports[key][pkey][ik] = await resolveMain(root, iv)
        }
      }
    }
  }
}

async function addMatchFileToExports(
  pattern: string,
  pkgExports: Recordable,
  cwd: string,
) {
  const files = fg.sync(pattern.endsWith('/') ? `${pattern}*` : pattern, {
    cwd,
  })

  for (const key of files) {
    pkgExports[key] = normalizeExport(key)
  }
}

async function addCjsFiledToExports({
  root,
  pkgExports,
  cjsMainFiles,
}: {
  root: string
  pkgExports: Recordable
  cjsMainFiles: string[]
}) {
  for (const [key, value] of Object.entries(pkgExports)) {
    // {require:'./index'}
    if (_.isObject(value)) {
      const requireFile = (value as any).require
      if (requireFile) {
        if (_.isString(requireFile)) {
          pkgExports[`${requireFile}!cjs`] = requireFile
        } else if (_.isObject(requireFile)) {
          for (const rval of Object.values(requireFile)) {
            pkgExports[`${rval}!cjs`] = rval
          }
        }
      }
    } else {
      try {
        if (!key.includes('*') && !key.startsWith('.ts')) {
          if (JS_RE.test(key)) {
            const { isCjs } = await getFileType(root, key)
            if (isCjs) {
              pkgExports[`${key}!cjs`] = key
            }
          } else if (key.endsWith('package.json')) {
            pkgExports[`${value}!cjs`] = value
          }
        }
      } catch (error) {
        // No error will be reported for files that cannot be obtained
      }
    }
  }

  for (const cjsMainFile of cjsMainFiles) {
    pkgExports[`${cjsMainFile}!cjs`] = cjsMainFile
  }
  return pkgExports
}

// For packages that do not specify any identity, you need to try to guess his entry
function guessEntryFile(root: string, pkgFiles: string[]) {
  let indexFile = pkgFiles.find((file) => path.basename(file) === 'index')

  if (indexFile) {
    indexFile = path.basename(indexFile)
  } else {
    indexFile = 'index'
  }

  const entryFile = fileResolveByExtension(path.join(root || '', indexFile))

  if (entryFile) {
    return path.relative(root, entryFile)
  }
  return DEFAULT_ENTRY
}

async function resolveMainAndModule(
  root: string,
  pkgMain: string,
  pkgModule: string,
  pkgBrowser: any,
) {
  const result = {
    module: normalizeExport(pkgModule),
    default: await resolveMain(root, pkgMain),
  }

  if (_.isString(pkgBrowser)) {
    Object.assign(result, {
      browser: normalizeExport(pkgBrowser),
    })
  }

  return result
}

async function resolveMain(root: string, pkgMain: string) {
  const normalizeMain = normalizeExport(pkgMain)
  const isDynamicEntry = await getIsDynamic(root, pkgMain)
  const { isUmd } = await getFileType(root, pkgMain)

  if (isUmd || pkgMain.endsWith('.mjs') || !isDynamicEntry) {
    return normalizeMain
  }

  return {
    // TODO dev file
    // development: normalizeMain,
    development: !isDynamicEntry
      ? normalizeMain
      : // "./index.js" => "./dev.index.js"
        await resolveDevFilename(normalizeMain),
    default: normalizeMain,
  }
}

export async function resolveDevFilename(filename: string) {
  // "./index.js" => "./dev.index.js"
  const basename = path.basename(filename)
  const newBaseName = basename.replace(
    /^(\.?\/?)*/,
    (m, $1) => `${$1 || ''}dev.`,
  )
  return filename.replace(basename, newBaseName)
}

async function getIsDynamic(root: string, filepath: string) {
  const mainContent = fileReader(path.join(root, filepath))
  let isDynamic = false
  if (JS_RE.test(filepath)) {
    isDynamic = await isDynamicEntry(mainContent)
  }
  return isDynamic
}

export async function isDynamicEntry(source: string) {
  try {
    // const { exports: devExports, reexports: devReexports } = cjsParse(
    //   filename,
    //   source,
    //   'development',
    // )
    // const { exports: prodExports, reexports: prodReexports } = cjsParse(
    //   filename,
    //   source,
    //   'production',
    // )
    // return devReexports?.length !== 0 && prodReexports?.length !== 0
    // process.env.NODE_ENV === 'production'
    return (
      source.replace(/\s*/g, '').includes(`process.env.NODE_ENV==`) ||
      source.replace(/\s*/g, '').includes(`process.env.NODE_ENV!=`)
    )
  } catch (error) {
    return false
  }
}

async function getFileType(root: string, filepath: string) {
  if (!JS_RE.test(filepath)) {
    return { isCjs: false, isEsm: false, isUmd: false }
  }
  const pkg = await readPackageJSON(root)
  await esInit

  const source = fileReader(path.join(root, filepath))
  const [importer, exporter] = esParse(source)

  const { reexports } = cjsParse(filepath, source, 'production')

  const isCjs =
    pkg.type !== 'module' &&
    importer.length === 0 &&
    exporter.length === 0 &&
    !filepath.endsWith('.mjs')

  if (isCjs) {
    const isUmd = !exporter.length && !importer.length && reexports.length === 0
    return {
      isCjs: true,
      isEsm: false,
      isUmd,
    }
  }

  const isEsm =
    importer.length !== 0 ||
    exporter.length !== 0 ||
    pkg.type === 'module' ||
    filepath.endsWith('.mjs')

  return {
    isCjs,
    isEsm,
    isUmd: isCjs && exporter.length === 0,
  }
}

export function normalizeExport(str: string) {
  if (str?.startsWith('./')) {
    return str
  }

  if (str?.startsWith('/')) {
    return `.${str}`
  }
  return `./${str}`
}

export function removeFileExt(file: string) {
  const lastIndex = file.lastIndexOf(path.extname(file))
  return file.substring(0, lastIndex)
}
