'use strict'
exports.generate = generate
exports.generateFromInstall = generateFromInstall
exports.submitForInstallReport = submitForInstallReport
exports.submitForFullReport = submitForFullReport
exports.printInstallReport = printInstallReport
exports.printFullReport = printFullReport

const Bluebird = require('bluebird')
const auditReport = require('npm-audit-report')
const treeToShrinkwrap = require('../shrinkwrap.js').treeToShrinkwrap
const packageId = require('../utils/package-id.js')
const output = require('../utils/output.js')
const npm = require('../npm.js')
const qw = require('qw')
const registryFetch = require('npm-registry-fetch')
const zlib = require('zlib')
const gzip = Bluebird.promisify(zlib.gzip)
const log = require('npmlog')
const perf = require('../utils/perf.js')
const url = require('url')
const npa = require('npm-package-arg')
const uuid = require('uuid')
const ssri = require('ssri')
const cloneDeep = require('lodash.clonedeep')
const pacoteOpts = require('../config/pacote.js')

// used when scrubbing module names/specifiers
const runId = uuid.v4()

function submitForInstallReport (auditData) {
  const cfg = npm.config // avoid the no-dynamic-lookups test
  const scopedRegistries = cfg.keys.filter((_) => /:registry$/.test(_)).map((_) => cfg.get(_))
  perf.emit('time', 'audit compress')
  // TODO: registryFetch will be adding native support for `Content-Encoding: gzip` at which point
  // we'll pass in something like `gzip: true` and not need to JSON stringify, gzip or headers.
  return gzip(JSON.stringify(auditData)).then((body) => {
    perf.emit('timeEnd', 'audit compress')
    log.info('audit', 'Submitting payload of ' + body.length + 'bytes')
    scopedRegistries.forEach((reg) => {
      // we don't care about the response so destroy the stream if we can, or leave it flowing
      // so it can eventually finish and clean up after itself
      fetchAudit(url.resolve(reg, '/-/npm/v1/security/audits/quick'))
        .then((_) => {
          _.body.on('error', () => {})
          if (_.body.destroy) {
            _.body.destroy()
          } else {
            _.body.resume()
          }
        }, () => {})
    })
    perf.emit('time', 'audit submit')
    return fetchAudit('/-/npm/v1/security/audits/quick', body).then((response) => {
      perf.emit('timeEnd', 'audit submit')
      perf.emit('time', 'audit body')
      return response.json()
    }).then((result) => {
      perf.emit('timeEnd', 'audit body')
      return result
    })
  })
}

function submitForFullReport (auditData) {
  perf.emit('time', 'audit compress')
  // TODO: registryFetch will be adding native support for `Content-Encoding: gzip` at which point
  // we'll pass in something like `gzip: true` and not need to JSON stringify, gzip or headers.
  return gzip(JSON.stringify(auditData)).then((body) => {
    perf.emit('timeEnd', 'audit compress')
    log.info('audit', 'Submitting payload of ' + body.length + ' bytes')
    perf.emit('time', 'audit submit')
    return fetchAudit('/-/npm/v1/security/audits', body).then((response) => {
      perf.emit('timeEnd', 'audit submit')
      perf.emit('time', 'audit body')
      return response.json()
    }).then((result) => {
      perf.emit('timeEnd', 'audit body')
      return result
    })
  })
}

