const Fetcher = require('./fetcher.js')
const RemoteFetcher = require('./remote.js')
const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
const pacoteVersion = require('../package.json').version
const removeTrailingSlashes = require('./util/trailing-slashes.js')
const npa = require('npm-package-arg')
const rpj = require('read-package-json-fast')
const pickManifest = require('npm-pick-manifest')
const ssri = require('ssri')

// Corgis are cute. 🐕🐶
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
const fullDoc = 'application/json'

const fetch = require('npm-registry-fetch')

// TODO: memoize reg requests, so we don't even have to check cache

const _headers = Symbol('_headers')
class RegistryFetcher extends Fetcher {
  constructor (spec, opts) {
    super(spec, opts)

    // you usually don't want to fetch the same packument multiple times in
    // the span of a given script or command, no matter how many pacote calls
    // are made, so this lets us avoid doing that.  It's only relevant for
    // registry fetchers, because other types simulate their packument from
    // the manifest, which they memoize on this.package, so it's very cheap
    // already.
    this.packumentCache = this.opts.packumentCache || null

    // handle case when npm-package-arg guesses wrong.
    if (this.spec.type === 'tag' &&
        this.spec.rawSpec === '' &&
        this.defaultTag !== 'latest') {
      this.spec = npa(`${this.spec.name}@${this.defaultTag}`)
    }
    this.registry = fetch.pickRegistry(spec, opts)
    this.packumentUrl = removeTrailingSlashes(this.registry) + '/' +
      this.spec.escapedName

    // XXX pacote <=9 has some logic to ignore opts.resolved if
    // the resolved URL doesn't go to the same registry.
    // Consider reproducing that here, to throw away this.resolved
    // in that case.
  }

  resolve () {
    if (this.resolved) {
      return Promise.resolve(this.resolved)
    }

    // fetching the manifest sets resolved and (usually) integrity
    return this.manifest().then(() => {
      if (this.resolved) {
        return this.resolved
      }

      throw Object.assign(
        new Error('Invalid package manifest: no `dist.tarball` field'),
        { package: this.spec.toString() }
      )
    })
  }

  [_headers] () {
    return {
      // npm will override UA, but ensure that we always send *something*
      'user-agent': this.opts.userAgent ||
        `pacote/${pacoteVersion} node/${process.version}`,
      ...(this.opts.headers || {}),
      'pacote-version': pacoteVersion,
      'pacote-req-type': 'packument',
      'pacote-pkg-id': `registry:${this.spec.name}`,
      accept: this.fullMetadata ? fullDoc : corgiDoc,
    }
  }

  async packument () {
    // note this might be either an in-flight promise for a request,
    // or the actual packument, but we never want to make more than
    // one request at a time for the same thing regardless.
    if (this.packumentCache && this.packumentCache.has(this.packumentUrl)) {
      return this.packumentCache.get(this.packumentUrl)
    }

    // npm-registry-fetch the packument
    // set the appropriate header for corgis if fullMetadata isn't set
    // return the res.json() promise
    const p = fetch(this.packumentUrl, {
      ...this.opts,
      headers: this[_headers](),
      spec: this.spec,
      // never check integrity for packuments themselves
      integrity: null,
    }).then(res => res.json().then(packument => {
      packument._cached = res.headers.has('x-local-cache')
      packument._contentLength = +res.headers.get('content-length')
      if (this.packumentCache) {
        this.packumentCache.set(this.packumentUrl, packument)
      }
      return packument
    })).catch(er => {
      if (this.packumentCache) {
        this.packumentCache.delete(this.packumentUrl)
      }
      if (er.code === 'E404' && !this.fullMetadata) {
        // possible that corgis are not supported by this registry
        this.fullMetadata = true
        return this.packument()
      }
      throw er
    })
    if (this.packumentCache) {
      this.packumentCache.set(this.packumentUrl, p)
    }
    return p
  }

  manifest () {
    if (this.package) {
      return Promise.resolve(this.package)
    }

    return this.packument()
      .then(packument => pickManifest(packument, this.spec.fetchSpec, {
        ...this.opts,
        defaultTag: this.defaultTag,
        before: this.before,
      }) /* XXX add ETARGET and E403 revalidation of cached packuments here */)
      .then(mani => {
        // add _resolved and _integrity from dist object
        const { dist } = mani
        if (dist) {
          this.resolved = mani._resolved = dist.tarball
          mani._from = this.from
          const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
            : dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', { ...this.opts })
            : null
          if (distIntegrity) {
            if (!this.integrity) {
              this.integrity = distIntegrity
            } else if (!this.integrity.match(distIntegrity)) {
              // only bork if they have algos in common.
              // otherwise we end up breaking if we have saved a sha512
              // previously for the tarball, but the manifest only
              // provides a sha1, which is possible for older publishes.
              // Otherwise, this is almost certainly a case of holding it
              // wrong, and will result in weird or insecure behavior
              // later on when building package tree.
              for (const algo of Object.keys(this.integrity)) {
                if (distIntegrity[algo]) {
                  throw Object.assign(new Error(
                    `Integrity checksum failed when using ${algo}: ` +
                    `wanted ${this.integrity} but got ${distIntegrity}.`
                  ), { code: 'EINTEGRITY' })
                }
              }
              // made it this far, the integrity is worthwhile.  accept it.
              // the setter here will take care of merging it into what we
              // already had.
              this.integrity = distIntegrity
            }
          }
        }
        if (this.integrity) {
          mani._integrity = String(this.integrity)
        }
        this.package = rpj.normalize(mani)
        return this.package
      })
  }

  [_tarballFromResolved] () {
    // we use a RemoteFetcher to get the actual tarball stream
    return new RemoteFetcher(this.resolved, {
      ...this.opts,
      resolved: this.resolved,
      pkgid: `registry:${this.spec.name}@${this.resolved}`,
    })[_tarballFromResolved]()
  }

  get types () {
    return [
      'tag',
      'version',
      'range',
    ]
  }
}
module.exports = RegistryFetcher
