import fs from 'fs'
import Module from 'module'
import { dirname, extname, join, resolve as pathResolve } from 'path'
import { pathToFileURL, fileURLToPath } from 'url'
import JoyCon from 'joycon'
import { addHook } from 'pirates'
import sourceMapSupport from 'source-map-support'
import tsconfigLoader from 'tsconfig-paths/lib/tsconfig-loader'
import { createMatchPath } from 'tsconfig-paths'

const ESBUILD_PATH = process.env.ESBUILD_PATH || '/opt/nodejs/node_modules/esbuild'
const { default: esbuild } = await import(ESBUILD_PATH)
const { transform, transformSync } = esbuild

const baseURL = pathToFileURL(`${process.cwd()}/`).href

/*
TODO: consider caching loaded files in to memory if asked, so that we can cache them to negate the need to nft/esbuild?
at least cache transforms?
*/

const joyCon = new JoyCon()
const matcherCache = {}

function isDir(path) {
  const stats = fs.statSync(path, { throwIfNoEntry: false })
  if (!stats) {
    return false
  }
  return stats.isDirectory()
}

const bareImportSearchExtensions = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']

function bareImportSearch(specifier) {
  for (const ext of [...bareImportSearchExtensions, ...bareImportSearchExtensions.map((ext) => `/index${ext}`)]) {
    const fullPath = `${specifier}${ext}`
    if (fs.existsSync(fullPath)) {
      return fullPath
    }
  }
}

function matchPath(specifier, parentPath) {
  // parentPath can be a file or a directory
  const base = isDir(parentPath) ? parentPath : dirname(parentPath || process.cwd())

  const configs = [joyCon.resolveSync(['tsconfig.json'], base), joyCon.resolveSync(['jsconfig.json'], base)]

  for (const configPath of configs) {
    if (configPath) {
      let matcher = matcherCache[configPath]
      if (!matcher) {
        const loaded = tsconfigLoader.loadTsconfig(configPath)
        if (loaded) {
          const { baseUrl, paths } = loaded.compilerOptions ?? {}

          if (paths) {
            const absoluteBaseUrl = baseUrl ? pathResolve(dirname(configPath), baseUrl) : dirname(configPath)
            matcher = createMatchPath(absoluteBaseUrl, paths)
            matcherCache[configPath] = matcher
          }
        }
      }

      if (matcher) {
        const matched = matcher(specifier)
        if (matched) {
          if (extensions.has(extname(matched))) {
            return matched
          }

          const bareImportMatch = bareImportSearch(matched)
          if (bareImportMatch) {
            return bareImportMatch
          }

          return matched
        }
      }
    }
  }

  if (isBareImport(specifier)) {
    const bareImportMatch = bareImportSearch(join(base, specifier))
    if (bareImportMatch) {
      return bareImportMatch
    }
  }
}

const sourceMaps = {}

sourceMapSupport.install({
  handleUncaughtExceptions: false,
  environment: 'node',
  retrieveSourceMap(file) {
    if (sourceMaps[file]) {
      return {
        url: file,
        map: sourceMaps[file]
      }
    } else {
      const name = `${file}.map`
      if (fs.existsSync(name)) {
        const map = JSON.parse(fs.readFileSync(name).toString('utf8'))
        sourceMaps[file] = map
        return {
          url: file,
          map
        }
      }
    }
    return null
  }
})

function isModulePackage(base) {
  const { data = {} } = joyCon.loadSync(['package.json'], base)
  return data.type === 'module'
}

const extensions = new Set(['.js', '.jsx', '.mjs', '.cjs', '.json', '.node', '.ts', '.tsx', '.mts', '.cts'])

function isBareImport(specifier) {
  return (
    (specifier.startsWith('./') ||
      specifier.startsWith('..') ||
      specifier.startsWith('/') ||
      specifier.startsWith('file:///') ||
      specifier.startsWith('file://.')) &&
    !extensions.has(extname(specifier))
  )
}

export function resolve(specifier, context, defaultResolve) {
  if (specifier === '@serverless/cloud') {
    return defaultResolve('@ampt/cloud', context)
  }

  if (!specifier.startsWith('file://') && !specifier.startsWith('node:')) {
    const { parentURL = baseURL } = context
    const matched = matchPath(specifier, fileURLToPath(parentURL))

    if (matched) {
      return { shortCircuit: true, url: pathToFileURL(matched).href }
    }
  }

  return defaultResolve(specifier, context)
}

async function readTsConfig(forFilePath) {
  const base = isDir(forFilePath) ? forFilePath : dirname(forFilePath || process.cwd())
  for (const configPath of [joyCon.resolveSync(['tsconfig.json'], base), joyCon.resolveSync(['jsconfig.json'], base)]) {
    if (fs.existsSync(configPath)) {
      return fs.readFileSync(configPath).toString('utf8')
    }
  }
}

const transformOptions = {
  '.jsx': {
    loader: 'jsx',
    tsconfig: true
  },
  '.ts': {
    loader: 'ts',
    tsconfig: true
  },
  '.tsx': {
    loader: 'tsx',
    tsconfig: true
  },
  '.cts': {
    loader: 'ts',
    esm: false
  },
  '.mts': {
    loader: 'ts',
    esm: true
  }
}

export async function load(sourcefile, context, defaultLoad) {
  const result = await defaultLoad(sourcefile, context)

  if (sourcefile.startsWith('file://')) {
    const path = fileURLToPath(sourcefile)
    const ext = extname(path)
    const options = transformOptions[ext]

    if (options) {
      const isESM =
        context.format === 'module' ||
        result.format === 'module' ||
        options.esm === true ||
        (options.esm === undefined && isModulePackage(path))

      const tsconfigRaw = options.tsconfig ? await readTsConfig(path) : undefined

      const { code, warnings, map } = await transform(result.source.toString(), {
        sourcefile,
        format: isESM ? 'esm' : 'cjs',
        sourcemap: true,
        sourcesContent: false,
        loader: options.loader,
        target: `node${process.version.slice(1)}`,
        tsconfigRaw
      })

      sourceMaps[sourcefile] = map

      if (warnings && warnings.length > 0) {
        for (const warning of warnings) {
          console.log(warning.location)
          console.log(warning.text)
        }
      }

      result.shortCircuit = true
      result.source = code
      result.format = isESM ? 'module' : 'commonjs'
    }
  }

  return result
}

function compile(source, sourcefile) {
  const {
    code,
    warnings = [],
    map
  } = transformSync(source, {
    sourcefile,
    format: 'cjs',
    loader: 'ts',
    sourcemap: true,
    sourcesContent: false
  })

  sourceMaps[sourcefile] = map

  for (const warning of warnings) {
    console.warn(warning)
  }

  return code
}

addHook(compile, {
  exts: ['.ts', '.cts'],
  ignoreNodeModules: false
})

const originalResolveFilename = Module._resolveFilename
Module._resolveFilename = function (request, parent, ...rest) {
  if (request === '@serverless/cloud') {
    return originalResolveFilename.call(this, '@ampt/cloud', parent, ...rest)
  }

  // console.log('resolve filenae', request, parent)
  // todo: why is parent undefined sometimes, and what can we pass to matchPath?
  const matchedPath = parent ? matchPath(request, parent.path) : null
  return originalResolveFilename.call(this, matchedPath || request, parent, ...rest)
}
