const npm = require('./npm.js')

const chalk = require('chalk')
const ansiTrim = require('./utils/ansi-trim.js')
const table = require('text-table')
const output = require('./utils/output.js')
const usageUtil = require('./utils/usage.js')
const usage = usageUtil('doctor', 'npm doctor')
const { resolve } = require('path')

const ping = require('./utils/ping.js')
const checkPing = async () => {
  const tracker = npm.log.newItem('checkPing', 1)
  tracker.info('checkPing', 'Pinging registry')
  try {
    await ping(npm.flatOptions)
    return ''
  } catch (er) {
    if (/^E\d{3}$/.test(er.code || ''))
      throw er.code.substr(1) + ' ' + er.message
    else
      throw er.message
  } finally {
    tracker.finish()
  }
}

const pacote = require('pacote')
const getLatestNpmVersion = async () => {
  const tracker = npm.log.newItem('getLatestNpmVersion', 1)
  tracker.info('getLatestNpmVersion', 'Getting npm package information')
  try {
    const latest = (await pacote.manifest('npm@latest', npm.flatOptions)).version
    if (semver.gte(npm.version, latest))
      return `current: v${npm.version}, latest: v${latest}`
    else
      throw `Use npm v${latest}`
  } finally {
    tracker.finish()
  }
}

const semver = require('semver')
const fetch = require('make-fetch-happen')
const getLatestNodejsVersion = async () => {
  // XXX get the latest in the current major as well
  const current = process.version
  const currentRange = `^${current}`
  const url = 'https://nodejs.org/dist/index.json'
  const tracker = npm.log.newItem('getLatestNodejsVersion', 1)
  tracker.info('getLatestNodejsVersion', 'Getting Node.js release information')
  try {
    const res = await fetch(url, { method: 'GET', ...npm.flatOptions })
    const data = await res.json()
    let maxCurrent = '0.0.0'
    let maxLTS = '0.0.0'
    for (const { lts, version } of data) {
      if (lts && semver.gt(version, maxLTS))
        maxLTS = version

      if (semver.satisfies(version, currentRange) &&
          semver.gt(version, maxCurrent))
        maxCurrent = version
    }
    const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS
    if (semver.gte(process.version, recommended))
      return `current: ${current}, recommended: ${recommended}`
    else
      throw `Use node ${recommended} (current: ${current})`
  } finally {
    tracker.finish()
  }
}

const { promisify } = require('util')
const fs = require('fs')
const { R_OK, W_OK, X_OK } = fs.constants
const maskLabel = mask => {
  const label = []
  if (mask & R_OK)
    label.push('readable')

  if (mask & W_OK)
    label.push('writable')

  if (mask & X_OK)
    label.push('executable')

  return label.join(', ')
}
const lstat = promisify(fs.lstat)
const readdir = promisify(fs.readdir)
const access = promisify(fs.access)
const isWindows = require('./utils/is-windows.js')
const checkFilesPermission = async (root, shouldOwn, mask = null) => {
  if (mask === null)
    mask = shouldOwn ? R_OK | W_OK : R_OK

  let ok = true

  const tracker = npm.log.newItem(root, 1)

  try {
    const uid = process.getuid()
    const gid = process.getgid()
    const files = new Set([root])
    for (const f of files) {
      tracker.silly('checkFilesPermission', f.substr(root.length + 1))
      const st = await lstat(f)
        .catch(er => {
          ok = false
          tracker.warn('checkFilesPermission', 'error getting info for ' + f)
        })

      tracker.completeWork(1)

      if (!st)
        continue

      if (shouldOwn && (uid !== st.uid || gid !== st.gid)) {
        tracker.warn('checkFilesPermission', 'should be owner of ' + f)
        ok = false
      }

      if (!st.isDirectory() && !st.isFile())
        continue

      try {
        await access(f, mask)
      } catch (er) {
        ok = false
        const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})`
        tracker.error('checkFilesPermission', msg)
        continue
      }

      if (st.isDirectory()) {
        const entries = await readdir(f)
          .catch(er => {
            ok = false
            tracker.warn('checkFilesPermission', 'error reading directory ' + f)
            return []
          })
        for (const entry of entries)
          files.add(resolve(f, entry))
      }
    }
  } finally {
    tracker.finish()
    if (!ok) {
      throw `Check the permissions of files in ${root}` +
        (shouldOwn ? ' (should be owned by current user)' : '')
    } else
      return ''
  }
}

const which = require('which')
const getGitPath = async () => {
  const tracker = npm.log.newItem('getGitPath', 1)
  tracker.info('getGitPath', 'Finding git in your PATH')
  try {
    return await which('git').catch(er => {
      tracker.warn(er)
      throw "Install git and ensure it's in your PATH."
    })
  } finally {
    tracker.finish()
  }
}

const cacache = require('cacache')
const verifyCachedFiles = async () => {
  const tracker = npm.log.newItem('verifyCachedFiles', 1)
  tracker.info('verifyCachedFiles', 'Verifying the npm cache')
  try {
    const stats = await cacache.verify(npm.flatOptions.cache)
    const {
      badContentCount,
      reclaimedCount,
      missingContent,
      reclaimedSize,
    } = stats
    if (badContentCount || reclaimedCount || missingContent) {
      if (badContentCount)
        tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`)

      if (reclaimedCount)
        tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`)

      if (missingContent)
        tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`)

      tracker.warn('verifyCachedFiles', 'Cache issues have been fixed')
    }
    tracker.info('verifyCachedFiles', `Verification complete. Stats: ${
      JSON.stringify(stats, null, 2)
    }`)
    return `verified ${stats.verifiedContent} tarballs`
  } finally {
    tracker.finish()
  }
}

