import path from 'path';

import addLeadingSlash from '../utils/addLeadingSlash';
import createPackageURL from '../utils/createPackageURL';
import createSearch from '../utils/createSearch';
import fetchNpmPackage from '../utils/fetchNpmPackage';
import getIntegrity from '../utils/getIntegrity';
import getContentType from '../utils/getContentType';

function indexRedirect(req, res, entry) {
  // Redirect to the index file so relative imports
  // resolve correctly.
  res
    .set({
      'Cache-Control': 'public, max-age=31536000', // 1 year
      'Cache-Tag': 'redirect, index-redirect'
    })
    .redirect(
      302,
      createPackageURL(
        req.packageName,
        req.packageVersion,
        addLeadingSlash(entry.name),
        createSearch(req.query)
      )
    );
}

function stripLeadingSegment(name) {
  return name.replace(/^[^/]+\/?/, '');
}

function searchEntries(tarballStream, entryName, wantsIndex) {
  return new Promise((resolve, reject) => {
    const entries = {};
    let foundEntry = null;

    if (entryName === '') {
      foundEntry = entries[''] = { name: '', type: 'directory' };
    }

    tarballStream
      .on('error', reject)
      .on('finish', () => resolve({ entries, foundEntry }))
      .on('entry', (header, stream, next) => {
        const entry = {
          // Most packages have header names that look like `package/index.js`
          // so we shorten that to just `index.js` here. A few packages use a
          // prefix other than `package/`. e.g. the firebase package uses the
          // `firebase_npm/` prefix. So we just strip the first dir name.
          name: stripLeadingSegment(header.name),
          type: header.type
        };

        // We are only interested in files that match the entryName.
        if (entry.type !== 'file' || entry.name.indexOf(entryName) !== 0) {
          stream.resume();
          stream.on('end', next);
          return;
        }

        entries[entry.name] = entry;

        // Dynamically create "directory" entries for all directories
        // that are in this file's path. Some tarballs omit these entries
        // for some reason, so this is the brute force method.
        let dirname = path.dirname(entry.name);
        while (dirname !== '.') {
          const directoryEntry = { name: dirname, type: 'directory' };

          if (!entries[dirname]) {
            entries[dirname] = directoryEntry;

            if (directoryEntry.name === entryName) {
              foundEntry = directoryEntry;
            }
          }

          dirname = path.dirname(dirname);
        }

        // Set the foundEntry variable if this entry name
        // matches exactly or if it's an index.html file
        // and the client wants HTML.
        if (
          entry.name === entryName ||
          // Allow accessing e.g. `/index.js` or `/index.json` using
          // `/index` for compatibility with CommonJS
          (!wantsIndex && entry.name === `${entryName}.js`) ||
          (!wantsIndex && entry.name === `${entryName}.json`)
        ) {
          foundEntry = entry;
        }

        const chunks = [];

        stream
          .on('data', chunk => chunks.push(chunk))
          .on('end', () => {
            const content = Buffer.concat(chunks);

            // Set some extra properties for files that we will
            // need to serve them and for ?meta listings.
            entry.contentType = getContentType(entry.name);
            entry.integrity = getIntegrity(content);
            entry.lastModified = header.mtime.toUTCString();
            entry.size = content.length;

            // Set the content only for the foundEntry and
            // discard the buffer for all others.
            if (entry === foundEntry) {
              entry.content = content;
            }

            next();
          });
      });
  });
}

const leadingSlash = /^\//;
const trailingSlash = /\/$/;
const multipleSlash = /\/\/+/;

/**
 * Fetch and search the archive to try and find the requested file.
 * Redirect to the "index" file if a directory was requested.
 */
export default function findFile(req, res, next) {
  fetchNpmPackage(req.packageConfig).then(tarballStream => {
    const entryName = req.filename
      .replace(multipleSlash, '/')
      .replace(trailingSlash, '')
      .replace(leadingSlash, '');
    const wantsIndex = trailingSlash.test(req.filename);

    searchEntries(tarballStream, entryName, wantsIndex).then(
      ({ entries, foundEntry }) => {
        if (!foundEntry) {
          return res
            .status(404)
            .type('text')
            .send(`Cannot find "${req.filename}" in ${req.packageSpec}`);
        }

        // If the foundEntry is a directory and there is no trailing slash
        // on the request path, we need to redirect to some "index" file
        // inside that directory. This is so our URLs work in a similar way
        // to require("lib") in node where it searches for `lib/index.js`
        // and `lib/index.json` when `lib` is a directory.
        if (foundEntry.type === 'directory' && !wantsIndex) {
          const indexEntry =
            entries[path.join(entryName, 'index.js')] ||
            entries[path.join(entryName, 'index.json')];

          if (indexEntry && indexEntry.type === 'file') {
            return indexRedirect(req, res, indexEntry);
          } else {
            return res
              .status(404)
              .type('text')
              .send(
                `Cannot find an index in "${req.filename}" in ${
                  req.packageSpec
                }`
              );
          }
        }

        req.entries = entries;
        req.entry = foundEntry;

        next();
      }
    );
  });
}
