const path = require("path");

const addLeadingSlash = require("../utils/addLeadingSlash");
const createPackageURL = require("../utils/createPackageURL");
const createSearch = require("../utils/createSearch");
const fetchNpmPackage = require("../utils/fetchNpmPackage");
const getIntegrity = require("../utils/getIntegrity");
const getContentType = require("../utils/getContentType");

function indexRedirect(req, res, entry) {
  // Redirect to the index file so relative imports
  // resolve correctly.
  // TODO: increase the max-age?
  res
    .set({
      "Cache-Control": "public,max-age=60",
      "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, wantsHTML) {
  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 ||
          (wantsHTML && entry.name === entryName + "/index.html")
        ) {
          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 = /\/$/;

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

    searchEntries(tarballStream, entryName, wantsHTML).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" && !wantsHTML) {
          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();
      }
    );
  });
}

module.exports = findFile;