import url from 'url';
import https from 'https';
import gunzip from 'gunzip-maybe';
import tar from 'tar-stream';
import LRUCache from 'lru-cache';

import debug from './debug.js';
import bufferStream from './bufferStream.js';

const npmRegistryURL =
  process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org';

const agent = new https.Agent({
  keepAlive: true
});

function parseJSON(res) {
  return bufferStream(res).then(JSON.parse);
}

export function fetchPackageInfo(packageName) {
  return new Promise((accept, reject) => {
    const encodedPackageName =
      packageName.charAt(0) === '@'
        ? `@${encodeURIComponent(packageName.substring(1))}`
        : encodeURIComponent(packageName);

    const infoURL = `${npmRegistryURL}/${encodedPackageName}`;

    debug('Fetching package info for %s from %s', packageName, infoURL);

    const { hostname, pathname } = url.parse(infoURL);
    const options = {
      agent: agent,
      hostname: hostname,
      path: pathname,
      headers: {
        Accept: 'application/json'
      }
    };

    https
      .get(options, async res => {
        if (res.statusCode === 200) {
          accept(parseJSON(res));
        } else if (res.statusCode === 404) {
          accept(null);
        } else {
          const data = await bufferStream(res);
          const content = data.toString('utf-8');
          const error = new Error(
            `Failed to fetch info for ${packageName}\nstatus: ${res.statusCode}\ndata: ${content}`
          );

          reject(error);
        }
      })
      .on('error', reject);
  });
}

export function fetchPackage(packageConfig) {
  return new Promise((accept, reject) => {
    const tarballURL = packageConfig.dist.tarball;

    debug('Fetching package for %s from %s', packageConfig.name, tarballURL);

    const { hostname, pathname } = url.parse(tarballURL);
    const options = {
      agent: agent,
      hostname: hostname,
      path: pathname
    };

    https
      .get(options, async res => {
        if (res.statusCode === 200) {
          accept(res.pipe(gunzip()).pipe(tar.extract()));
        } else {
          const data = await bufferStream(res);
          const spec = `${packageConfig.name}@${packageConfig.version}`;
          const content = data.toString('utf-8');
          const error = new Error(
            `Failed to fetch tarball for ${spec}\nstatus: ${res.statusCode}\ndata: ${content}`
          );

          reject(error);
        }
      })
      .on('error', reject);
  });
}

const oneMegabyte = 1024 * 1024;
const oneSecond = 1000;
const oneMinute = oneSecond * 60;

const cache = new LRUCache({
  max: oneMegabyte * 40,
  length: Buffer.byteLength,
  maxAge: oneSecond
});

const notFound = '';

export async function getPackageInfo(packageName) {
  const key = `npmPackageInfo-${packageName}`;
  const value = cache.get(key);

  if (value != null) {
    return value === notFound ? null : JSON.parse(value);
  }

  const info = await fetchPackageInfo(packageName);

  if (info == null) {
    // Cache 404s for 5 minutes. This prevents us from making
    // unnecessary requests to the registry for bad package names.
    // In the worst case, a brand new package's info will be
    // available within 5 minutes.
    cache.set(key, notFound, oneMinute * 5);
    return null;
  }

  // Cache valid package info for 1 minute. In the worst case,
  // new versions won't be available for 1 minute.
  cache.set(key, JSON.stringify(info), oneMinute);
  return info;
}