function fetchAudit (href, body) {
  const opts = pacoteOpts()
  return registryFetch(href, {
    method: 'POST',
    headers: { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' },
    config: npm.config,
    npmSession: opts.npmSession,
    projectScope: npm.projectScope,
    log: log,
    body: body
  })
}

function printInstallReport (auditResult) {
  return auditReport(auditResult, {
    reporter: 'install',
    withColor: npm.color,
    withUnicode: npm.config.get('unicode')
  }).then((result) => output(result.report))
}

function printFullReport (auditResult) {
  return auditReport(auditResult, {
    log: output,
    reporter: 'detail',
    withColor: npm.color,
    withUnicode: npm.config.get('unicode')
  }).then((result) => output(result.report))
}

function generate (shrinkwrap, requires, diffs, install, remove) {
  const sw = cloneDeep(shrinkwrap)
  delete sw.lockfileVersion
  sw.requires = scrubRequires(requires)
  scrubDeps(sw.dependencies)

  // sw.diffs = diffs || {}
  sw.install = (install || []).map(scrubArg)
  sw.remove = (remove || []).map(scrubArg)
  return generateMetadata().then((md) => {
    sw.metadata = md
    return sw
  })
}

const scrubKeys = qw`version`
const deleteKeys = qw`from resolved`

function scrubDeps (deps) {
  if (!deps) return
  Object.keys(deps).forEach((name) => {
    if (!shouldScrubName(name) && !shouldScrubSpec(name, deps[name].version)) return
    const value = deps[name]
    delete deps[name]
    deps[scrub(name)] = value
  })
  Object.keys(deps).forEach((name) => {
    for (let toScrub of scrubKeys) {
      if (!deps[name][toScrub]) continue
      deps[name][toScrub] = scrubSpec(name, deps[name][toScrub])
    }
    for (let toDelete of deleteKeys) delete deps[name][toDelete]

    scrubRequires(deps[name].requires)
    scrubDeps(deps[name].dependencies)
  })
}

function scrubRequires (reqs) {
  if (!reqs) return reqs
  Object.keys(reqs).forEach((name) => {
    const spec = reqs[name]
    if (shouldScrubName(name) || shouldScrubSpec(name, spec)) {
      delete reqs[name]
      reqs[scrub(name)] = scrubSpec(name, spec)
    } else {
      reqs[name] = scrubSpec(name, spec)
    }
  })
  return reqs
}

function getScope (name) {
  if (name[0] === '@') return name.slice(0, name.indexOf('/'))
}

function shouldScrubName (name) {
  const scope = getScope(name)
  const cfg = npm.config // avoid the no-dynamic-lookups test
  return Boolean(scope && cfg.get(scope + ':registry'))
}
function shouldScrubSpec (name, spec) {
  const req = npa.resolve(name, spec)
  return !req.registry
}

function scrubArg (arg) {
  const req = npa(arg)
  let name = req.name
  if (shouldScrubName(name) || shouldScrubSpec(name, req.rawSpec)) {
    name = scrubName(name)
  }
  const spec = scrubSpec(req.name, req.rawSpec)
  return name + '@' + spec
}

function scrubName (name) {
  return shouldScrubName(name) ? scrub(name) : name
}

function scrubSpec (name, spec) {
  const req = npa.resolve(name, spec)
  if (req.registry) return spec
  if (req.type === 'git') {
    return 'git+ssh://' + scrub(spec)
  } else if (req.type === 'remote') {
    return 'https://' + scrub(spec)
  } else if (req.type === 'directory') {
    return 'file:' + scrub(spec)
  } else if (req.type === 'file') {
    return 'file:' + scrub(spec) + '.tar'
  } else {
    return scrub(spec)
  }
}

function scrub (value) {
  return ssri.fromData(runId + ' ' + value, {algorithms: ['sha256']}).hexDigest()
}

function generateMetadata () {
  const meta = {}
  meta.npm_version = npm.version
  meta.node_version = process.version
  meta.platform = process.platform
  meta.node_env = process.env.NODE_ENV

  return Promise.resolve(meta)
}
/*
  const head = path.resolve(npm.prefix, '.git/HEAD')
  return readFile(head, 'utf8').then((head) => {
    if (!head.match(/^ref: /)) {
      meta.commit_hash = head.trim()
      return
    }
    const headFile = head.replace(/^ref: /, '').trim()
    meta.branch = headFile.replace(/^refs[/]heads[/]/, '')
    return readFile(path.resolve(npm.prefix, '.git', headFile), 'utf8')
  }).then((commitHash) => {
    meta.commit_hash = commitHash.trim()
    const proc = spawn('git', qw`diff --quiet --exit-code package.json package-lock.json`, {cwd: npm.prefix, stdio: 'ignore'})
    return new Promise((resolve, reject) => {
      proc.once('error', reject)
      proc.on('exit', (code, signal) => {
        if (signal == null) meta.state = code === 0 ? 'clean' : 'dirty'
        resolve()
      })
    })
  }).then(() => meta, () => meta)
*/

function generateFromInstall (tree, diffs, install, remove) {
  const requires = {}
  tree.requires.forEach((pkg) => {
    requires[pkg.package.name] = tree.package.dependencies[pkg.package.name] || tree.package.devDependencies[pkg.package.name] || pkg.package.version
  })

  const auditInstall = (install || []).filter((a) => a.name).map(packageId)
  const auditRemove = (remove || []).filter((a) => a.name).map(packageId)
  const auditDiffs = {}
  diffs.forEach((action) => {
    const mutation = action[0]
    const child = action[1]
    if (mutation !== 'add' && mutation !== 'update' && mutation !== 'remove') return
    if (!auditDiffs[mutation]) auditDiffs[mutation] = []
    if (mutation === 'add') {
      auditDiffs[mutation].push({location: child.location})
    } else if (mutation === 'update') {
      auditDiffs[mutation].push({location: child.location, previous: packageId(child.oldPkg)})
    } else if (mutation === 'remove') {
      auditDiffs[mutation].push({previous: packageId(child)})
    }
  })

  return generate(treeToShrinkwrap(tree), requires, auditDiffs, auditInstall, auditRemove)
}