const { defaults: { registry: defaultRegistry } } = require('./utils/config.js')
const checkNpmRegistry = async () => {
  if (npm.flatOptions.registry !== defaultRegistry)
    throw `Try \`npm config set registry=${defaultRegistry}\``
  else
    return `using default registry (${defaultRegistry})`
}

const cmd = (args, cb) => doctor(args).then(() => cb()).catch(cb)

const doctor = async args => {
  npm.log.info('Running checkup')

  // each message is [title, ok, message]
  const messages = []

  const actions = [
    ['npm ping', checkPing, []],
    ['npm -v', getLatestNpmVersion, []],
    ['node -v', getLatestNodejsVersion, []],
    ['npm config get registry', checkNpmRegistry, []],
    ['which git', getGitPath, []],
    ...(isWindows ? [] : [
      ['Perms check on cached files', checkFilesPermission, [npm.cache, true, R_OK]],
      ['Perms check on local node_modules', checkFilesPermission, [npm.localDir, true]],
      ['Perms check on global node_modules', checkFilesPermission, [npm.globalDir, false]],
      ['Perms check on local bin folder', checkFilesPermission, [npm.localBin, false, R_OK | W_OK | X_OK]],
      ['Perms check on global bin folder', checkFilesPermission, [npm.globalBin, false, X_OK]],
    ]),
    ['Verify cache contents', verifyCachedFiles, [npm.flatOptions.cache]],
    // TODO:
    // - ensure arborist.loadActual() runs without errors and no invalid edges
    // - ensure package-lock.json matches loadActual()
    // - verify loadActual without hidden lock file matches hidden lockfile
    // - verify all local packages have bins linked
  ]

  for (const [msg, fn, args] of actions) {
    const line = [msg]
    try {
      line.push(true, await fn(...args))
    } catch (er) {
      line.push(false, er)
    }
    messages.push(line)
  }

  const silent = npm.log.levels[npm.log.level] > npm.log.levels.error

  const outHead = ['Check', 'Value', 'Recommendation/Notes']
    .map(!npm.color ? h => h : h => chalk.underline(h))
  let allOk = true
  const outBody = messages.map(!npm.color
    ? item => {
      allOk = allOk && item[1]
      item[1] = item[1] ? 'ok' : 'not ok'
      item[2] = String(item[2])
      return item
    }
    : item => {
      allOk = allOk && item[1]
      if (!item[1]) {
        item[0] = chalk.red(item[0])
        item[2] = chalk.magenta(String(item[2]))
      }
      item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok')
      return item
    })
  const outTable = [outHead, ...outBody]
  const tableOpts = {
    stringLength: s => ansiTrim(s).length,
  }

  if (!silent) {
    output(table(outTable, tableOpts))
    if (!allOk)
      console.error('')
  }
  if (!allOk)
    throw 'Some problems found. See above for recommendations.'
}

module.exports = Object.assign(cmd, { usage })
