text: const log = require('npmlog')
  const npa = require('npm-package-arg')
  const regFetch = require('npm-registry-fetch')
  const semver = require('semver')
  
  const otplease = require('./utils/otplease.js')
  const readPackageName = require('./utils/read-package-name.js')
  const BaseCommand = require('./base-command.js')
  
  class DistTag extends BaseCommand {
    static get description () {
      return 'Modify package distribution tags'
    }
  
    /* istanbul ignore next - see test/lib/load-all-commands.js */
    static get params () {
      return ['workspace', 'workspaces', 'include-workspace-root']
    }
  
    /* istanbul ignore next - see test/lib/load-all-commands.js */
    static get name () {
      return 'dist-tag'
    }
  
    /* istanbul ignore next - see test/lib/load-all-commands.js */
    static get usage () {
      return [
        'add <pkg>@<version> [<tag>]',
        'rm <pkg> <tag>',
        'ls [<pkg>]',
      ]
    }
  
    async completion (opts) {
      const argv = opts.conf.argv.remain
      if (argv.length === 2)
        return ['add', 'rm', 'ls']
  
      switch (argv[2]) {
        default:
          return []
      }
    }
  
    exec (args, cb) {
      this.distTag(args).then(() => cb()).catch(cb)
    }
  
    async distTag ([cmdName, pkg, tag]) {
      const opts = this.npm.flatOptions
  
      if (['add', 'a', 'set', 's'].includes(cmdName))
        return this.add(pkg, tag, opts)
  
      if (['rm', 'r', 'del', 'd', 'remove'].includes(cmdName))
        return this.remove(pkg, tag, opts)
  
      if (['ls', 'l', 'sl', 'list'].includes(cmdName))
        return this.list(pkg, opts)
  
      if (!pkg) {
        // when only using the pkg name the default behavior
        // should be listing the existing tags
        return this.list(cmdName, opts)
      } else
        throw this.usageError()
    }
  
    execWorkspaces (args, filters, cb) {
      this.distTagWorkspaces(args, filters).then(() => cb()).catch(cb)
    }
  
    async distTagWorkspaces ([cmdName, pkg, tag], filters) {
      // cmdName is some form of list
      // pkg is one of:
      // - unset
      // - .
      // - .@version
      if (['ls', 'l', 'sl', 'list'].includes(cmdName) && (!pkg || pkg === '.' || /^\.@/.test(pkg)))
        return this.listWorkspaces(filters)
  
      // pkg is unset
      // cmdName is one of:
      // - unset
      // - .
      // - .@version
      if (!pkg && (!cmdName || cmdName === '.' || /^\.@/.test(cmdName)))
        return this.listWorkspaces(filters)
  
      // anything else is just a regular dist-tag command
      // so we fallback to the non-workspaces implementation
      log.warn('Ignoring workspaces for specified package')
      return this.distTag([cmdName, pkg, tag])
    }
  
    async add (spec, tag, opts) {
      spec = npa(spec || '')
      const version = spec.rawSpec
      const defaultTag = tag || this.npm.config.get('tag')
  
      log.verbose('dist-tag add', defaultTag, 'to', spec.name + '@' + version)
  
      if (!spec.name || !version || !defaultTag)
        throw this.usageError()
  
      const t = defaultTag.trim()
  
      if (semver.validRange(t))
        throw new Error('Tag name must not be a valid SemVer range: ' + t)
  
      const tags = await this.fetchTags(spec, opts)
      if (tags[t] === version) {
        log.warn('dist-tag add', t, 'is already set to version', version)
        return
      }
      tags[t] = version
      const url =
        `/-/package/${spec.escapedName}/dist-tags/${encodeURIComponent(t)}`
      const reqOpts = {
        ...opts,
        method: 'PUT',
        body: JSON.stringify(version),
        headers: {
          'content-type': 'application/json',
        },
        spec,
      }
      await otplease(reqOpts, reqOpts => regFetch(url, reqOpts))
      this.npm.output(`+${t}: ${spec.name}@${version}`)
    }
  
    async remove (spec, tag, opts) {
      spec = npa(spec || '')
      log.verbose('dist-tag del', tag, 'from', spec.name)
  
      if (!spec.name)
        throw this.usageError()
  
      const tags = await this.fetchTags(spec, opts)
      if (!tags[tag]) {
        log.info('dist-tag del', tag, 'is not a dist-tag on', spec.name)
        throw new Error(tag + ' is not a dist-tag on ' + spec.name)
      }
      const version = tags[tag]
      delete tags[tag]
      const url =
        `/-/package/${spec.escapedName}/dist-tags/${encodeURIComponent(tag)}`
      const reqOpts = {
        ...opts,
        method: 'DELETE',
        spec,
      }
      await otplease(reqOpts, reqOpts => regFetch(url, reqOpts))
      this.npm.output(`-${tag}: ${spec.name}@${version}`)
    }
  
    async list (spec, opts) {
      if (!spec) {
        if (this.npm.config.get('global'))
          throw this.usageError()
        const pkg = await readPackageName(this.npm.prefix)
        if (!pkg)
          throw this.usageError()
  
        return this.list(pkg, opts)
      }
      spec = npa(spec)
  
      try {
        const tags = await this.fetchTags(spec, opts)
        const msg =
          Object.keys(tags).map(k => `${k}: ${tags[k]}`).sort().join('\n')
        this.npm.output(msg)
        return tags
      } catch (err) {
        log.error('dist-tag ls', "Couldn't get dist-tag data for", spec)
        throw err
      }
    }
  
    async listWorkspaces (filters) {
      await this.setWorkspaces(filters)
  
      for (const name of this.workspaceNames) {
        try {
          this.npm.output(`${name}:`)
          await this.list(npa(name), this.npm.flatOptions)
        } catch (err) {
          // set the exitCode directly, but ignore the error
          // since it will have already been logged by this.list()
          process.exitCode = 1
        }
      }
    }
  
    async fetchTags (spec, opts) {
      const data = await regFetch.json(
        `/-/package/${spec.escapedName}/dist-tags`,
        { ...opts, 'prefer-online': true, spec }
      )
      if (data && typeof data === 'object')
        delete data._etag
      if (!data || !Object.keys(data).length)
        throw new Error('No dist-tags found for ' + spec.name)
  
      return data
    }
  }
  module.exports = DistTag
  