// These commands deal with aggregating local package data with the information
// contained in the Meteor Package Server. They also deal with presenting this
// to the user in various human or machine-readable ways.
var _ = require('underscore');
var buildmessage = require('../utils/buildmessage.js');
var catalog = require('../packaging/catalog/catalog.js');
var Console = require('../console/console.js').Console;
var files = require('../fs/files');
import { loadIsopackage } from '../tool-env/isopackets.js';
var main = require('./main.js');
var packageVersionParser = require('../packaging/package-version-parser.js');
var projectContextModule = require('../project-context.js');
var utils = require('../utils/utils.js');
var catalogUtils = require('../packaging/catalog/catalog-utils.js');
var compiler = require('../isobuild/compiler.js');

// We want these queries to be relatively fast, so we will only refresh the
// catalog if it is > 15 minutes old
var DEFAULT_MAX_AGE_MS = 15 * 60 * 1000;

// Maximum number of recent versions of a package or a release that we should
// return to the user, unless a more complete mode is requested.
var MAX_RECENT_VERSIONS = 5;

// XXX: Remove this if/when we do a Troposphere migration to backfill release
// version publication times.
// Estimate the publication date for a release. Since we have failed to keep
// track of publication times of release versions in the past, we will try to
// guess that the release was published at the same time as the tool.
var getReleaseVersionPublishedOn = async function (versionRecord) {
  if (versionRecord.published) {
    return new Date(versionRecord.published);
  }
  // We don't know when the release was published. Luckily, since there is no
  // way to use the tool outside of a release, and we always change the tool
  // between releases, it is a good bet that the release was published on the
  // same day as the tool.
  var toolPackage = versionRecord.tool.split('@');
  var toolName = toolPackage[0];
  var toolVersion = toolPackage[1];
  var toolRecord = await catalog.official.getVersion(toolName, toolVersion);
  if (! toolRecord || ! toolRecord.published) {
    return null;
  }
  return new Date(toolRecord.published);
};

// Processes information about the versions that we hid. Returns a brief
// human-friendly string listing the reasons why some versions of the package
// were not shown.
var formatHiddenVersions = function (hiddenVersions, oldestShownVersion) {
  // An array of strings, listing the reasons why some versions were hidden.
  var reasons = [];
  // Use our information about hidden versions to figure what reasons we
  // actually want to return to the user.
  if (! oldestShownVersion) {
    // We did not show any versions, so presumably all existing versions of
    // this package are either unmigrated or pre-release versions.
    if (hiddenVersions.lastUnmigrated) {
      reasons.push("unmigrated");
    }
    if (hiddenVersions.lastPreRelease) {
      reasons.push("pre-release");
    }
  } else {
    // If the oldest version on record is older than the oldest shown
    // version, then it was hidden due to MAX_RECENT_VERSION number. (It
    // might also be hidden because it is a pre-release or unmigrated, but
    // age takes priority).
    if (packageVersionParser.lessThan(
        hiddenVersions.oldestVersion, oldestShownVersion)) {
      reasons.push("older");
    }

    // If the latest unmigrated/pre-release version is older than the oldest
    // version that we are showing, then we don't care about it. If it is
    // younger, we need to tell the user.
    //
    // It is certainly possible that, even though a pre-release version is older
    // than the oldest version that we are showing, but under the limit for the
    // MAX_RECENT_VERSIONS. So, in that case, we are eliding that version
    // because it is a pre-release, not because of age. It is still,
    // technically, an 'older' version though, and that explanation is more
    // intuitive.
    if (hiddenVersions.lastPreRelease &&
        packageVersionParser.lessThan(
          oldestShownVersion, hiddenVersions.lastPreRelease)) {
      reasons.push("pre-release");
    }
    if (hiddenVersions.lastUnmigrated &&
        packageVersionParser.lessThan(
          oldestShownVersion, hiddenVersions.lastUnmigrated)) {
      reasons.push("unmigrated");
    }
  }

  // Now, we will aggregate the reasons into a human-readable string.
  if (reasons.length === 1) {
    return reasons[0];
  } else if (reasons.length === 2) {
    // There is no oxford comma if only listing two objects
    return reasons[0] + " and " + reasons[1];
  } else if (reasons.length > 2)  {
    return reasons.slice(0, -1).join(", ") + ", and " + _.last(reasons);
  } else {
    // Did we not figure out anything to write? Did something else go wrong?
    // This should never happen, but if it does, recover by omitting
    // information.
    return "Some";
  }
};

// Converts an object to an EJSON string with the right spacing.
async function formatEJSON(data) {
  const { EJSON } = await loadIsopackage('ejson');
  return EJSON.stringify(data, { indent: true }) + "\n";
}

// Takes in a string and pads it with whitespace to the length of the longest
// possible date string.
var padLongformDate = function (dateStr) {
  var numSpaces = utils.maxDateLength - dateStr.length;
  return dateStr + Array(numSpaces + 1).join(' ');
};

// In order to get access to local package data, we need to create a local
// package catalog. The best way to do that is to create a temporary
// ProjectContext and let it handle catalog initialization. When we do, we need
// to make sure that it is aware of all the local packages that we might care
// about.
//
// This function returns such a ProjectContext, and takes in the following
// options:
//  - appDir: If we are running in the context of an app, this will contain the
//    root of the app. We want to make sure to grab the data from the app's
//    local packages.
//  - packageDir: If we are running in a package directory, this will contain
//    the source root of that package. If we are running from inside a package,
//    we want that package to show up in our results.
var getTempContext = async function (options) {
  var projectContext;
  // If we are running in an app, we will use it to create a
  // (mostly immutable) projectContext.
  if (options.appDir) {
    projectContext = new projectContextModule.ProjectContext({
      projectDir: options.appDir
    });
  } else {
    // We're not in an app, so we will create a temporary app and use it to load
    // the local catalog. If a local packageDir exists, include it manually.
    var currentPackageDir = options.packageDir ? [options.packageDir] : [];
    var tempProjectDir = files.mkdtemp('meteor-show');
    projectContext = new projectContextModule.ProjectContext({
      projectDir: tempProjectDir,
      explicitlyAddedLocalPackageDirs: currentPackageDir
    });
  }

  // It is possible that we can't process package.js files in our local packages
  // and have to exit early. This is unfortunate, but we can't search local
  // packages if we can't read them. If this turns out to be a frequent problem,
  // we can give a warning, instead of failing in the future. For now, we want
  // to err on the side of consistency.
  await main.captureAndExit("=> Errors while reading local packages:", function () {
    return projectContext.initializeCatalog();
  });

  return projectContext;
};

// Print an error message if the user asks about an unknown item.
var itemNotFound = function (item) {
  Console.error(item + ": not found");
  catalogUtils.explainIfRefreshFailed();
  return 1;
};

// This is a base class for storing package fields that require some processing
// to store and display correctly.
//
// Do NOT initialize this class by itself -- use one of the classes that
// inherits from it.
var BasePkgDatum = function () {
  var self = this;
  self.data = null;
};
Object.assign(BasePkgDatum.prototype, {
  // Throws if data has not been initialized.
  _checkInitialized: function () {
    var self = this;
    if (self.data === null) {
      throw new Error("do not use the BasePkgDatum class by itself");
    }
  },
  // Returns true if this class does not contain any exports.
  isEmpty : function () {
    var self = this;
    self._checkInitialized();
    return _.isEmpty(self.data);
  },
  // Get exports as a raw object.
  getObject : function () {
    var self = this;
    self._checkInitialized();
    return self.data;
  },
  getConsoleStr : function () {
    var self = this;
    self._checkInitialized();
    return "";
  }
});

// This class stores exports from a given package.
//
// Stores exports for a given package and returns them to the caller in a given
// format. Takes in the raw exports from the package.
var PkgExports = function (pkgExports) {
 var self = this;
 // Process and save the export data.
 self.data = _.map(pkgExports, function (exp) {
    var arches = exp.architectures;
    // Replace 'os' (what we store) with 'server' (what you would put in a
    // package.js file). That's more user friendly, and avoids confusing this
    // with different OS arches used in binary packages.
    if ( arches.indexOf("os") !== -1) {
      arches = _.without(arches, "os");
      arches.push("server");
    }
    // Sort architectures alphabetically.
    arches.sort();
    return { name: exp.name, architectures: arches };
  });
  // Sort exports alphabetically by name.
  self.data =  _.sortBy(self.data, "name");
};
// Extend BasePkgDatum.
PkgExports.prototype = new BasePkgDatum();

Object.assign(PkgExports.prototype, {
  // Convert package exports into a pretty, Console non-wrappable string. If an
  // export is only declared for certain architectures, mentions those
  // architectures in a user-friendly format.
  getConsoleStr: function () {
    var self = this;
    var strExports = _.map(self.data, function (exp) {
      // If this export is valid for all architectures, don't specify
      // architectures here.
      if (exp.architectures.length === compiler.ALL_ARCHES.length) {
        return exp.name;
      }

      // Don't split descriptions of individual pkgExports between lines.
      return Console.noWrap(
        exp.name + " (" + exp.architectures.join(", ") + ")");
    });
    return strExports.join(", ");
  }
});

// This class stores implies from a given package.
//
// Stores implies for a given package and returns them to the caller in a given
// format. Takes in the dependencies from the package.
var PkgImplies = function (pkgDeps) {
  var self = this;
  self.data = [];
  // Go through all the package dependencies. If a dependency has any implied
  // references, add it to the list.
  _.each(pkgDeps, function (ref, name) {
    var architectures = [];
    // We want to select the references that are implied (instead of just used)
    // and save their architectures. Also, we want to replace 'os' with
    // 'server', as with exports.
    _.each(ref.references, function (r) {
      if (! r.implied) {
        return;
      }
      var archName = (r.arch === "os") ? "server" : r.arch;
      architectures.push(archName);
    });
    // Sort architectures alphabetically.
    architectures.sort();
    if (! _.isEmpty(architectures)) {
      self.data.push({ name: name, architectures: architectures });
    }
  });
  // Sort by name.
  self.data =  _.sortBy(self.data, "name");
};

// Extend BasePkgDatum.
PkgImplies.prototype = new BasePkgDatum();

Object.assign(PkgImplies.prototype, {
  // Convert package exports into a pretty, Console non-wrappable string. If an
  // export is only declared for certain architectures, mentions those
  // architectures in a user-friendly format.
  getConsoleStr: function () {
    var self = this;
    var strImplies = _.map(self.data, function (ref) {
      // If an imply is valid for all architectures, don't specify it here.
      if (ref["architectures"].length === compiler.ALL_ARCHES.length) {
        return ref.name;
      }

      // Don't split descriptions of individual implies between lines.
      return Console.noWrap(
        ref.name + " (" + ref.architectures.join(", ") + ")");
    });
    return strImplies.join(", ");
  }
});

// This class stores dependencies from a given package.
//
// Stores dependencies for a given package and returns them to the caller in a given
// format. Takes in the raw dependencies from the package record.
var PkgDependencies = function (pkgDeps) {
  var self = this;
  self.data = _.map(
    // The dependency on 'meteor' was almost certainly added automatically, by
    // Isobuild. Returning this to the user will only cause confusion.
    _.omit(pkgDeps, "meteor"),
    function (dep, depName) {
      // We will only consider this a weak dependency if all of its references
      // are marked as weak.
      var weak = _.every(dep.references, function (ref) {
        return !! ref.weak;
      });
      return {
        name: depName,
        constraint: dep.constraint,
        weak: weak
      };
  });
  // Sort by name.
  self.data =  _.sortBy(self.data, "name");
};

// Extend BasePkgDatum.
PkgDependencies.prototype = new BasePkgDatum();

Object.assign(PkgDependencies.prototype, {
  // Convert package exports into a pretty, Console non-wrappable string. If an
  // export is only declared for certain architectures, mentions those
  // architectures in a user-friendly format.
  getConsoleStr: function () {
    var self = this;
    var strDeps = _.map(self.data, function (dep) {
      var depString = dep.name;
      if (dep.constraint && dep.constraint !== null) {
        depString += "@" + dep.constraint;
      }
      if (dep.weak) {
        depString += " (weak dependency)";
      }
      return Console.noWrap(depString);
    });
    return strDeps.join("\n");
  }
});


// The two classes below collect and print relevant information about Meteor
// packages and Meteor releases, respectively. Specifically, they query the
// official catalog and, if applicable, relevant local sources. They also handle
// the details of printing their data to the screen.
//
// A query class has:
//  - data: an object representing the data it has collected in response to the
//  - query.
//  - a print method, that take options as an argument and prints the results to
//    the terminal.


// This class deals with information related to packages. To deal with local
// packages, it has to interact with the projectContext.
//
// The constructor takes in the following options:
//   - metaRecord: (mandatory) the meta-record for this package from the Packages
//     collection.
//   - projectContext: (mandatory) a projectContext that we can use to look up
//     information on local packages.
//   - version: query for a specific version of this package.
//   - showArchitecturesOS: collect and process data on OS
//     architectures that are available for different versions of this package.
//   - showHiddenVersions: return information about all the versions of the
//     package, including pre-releases and unmigrated versions.
//   - showDependencies: return information about
//     versions' dependencies.
var PackageQuery = function (options) {
  var self = this;

  // This is the record in the packages collection. It contains things like
  // maintainers, and the package homepage.
  self.metaRecord = options.metaRecord;
  self.name = options.metaRecord.name;

  // This argument is required -- we use it to look up data. If it has not been
  // passed in, fail early.
  if (! options.projectContext) {
    throw Error("Missing required argument: projectContext");
  }
  self.projectContext = options.projectContext;
  self.localCatalog = options.projectContext.localCatalog;

  // Processing per-version availability architectures & dependencies is
  // expensive, so we don't do it unless we are asked to.
  self.showArchitecturesOS = options.showArchitecturesOS;
  self.showDependencies = options.showDependencies;

  // We don't want to show pre-releases and un-migrated versions to the user
  // unless they explicitly ask us about it.
  self.showHiddenVersions = options.showHiddenVersions;
  self.version = options.version;
};

Object.assign(PackageQuery.prototype, {
  init: async function() {
    const self = this;
    // Collect the data for this package, including looking up any specific
    // package version that we care about.
    if (self.version) {
      var versionRecord = await self._getVersionRecord(self.version);
      if (! versionRecord) {
        self.data = null;
        return;
      }
      self.data = versionRecord.local ?
          await self._getLocalVersion(versionRecord) :
          await self._getOfficialVersion(versionRecord);
    } else {
      self.data = await self._collectPackageData();
    }
  },
  // Find and return a version record for a given version. Mark the version
  // record as local, if it is a local version of the package.
  _getVersionRecord: async function (version) {
    var self = this;

    // We allow local version to override remote versions in meteor show, so we
    // should start by checking if this is a local version first.
    var versionRecord = await self.localCatalog.getLatestVersion(self.name);

    // If we asked for "local" as the version number, and found any local version
    // at all, we are done.
    if (version === "local") {
      return versionRecord && Object.assign(versionRecord, { local: true });
    }

    // We have a local record, and its version matches the version that we asked
    // for, so we are done.
    if (versionRecord && (versionRecord.version === version)) {
      return Object.assign(versionRecord, { local: true });
    }

    // If we haven't found a local record, or if the local record that we found
    // doesn't match the version that we asked for, then we have to go look in
    // the server catalog.
    versionRecord = await catalog.official.getVersion(self.name, version);
    return versionRecord;
  },
  // Print the query information to screen.
  //
  // options:
  //   - ejson: Don't pretty-print the data. Print a machine-readable ejson
  //     object.
  print: async function (options) {
    var self = this;

    // If we are asking for an EJSON-style output, we will only print out the
    // relevant fields.
    if (options.ejson) {
      Console.rawInfo(await formatEJSON(
        self.data.version ?
          self._generateVersionObject(self.data) :
          self._generatePackageObject(self.data)));
      return;
    }

    // Otherwise, display the information that we have. If we were asking about
    // a specific version, display that. Otherwise, display package metadata in
    // general.
    if (self.data.version) {
      self._displayVersion(self.data);
      return;
    }
    self._displayPackage(self.data);
  },
  // Aggregates data about the package as a whole. Returns an object with the
  // following keys:
  //
  // - name: package name
  // - maintainers: an array of usernames of maintainers
  // - homepage: string homepage
  // - totalVersions: total number of versions that this package has, including
  //   local and hidden versions.
  // - defaultVersion: a default version: use this version to look up
  //   per-version information that is relevant to the package as a whole, such
  //   as git, description,etc.
  // - versions: an array of objects representing versions of this package.
  _collectPackageData: async function () {
    var self = this;
    var data = {
      name: self.metaRecord.name,
      maintainers: _.pluck(self.metaRecord.maintainers, "username"),
      homepage: self.metaRecord.homepage
    };

    // Collect surface information about available versions, starting with the
    // versions available on the server.
    var serverVersionRecords =
          await catalog.official.getSortedVersionRecords(self.name);
    var totalVersions = serverVersionRecords.length;

    // If we are not going to show hidden versions, then we shouldn't waste time
    // on them. Trim the serverVersionRecords array to only have the top
    // MAX_RECENT_VERSIONS migrated, official versions.
    if (! self.showHiddenVersions) {
      // We might have to hide some versions from the user. We want to explain
      // why we hid them. Here is how we are going to explain things -- any
      // versions older than the oldest version that we show, are hidden because
      // of age. If, in the covered time period, there are
      // unmigrated/pre-release versions, then we will mention those  as well.
      //
      // Specifically, while we filter versions, we are going to memorize the
      // most recent version hidden for a specific reason.
      var lastUnmigrated = "";
      var lastPreRelease = "";
      var oldestVersion =
        serverVersionRecords[0] && serverVersionRecords[0].version;
      var filteredVersionRecords =
        _.filter(serverVersionRecords, function (vr) {
          if (vr.unmigrated) {
            lastUnmigrated = vr.version;
            return false;
          }

          if (vr.version.indexOf("-") !== -1) {
            lastPreRelease = vr.version;
            return false;
          }
          return true;
        });
     serverVersionRecords = _.last(filteredVersionRecords, MAX_RECENT_VERSIONS);
     data["hiddenVersions"] = {
       oldestVersion: oldestVersion,
       lastUnmigrated: lastUnmigrated,
       lastPreRelease: lastPreRelease
     };
    };

    // Process the catalog records into our preferred format, and look up any
    // other per-version information that we might need.
    data["versions"] = [];
    for (const versionRecord of serverVersionRecords) {
      data["versions"].push(await self._getOfficialVersion(versionRecord))
    }

    // The local version doesn't count against the version limit. Look up relevant
    // information about the local version.
    var localVersion = await self.localCatalog.getLatestVersion(self.name);
    var local;
    if (localVersion) {
      local = await self._getLocalVersion(localVersion);
      data["versions"].push(local);
      totalVersions++;
    }

    // Record the total number of versions, including the ones we hid from the
    // user.
    data["totalVersions"] = totalVersions;

    // Some per-version information gets displayed with the rest of the package
    // information.  We want to use the right version for that. (We don't want
    // to display data from unofficial or un-migrated versions just because they
    // are recent.)
    if (local) {
      data["defaultVersion"] = {
        version: "local",
        summary: local.summary,
        description: local.description,
        git: local.git,
        implies: local.implies,
        exports: local.exports,
        deprecated: local.deprecated,
        deprecatedMessage: local.deprecatedMessage
      };
    } else {
      var mainlineRecord = await catalog.official.getLatestMainlineVersion(self.name);
      if (mainlineRecord) {
        var pkgExports = new PkgExports(mainlineRecord.exports);
        var implies = new PkgImplies(mainlineRecord.dependencies);
        data["defaultVersion"] = {
          version: mainlineRecord.version,
          summary: mainlineRecord.description,
          description: mainlineRecord.longDescription,
          git: mainlineRecord.git,
          exports: pkgExports,
          implies: implies,
          deprecated: mainlineRecord.deprecated,
          deprecatedMessage: mainlineRecord.deprecatedMessage
        };
      } else {
        data["defaultVersion"] = _.last(data.versions);
      }
    }
    return data;
  },
  // Takes in a version record from the official catalog and looks up extra
  // information that's relevant to this PackageQuery.
  //
  // - name: package Name
  // - version: package version
  // - summary: version summary/short description (from Package.describe)
  // - description: long-form description (from the README.md)
  // - publishedBy: username of the publisher
  // - publishedOn: date of publication
  // - git: git URL for this version
  // - installed: true if the package exists in warehouse, and is therefore
  //   available for use offline.
  // - architectures: (optional) if self.showArchitecturesOS is true, returns an
  //   array of system architectures for which that package is available.
  // - dependencies: (optional) if self.showDependencies is true, return an
  //   array of objects denoting that package's dependencies. The objects have
  //   the following keys:
  //     - packageName: name of the dependency
  //     - constraint: constraint for that dependency
  //     - weak: true if this is a weak dependency.
  _getOfficialVersion: async function (versionRecord) {
    var self = this;
    var version = versionRecord.version;
    var name = self.name;
    var data = {
      name: name,
      version: version,
      summary: versionRecord.description,
      description: versionRecord.longDescription,
      publishedBy:
      versionRecord.publishedBy && versionRecord.publishedBy.username,
      publishedOn: new Date(versionRecord.published),
      git: versionRecord.git,
      exports: versionRecord.exports,
      deprecated: versionRecord.deprecated,
      deprecatedMessage: versionRecord.deprecatedMessage
    };

    // Get the export and imply data, if the record has any.
    data["exports"] = new PkgExports(versionRecord.exports);
    data["implies"] = new PkgImplies(versionRecord.dependencies);

    // Processing and formatting architectures takes time, so we don't want to
    // do this if we don't have to.
    if (self.showArchitecturesOS) {
      var allBuilds = await catalog.official.getAllBuilds(self.name, version);
      var architectures = _.map(allBuilds, function (build) {
        if (! build['buildArchitectures']) {
          return "unknown";
        }
        var archOS =
          _.filter(build.buildArchitectures.split('+'), function (arch) {
             return ( arch !== "web.browser" ) && ( arch !== "web.cordova" );
        });
        // At this point, you can only have OS arch at a time per-build.
        return archOS[0];
      });
      data["architecturesOS"] = architectures;
    }

    // Processing and formatting dependencies also takes time, so we would
    // rather not do it if we don't have to.
    if (self.showDependencies) {
      data["dependencies"] = new PkgDependencies(versionRecord.dependencies);
    }

    // We want to figure out if we have already downloaded this package, and,
    // therefore, can use it offline.
    var tropohouse = self.projectContext.tropohouse;
    try {
      data["installed"] = tropohouse.installed({
        packageName: name,
        version: version
      });
    } catch (e) {
      // Sometimes, we might be unable to determine if the package is installed
      // -- maybe we don't have access to the directory, or there is some sort
      // of disk corruption. This might only extend to one version, so it would
      // be awkward to fail 'meteor show' altogether. Print an error message (if
      // it is a permissions error, for example, that's something the user might
      // want to know), but don't throw.
      Console.printError(e);
      data["installed"] = false;
    }
    return data;
  },

  // Takes in a version record from the local catalog and looks up extra
  // information that's relevant to this PackageQuery. Returns an object with
  // the following keys.
  //
  // - name: package Name
  // - version: package version
  // - summary: version summary/short description (from Package.describe)
  // - description: long-form description (from the README.md)
  // - git: git URL for this version
  // - local: always true (denotes that this is a local package).
  // - directory: source directory of this package.
  // - dependencies: (optional) if self.showDependencies is true, return an
  //   array of objects denoting that package's dependencies. The objects have
  //   the following keys:
  //     - packageName: name of the dependency
  //     - constraint: constraint for that dependency
  //     - weak: true if this is a weak dependency.
  _getLocalVersion: async function (localRecord) {
    var self = this;
    var data =  {
      name: self.name,
      summary: localRecord.description,
      git: localRecord.git,
      local: true,
      deprecated: localRecord.deprecated,
      deprecatedMessage: localRecord.deprecatedMessage
    };

    // Get the source directory.
    var packageSource = self.localCatalog.getPackageSource(self.name);
    data["directory"] = packageSource.sourceRoot;

    // Get the exports.
    data["exports"] = new PkgExports(packageSource.getExports());
    data["implies"] = new PkgImplies(localRecord.dependencies);

    // If the version was not explicitly set by the user, the catalog backfills
    // a placeholder version for the constraint solver. We don't want to show
    // that version to the user.
    data["version"] = packageSource.versionExplicitlyProvided ?
      localRecord.version : "local";

    // Processing dependencies takes time, and we don't want to do it if we
    // don't have to.
    if (self.showDependencies) {
      data["dependencies"] = new PkgDependencies(localRecord.dependencies);
    }

    var readmeInfo;
    await main.captureAndExit(
      "=> Errors while reading local packages:",
      "reading " + data["directory"],
       function () {
        readmeInfo = packageSource.processReadme();
    });
    if (readmeInfo) {
      data["description"] = readmeInfo.excerpt;
    }
    return data;
  },
  // Displays version information from this PackageQuery to the terminal in a
  // human-friendly format. Takes in an object that contains some, but not all,
  // of the following keys:
  //
  // - name: (mandatory) package Name
  // - version: (mandatory) package version
  // - summary: version summary/short description (from Package.describe)
  // - publishedBy: username of the publisher
  // - publishedOn: date of publication
  // - description: long-form description (from the README.md)
  // - git: git URL for this version.
  // - local: true for a local version of a package.
  // - directory: source directory of this package.
  // - installed: true if the package exists in warehouse, and is therefore
  //   available for use offline.
  // - architectures: if self.showArchitecturesOS is true, returns an
  //   array of system architectures for which that package is available.
  // - exports: a PkgExports object, representing package exports.
  // - exports: a PkgImplies object, representing package implies.
  // - dependencies: a PkgDependencies object, representing dependencies.
  // - deprecated: If the package has been deprecated or not.
  // - deprecatedMessage: Optional message from the deprecated package for the users.
  _displayVersion: function (data) {
    var self = this;
    Console.info(
        data.name + "@" + data.version,
        Console.options({ bulletPoint: "Package: " }));
    if (data.directory) {
      Console.info("Directory: " + Console.path(data.directory));
    }
    if (data.deprecated) {
      Console.error('This package is deprecated!');
      if (data.deprecatedMessage) {
        Console.warn(data.deprecatedMessage);
      }
    }
    if (data.exports && ! data.exports.isEmpty()) {
      Console.info(
        data["exports"].getConsoleStr(),
        Console.options({ bulletPoint: "Exports: " }));
    }
    if (data.implies && ! data.implies.isEmpty()) {
      Console.info(
        data["implies"].getConsoleStr(),
        Console.options({ bulletPoint: "Implies: " }));
    }
    if (data.git) {
      Console.info(
        Console.url(data.git),
        Console.options({ bulletPoint: "Git: " }));
    }

    // If we don't have a long-form description, print the summary. (If we don't
    // have a summary, print nothing).
    if (data.description || data.summary) {
      Console.info();
      Console.info(data.description || data.summary);
    }

    // Print dependency information, if the package has any dependencies.
    if (data.dependencies && ! data.dependencies.isEmpty()) {
      Console.info();
      Console.info("Depends on:");
      Console.info(
          data.dependencies.getConsoleStr(),
          Console.options({ indent: 2 }));
    }

    // Print the 'published by' line at the very bottom.
    if (data.publishedBy) {
      var publisher = data.publishedBy;
      var pubDate = utils.longformDate(data.publishedOn);
      Console.info();
      Console.info("Published by", publisher, "on", pubDate + ".");
    }

    // Sometimes, there is a server package and a local package with the same
    // version. In this case, we prefer the local package. Explain our choice to
    // the user.
    // This is a side effect, it is not needed to be awaited.
    (async function () {
      if (
        data.local &&
        (await catalog.official.getVersion(data.name, data.version))
      ) {
        Console.info();
        Console.info(
          "This package version is built locally from source.",
          "The same version of this package also exists on the package server.",
          "To view its metadata, run",
          Console.command(
            "'meteor show " + data.name + "@" + data.version + "'"
          ),
          "from outside the project."
        );
      }
    })();
  },
  // Returns a user-friendly object from this PackageQuery to the caller.  Takes
  // in a data object with the same keys as _displayVersion.
  //
  // Returns an object with some of the following keys:
  // - name: String. Name of the package.
  // - version: String. Meteor version number.
  // - description: String. Longform description.
  // - summary: String. Short summary.
  // - git: String. Git URL.
  // - publishedBy: String. Username of the publisher.
  // - publishedOn: Date. Time of publication.
  // - local: Boolean. True if this is a local package.
  // - directory: source directory of this package.
  // - installed: Boolean. True if the isopack for this package has been
  //   downloaded, or if the package is local.
  // - dependencies: Array of objects representing package dependencies, sorted
  //   alphabetically by package name.
  // - OSarchitectures: Array of OS architectures on for which an isopack of
  //   this package exists (server packages only).
  // - exports: Array of objects representing the package exports, sorted by
  //   name of export.
  _generateVersionObject: function (data) {
    var versionFields = [
      "name", "version", "description", "summary", "git", "directory",
      "publishedBy", "publishedOn", "installed", "local", "architecturesOS",
      "deprecated", "deprecatedMessage"
    ];
    var processedData = {};
    ["exports", "implies", "dependencies"].forEach(function (key) {
      processedData[key] = data[key] ? data[key].getObject() : [];
    });
    return Object.assign(processedData, _.pick(data, versionFields));
  },

  // Displays general package data from this PackageQuery to the terminal in a
  // human-friendly format. Takes in an object that contains some, but not
  // always all, of the following keys:
  //
  // - name: (mandatory) package name
  // - maintainers: array of usernames of maintainers
  // - homepage: string of the package homepage
  // - defaultVersion: the default version of this package to use for looking up
  //   per-version information that's relevant to the package in general (ex:
  //   git).
  // - totalVersions: the total number of versions that this package has,
  //   including hidden versions.
  // - versions: an ordered array of objects, representing the versions of this
  //   package that we should return to the user. Each version should contain
  //   some of the following keys:
  //     - version: (mandatory) version number, or "local" for a version-less
  //       local package.
  //     - publishedOn: the date that the package was published.
  //     - installed: true if this is a server package that has already been
  //       downloaded to the warehouse.
  //     - local: true for a local package.
  //     - directory: source root directory of a local package.
  // - hiddenVersions: an object containing some information about versions that
  //   have been hidden from the user. Has keys:
  //     - oldestVersion: the version of this package with the smallest Meteor
  //       semver number that exists in our records.
  //     - lastUnmigrated: the most recent (largest Meteor semver) version that
  //       is marked 'unmigrated'.
  //     - lastPreRelease: the most recent pre-release version.
  _displayPackage: function (data) {
    var self = this;
    var defaultVersion = data.defaultVersion;

    // Every package has a name. Some packages have a homepage.
    var displayName = data.defaultVersion ?
      data.name + "@" + data.defaultVersion.version : data.name;
    Console.info(displayName, Console.options({ bulletPoint: "Package: " }));
    if (data.defaultVersion.deprecated) {
      Console.error('This package is deprecated!');
      if (data.defaultVersion.deprecatedMessage) {
        Console.warn(data.defaultVersion.deprecatedMessage);
      }
    }
    if (data.homepage) {
      Console.info(Console.url(data.homepage),
        Console.options({ bulletPoint: "Homepage: " }));
    }
    // Local packages might not have any maintainers.
    if (! _.isEmpty(data.maintainers)) {
      Console.info(data.maintainers.join(", "),
        Console.options({ bulletPoint: "Maintainers: " }));
    }
    // Git is per-version, so we will print the latest one, if one exists.
    if (defaultVersion && defaultVersion.git) {
      Console.info(Console.url(defaultVersion.git),
        Console.options({ bulletPoint: "Git: " }));
    }
    // Print the exports.
    if (defaultVersion && defaultVersion.exports &&
       ! defaultVersion.exports.isEmpty()) {
      Console.info(
        defaultVersion["exports"].getConsoleStr(),
        Console.options({ bulletPoint: "Exports: " }));
    }
    if (defaultVersion && defaultVersion.implies &&
        ! defaultVersion.implies.isEmpty()) {
      Console.info(
        defaultVersion["implies"].getConsoleStr(),
        Console.options({ bulletPoint: "Implies: " }));
    }
    Console.info();

    // If we don't have a long-form description, we will use the summary. For a
    // local package, we might not have a summary, in which case we should be
    // careful not to print extra lines.
    var printDescription = defaultVersion &&
      (defaultVersion.description || defaultVersion.summary);
    if (printDescription) {
      Console.info(printDescription );
      Console.info();
    }

    // If we have any versions to show, print them out now.
    var versionRows = [];
    if (data.versions && ! _.isEmpty(data.versions)) {
      var versionsHeader =
            self.showHiddenVersions ? "Versions:" : "Recent versions:";
      Console.info(versionsHeader);
      data.versions.forEach(function (v) {

        // For a local package, we don't have a published date, and we don't
        // need to show if it has already been downloaded (it is local, we don't
        // need to download it). Instead of showing both of these values, let's
        // show the directory.
        if (v.local) {
          versionRows.push([v.version, v.directory]);
          return;
        }

        // Convert the date into a display-friendly format, or print nothing for
        // a local package.
        var publishDate = utils.longformDate(v.publishedOn);

        // If there is a status that we would like to report for this package,
        // figure it out now.
        if (v.installed) {
          var paddedDate = padLongformDate(publishDate);
          versionRows.push([v.version, paddedDate + "  " + "installed"]);
        } else {
          versionRows.push([v.version, publishDate]);
        }
      });
      // The only time that we are going to go over a reasonable character limit
      // is with a directory for the local package. We would much rather display
      // the full directory than trail it off.
      Console.printTwoColumns(versionRows, { indent: 2, ignoreWidth: true });
    }

    // If we have not shown all the available versions, let the user know.
    if (data.totalVersions > versionRows.length) {
      var oldestShownVersion =
        (data["versions"][0] && data["versions"][0].version) || "";
      // A string explaining why those versions have been hidden.
      var hiddenVersions =
         formatHiddenVersions(data["hiddenVersions"], oldestShownVersion);

      // We will word things in the message in different ways, based on whether
      // multiple versions exist/have been hidden.
      var hiddenVersionsPluralizer =
         (data.totalVersions - data.versions.length == 1) ?
         "One " + hiddenVersions + " version of " + self.name + " has" :
         hiddenVersions[0].toUpperCase() + hiddenVersions.slice(1) +
         " versions of " + self.name + " have";
      var allVersionsPluralizer =
         (data.totalVersions === 1) ?
         "the hidden version" :
         "all " + data.totalVersions + " versions";

      // Display the final message.
      Console.info(
        hiddenVersionsPluralizer, "been hidden.",
        "To see " + allVersionsPluralizer + ", run",
        Console.command("'meteor show --show-all " + self.name + "'") + ".");
    }
  },
  // Returns a user-friendly object from this PackageQuery to the caller.  Takes
  // in a data object with the same keys as _displayPackage.
  //
  // Returns an object with some of the following keys:
  // - name: String. Name of the package.
  // - homepage: String. URL of the package homepage.
  // - maintainers: Array of strings. Usernames of package maintainers.
  // - totalVersions: Number. Total number of versions that exist for this
  //   package.
  // - versions: Array of objects, representing versions of this
  //   package. Objects have the following keys:
  //   - name: String. Name of the package.
  //   - version: String. Meteor version number.
  //   - description: String. Longform description.
  //   - summary: String. Short summary.
  //   - git: String. Git URL.
  //   - publishedBy: String. Username of the publisher.
  //   - publishedOn: Date. Time of publication.
  //   - local: Boolean. True if this is a local package.
  //   - directory: source directory of this package.
  //   - installed: Boolean. True if the isopack for this package has been
  //     downloaded, or if the package is local.
  //   - exports: Array of objects representing the package exports, sorted by
  //     name of export.
  _generatePackageObject: function (data) {
    var packageFields =
          [ "name", "homepage", "maintainers", "totalVersions" ];
    // Process the versions array. We only want some of the keys, and we want to
    // make sure to get the right exports object.
    var versions = data.versions.map(function (version) {
      var versionFields = [
        "name", "version", "description", "summary", "git", "publishedBy",
        "publishedOn", "installed", "local", "directory", "architecturesOS",
        "deprecated", "deprecatedMessage"
      ];
      var processedData = {};
      ["exports", "implies"].forEach(function (key) {
        processedData[key] = version[key] ? version[key].getObject() : [];
      });
      return Object.assign(processedData, _.pick(version, versionFields));
    });
    return Object.assign({ versions: versions }, _.pick(data, packageFields));
  },

});

// This class looks up release-related information in the official catalog.
//
// The constructor takes in an object with the following keys:
//   - metaRecord: (mandatory) the meta-record for this release from the
//     Releases collection.
//   - version: specific version of a release that we want to query.
//   - showHiddenVersions: show experimental, pre-release & otherwise
//     non-recommended versions of this release.
var ReleaseQuery = function (options) {
  var self = this;

  // This is the record in the Releases collection. Contains metadata, such as
  // maintainers.
  self.metaRecord = options.metaRecord;
  self.name = options.metaRecord.name;

  // We don't always want to show non-recommended release versions.
  self.showHiddenVersions = options.showHiddenVersions;

  // Aggregate the query data. If we are asking for a specific version, get data
  // for a specific version, otherwise aggregate the data about this release
  // track in general.
  self.version = options.version;
};

Object.assign(ReleaseQuery.prototype, {
  init: async function () {
    const self = this;
    self.data = self.version ? await self._getVersionDetails(self.version) : await self._getReleaseData();
  },
  // Prints the data from this ReleaseQuery to the terminal. Takes the following
  // options:
  //   - ejson: Don't pretty-print the data. Return a machine-readable ejson
  //     object.
  print: async function (options) {
    var self = this;

    // If we are asking for an EJSON-style output, print out the relevant fields.
    if (options.ejson) {
      var versionFields = [
        "track", "version", "description", "publishedBy", "publishedOn",
        "tool", "packages", "recommended"
      ];
      var packageFields = [ "name", "maintainers", "versions" ];
      var fields = self.data.version ? versionFields : packageFields;
      Console.rawInfo(await formatEJSON(_.pick(self.data, fields)));
      return;
    }

    // If we are asking for a specific version, display the information about
    // that version.
    if (self.data.version) {
      self._displayVersion(self.data);
      return;
    }
    // Otherwise, print the data about this release track in general.
    self._displayRelease(self.data);
  },

  // Gets detailed data about a specific version of this release. Returns an
  // object with the following keys:
  //  - track: name of the release track
  //  - version: release version
  //  - description: description of the release version
  //  - recommended: if this is a recommended version.
  //  - orderKey: the orderKey of this version
  //  - publishedBy: username of the publisher
  //  - publishedOn: date this version was published
  //  - packages: map of packages that go into this version
  //  - tool: the tool package@version for this release version
  _getVersionDetails: async function (version) {
    var self = this;
    var versionRecord =
       await catalog.official.getReleaseVersion(self.name, version);
    if (! versionRecord) {
      return null;
    }
    var publishDate = await getReleaseVersionPublishedOn(versionRecord);
    return {
      track: self.name,
      version: version,
      description: versionRecord.description,
      recommended: versionRecord.recommended,
      orderKey: versionRecord.orderKey,
      publishedBy: versionRecord.publishedBy["username"],
      publishedOn: publishDate,
      packages: versionRecord.packages,
      tool: versionRecord.tool
    };
  },
  // Gets aggregate data about this release track in general. Returns an object
  // with the following keys:
  //    - track: name of the release track
  //    - maintainers: an array of usernames of maintainers
  //    - defaultVersion: version record for the default version of this release.
  //    - totalVersions: total number of release versions for this track
  //    - versions: an array of version objects. If only recommended versions
  //      are returned, ordered by orderKey, otherwise unordered. Objects have
  //      the following keys:
  //         - version: version number
  //         - description: version description
  //         - recommended: true for recommended versions
  //         - orderKey: (only if showHiddenVersions is true) the orderKey of
  //           this version.
  //         - publishedBy: username of the publisher
  //         - publishedOn: date the version was published
  _getReleaseData: async function () {
    var self = this;
    var data = {
      track: self.metaRecord.name,
      maintainers: _.pluck(self.metaRecord.maintainers, "username")
    };
    data["defaultVersion"] =
      await catalog.official.getDefaultReleaseVersionRecord(self.name);

    // Collect information about versions.
    var versions;
    if (self.showHiddenVersions) {
      // There is no obvious way to get an absolute ranking of all release
      // versions, so this is unsorted. If we have to, we will deal with sorting
      // this at display time.
      versions = await catalog.official.getReleaseVersionRecords(self.name);
    } else {
      versions = await catalog.official.getSortedRecommendedReleaseRecords(self.name);
      versions.reverse();
    }

    // We don't want to show the user package or tool data in general release
    // mode (it is a lot of data). Select to show the fields that we want to
    // return only.
    var versionFields =
       [ "version", "description", "recommended"];

    // orderKey is important for dealing with experimental versions, but it is
    // an internal system detail that we would rather not reveal at this level.
    if (self.showHiddenVersions) {
      versionFields.push("orderKey");
    }
    data["versions"] = [];
    for (const versionRecord of versions) {
      const pickedValues = _.pick(versionRecord, versionFields);
      pickedValues.publishedBy = versionRecord.publishedBy["username"];
      pickedValues.publishedOn = await getReleaseVersionPublishedOn(versionRecord);
      data["versions"].push(pickedValues);
    }
    data["totalVersions"] = await catalog.official.getNumReleaseVersions(self.name);
    return data;
  },
  // Displays information about a specific release version in a human-readable
  // format. Takes in an object with the following keys:
  // - track: release track
  // - version: release version
  // - publishedBy: username of the publisher
  // - publishedOn: date the version was published
  // - recommended: true if this is a recommended version
  // - description: description of the release version
  // - tool: tool package specification for this version
  // - packages: map of packages for this release version
  _displayVersion: function (data) {
    Console.info("Release: " + data.track + "@" + data.version);
    var isRecommended = data.recommended ? "yes" : "no";
    Console.info("Recommended: " + isRecommended);
    Console.info("Tool package: " + data.tool);
    Console.info();
    Console.info(data.description);
    Console.info();
    if (!_.isEmpty(data.packages)) {
      Console.info("Packages:");
      _.each(data.packages, function (version, packageName) {
          Console.info(
            packageName + ": " + version,
            Console.options({ indent: 2 }));
      });
      Console.info();
    }
    Console.info(
      "Published by " + data.publishedBy + " on " +
      utils.longformDate(getReleaseVersionPublishedOn(data)));
  },
  // Displays information about this release track in general in a
  // human-readable format. Takes in an object with the following keys:
  //    - track: name of the release track
  //    - maintainers: an array of usernames of maintainers
  //    - defaultVersion: version record for the default version of this release.
  //    - totalVersions: total number of release versions for this track
  //    - versions: an array of version objects. If only recommended versions
  //      are returned, ordered by orderKey, otherwise unordered. Objects have
  //      the following keys:
  //         - version: version number
  //         - description: version description
  //         - recommended: true for recommended versions
  //         - orderKey: (only if showHiddenVersions is true) the orderKey of
  //           this version.
  //         - publishedBy: username of the publisher
  //         - publishedOn: date the version was published
  _displayRelease: function (data) {
    var self = this;

    Console.info("Release:",  data.track);
    // There is no such thing as a local release, which means all releases have
    // a maintainer.
    Console.info("Maintainers:", data.maintainers.join(", "));
    Console.info();

    if (data.defaultVersion) {
      Console.info(data.defaultVersion.description);
      Console.info();
    }

    if (self.showHiddenVersions) {
      self._displayAllReleaseVersions(data.versions);
      return;
    }

    // Display the recommended versions of this release.
    var rows = [];
    if (!_.isEmpty(data.versions)) {
      Console.info("Recommended versions:");
      data.versions.forEach(function (v) {
        rows.push([v.version, utils.longformDate(v.publishedOn)]);
      });
      Console.printTwoColumns(rows, { indent: 2 });
    }

    // Display a warning about other release versions at the bottom.
    if (data.totalVersions > rows.length) {
      var versionsPluralizer =
            (data.totalVersions > 1) ?
            "all " + data.totalVersions + " versions" :
            "the hidden version";
      // We only hide release versions for one reason -- they are not
      // recommended. We would have to parse version numbers to differentiate
      // between 'pre-release' and 'deprecated' (and sort-of-experimental, like
      // '1.0-weird-trick) and we don't want to rely on version number
      // conventions in code.
      var versionsHidden =
            (data.totalVersions - rows.length > 1) ?
            "Non-recommended versions of " + self.name + " have been hidden." :
            "One non-recommended version of " + self.name + " has been hidden.";

      Console.info(
        versionsHidden,
        "To see " + versionsPluralizer + ", run",
        Console.command("'meteor show --show-all " + self.name + "'") + ".");
    }
  },
  // Displays all the versions of a given release in a human-readable
  // format. Includes experimental and otherwise hidden versions. Takes in an
  // array of version objects, each of which has the following keys:
  //  - version: version string
  //  - orderKey: (optional) orderKey of this version. Not all versions have
  //    orderKeys.
  //  - publishedOn: date of publication
  //  - recommended: true if the version is recommended.
  _displayAllReleaseVersions: function (versions) {
    var self = this;
    var columnOpts = { indent: 2, ignoreWidth: true };
    // If we don't have any versions, then there is nothing to display.
    if (! versions) { return; }

    // We are going to print versions with order key ('versions'), separately
    // from versions without an order key ('experimental versions').
    var versionsDivided = _.groupBy(versions, function (v) {
      return _.has(v, "orderKey");
    });
    var experimentalVersions = versionsDivided[false];
    var versionsWithKey = versionsDivided[true];

    if (versionsWithKey) {
      // Sort versions that have order keys by order key, so that 1.0 comes
      // after 0.9.4.1, etc.
      versionsWithKey = _.sortBy(versionsWithKey, function (v) {
        return v.orderKey;
      });
      Console.info("Versions:");
      var rows = [];
      _.each(versionsWithKey, function (vr) {
        var dateStr = utils.longformDate(vr.publishedOn);
        if (! vr.recommended) {
          rows.push([ vr.version, dateStr ]);
        } else {
          var paddedDate = padLongformDate(dateStr);
          rows.push([ vr.version, paddedDate + "  (recommended)" ]);
        }
      });
      Console.printTwoColumns(rows, columnOpts);
    }

    if (experimentalVersions) {
      // We can't sort by order key, so sort by order of publication.
      experimentalVersions = _.sortBy(experimentalVersions, function (v) {
        return v.publishedOn;
      });
      Console.info("Experimental versions:");
      var rows = [];
      _.each(experimentalVersions, function (vr) {
        // Experimental versions cannot be recommended.
        rows.push([vr.version, utils.longformDate(vr.publishedOn)]);
      });
      Console.printTwoColumns(rows, columnOpts);
    }
  }
});


///////////////////////////////////////////////////////////////////////////////
// show
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'show',
  pretty: true,
  minArgs: 0,
  maxArgs: 1,
  usesPackage: true,
  options: {
    "show-all": { type: Boolean },
    "ejson": { type: Boolean }
  },
  catalogRefresh:
    new catalog.Refresh.OnceAtStart(
        { maxAge: DEFAULT_MAX_AGE_MS, ignoreErrors: true })
}, async function (options) {
  var fullName;
  var name;
  var version;
  // Because of the new projectContext interface, we need to initialize the
  // project context in order to load the local catalog. This is not ideal.
  var projectContext = await getTempContext(options);

  // If the user specified a query, process it.
  if (! _.isEmpty(options.args)) {
    // The foo@bar API means that we have to do some string parsing to figure out
    // if we want a particular version.
    fullName = options.args[0];
    var splitArgs = fullName.split('@');
    name = splitArgs[0];
    version = (splitArgs.length > 1) ? splitArgs[1] : null;
    if (splitArgs.length > 2) {
      Console.error("Invalid request format: " + fullName);
      process.exit(1);
    }
  } else {
    if (! options.packageDir) {
      // Letting the user run 'meteor show' without arguments from a package
      // directory is a pleasant shortcut, but the default should be specifying
      // a query.
      Console.error(
        "Please specify a package or release name to show information about it."
      );
      process.exit(1);
    }
    // Use the projectContext to get the name of the package.
    var currentVersion =
          await projectContext.localCatalog.getVersionBySourceRoot(options.packageDir);
    name = currentVersion.packageName;
    version = "local";
    fullName = name + "@local";
  }
  var query = null;

  // First, we need to figure out if we are dealing with a package, or a
  // release. We don't want to rely on capitalization conventions, so we will
  // start by checking if a package by that name exists. If it does, then we are
  // dealing with a package. (Unlike the normal projectContext, we want to
  // prefer the remote record, if one exists, rather than the local record. The
  // remote record contains data like 'homepage' and 'maintainers', that the
  // local record does not).
  var packageRecord =
        await catalog.official.getPackage(name) ||
        await projectContext.localCatalog.getPackage(name);
  if (packageRecord) {
    query =  new PackageQuery({
      metaRecord: packageRecord,
      version: version,
      projectContext: projectContext,
      showHiddenVersions: options["show-all"],
      showArchitecturesOS: options.ejson,
      showDependencies: !! version
    });
    await query.init();
  }

  // If this is not a package, it might be a release. Let's check if there is
  // a release by this name. There are no local releases, so we only need to
  // check the official catalog.
  if (! query) {
    var releaseRecord = await catalog.official.getReleaseTrack(name);
    if (releaseRecord) {
      query = new ReleaseQuery({
        metaRecord: releaseRecord,
        version: version,
        showHiddenVersions: options["show-all"]
      });
      await query.init();
    }
  }
  // If we have failed to create a query, or if we have created a query and it
  // couldn't gather any data about our request, then the item that we are
  // looking for does not exist.
  if (! query || ! query.data) {
    return itemNotFound(fullName);
  }

  await query.print({ ejson: !! options.ejson });
  return 0;
});


///////////////////////////////////////////////////////////////////////////////
// search
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'search',
  pretty: true,
  usesPackage: true,
  minArgs: 0, // So we can provide specific help
  maxArgs: 1,
  options: {
    maintainer: { type: String },
    "show-all": { type: Boolean },
    ejson: { type: Boolean },
    // Undocumented debug-only option (originally added for Velocity).
    "debug-only": { type: Boolean },
    "prod-only": { type: Boolean },
    "test-only": { type: Boolean },
  },
  catalogRefresh:
    new catalog.Refresh.OnceAtStart(
      { maxAge: DEFAULT_MAX_AGE_MS, ignoreErrors: true })
}, async function (options) {
  if (options.args.length === 0) {
    Console.info(
      "To show all packages, do", Console.command("meteor search ."));
    return 1;
  }

  // Because of the new projectContext interface, we need to initialize the
  // project context in order to load the local catalog.
  var projectContext = await getTempContext(options);

  // XXX We should push the queries into SQLite!
  var allPackages = _.union(
    await catalog.official.getAllPackageNames(),
    await projectContext.localCatalog.getAllPackageNames());
  var allReleases = await catalog.official.getAllReleaseTracks();
  var matchingPackages = [];
  var matchingReleases = [];

  var selector;
  var pattern = options.args[0];

  var search;
  try {
    search = new RegExp(pattern);
  } catch (err) {
    Console.error(err + "");
    return 1;
  }

  // Do not return true on broken packages, unless requested in options.
  var filterBroken = async function (match, isRelease, name) {
    // If the package does not match, or it is not a package at all or if we
    // don't want to filter anyway, we do not care.
    if (!match || isRelease) {
      return match;
    }
    var vr;
    if (!options["show-all"]) {
      // If we can't find a version in the local catalog, we want to get the
      // latest mainline (ie: non-RC) version from the official catalog.
      vr = await projectContext.localCatalog.getLatestVersion(name) ||
        await catalog.official.getLatestMainlineVersion(name);
    } else {
      // We want the latest version of this package, and we don't care if it is
      // a release candidate.
      vr = await projectContext.projectCatalog.getLatestVersion(name);
    }
    if (!vr) {
      return false;
    }
    // If we did NOT ask for unmigrated packages and this package is unmigrated,
    // we don't care.
    if (!options["show-all"] && vr.unmigrated){
      return false;
    }
    // If we asked for debug-only packages and this package is NOT debug only,
    // we don't care.
    if (options["debug-only"] && !vr.debugOnly) {
      return false;
    }
    // If we asked for prod-only packages and this package is NOT prod only,
    // we don't care.
    if (options["prod-only"] && !vr.prodOnly) {
      return false;
    }
    // If we asked for test-only packages and this package is NOT test only,
    // we don't care.
    if (options["test-only"] && !vr.testOnly) {
      return false;
    }
    return true;
  };

  if (options.maintainer) {
    var username =  options.maintainer;
    // In the future, we should consider checking this on the server, but I
    // suspect the main use of this command will be to deal with the automatic
    // migration and uncommon in everyday use. From that perspective, it makes
    // little sense to require you to be online to find out what packages you
    // own; and the consequence of not mentioning your group packages until
    // you update to a new version of meteor is not that dire.
    selector = async function (name, isRelease) {
      var record;
      // XXX make sure search works while offline
      if (isRelease) {
        record = await catalog.official.getReleaseTrack(name);
      } else {
        record = await catalog.official.getPackage(name);
      }
      return await filterBroken(
        (name.match(search) &&
         record && !!_.findWhere(record.maintainers, {username: username})),
        isRelease, name);
    };
  } else {
    selector = function (name, isRelease) {
      return filterBroken(name.match(search),
        isRelease, name);
    };
  }

  await buildmessage.enterJob({ title: 'Searching packages' }, async function () {
    for (const pack of allPackages) {
      if (await selector(pack, false)) {
        var vr;
        if (!options['show-all']) {
          vr =
              await projectContext.localCatalog.getLatestVersion(pack) ||
              await catalog.official.getLatestMainlineVersion(pack);
        } else {
          vr = await projectContext.projectCatalog.getLatestVersion(pack);
        }
        if (vr) {
          matchingPackages.push({
            name: pack,
            description: vr.description,
            latestVersion: vr.version,
            lastUpdated: new Date(vr.lastUpdated)
          });
        }
      }
    }
    for (const track of allReleases) {
      if (selector(track, true)) {
        var vr = await catalog.official.getDefaultReleaseVersionRecord(track);
        if (vr) {
          matchingReleases.push({
            name: track,
            description: vr.description,
            latestVersion: vr.version,
            lastUpdated: new Date(vr.lastUpdated)
          });
        }
      }
    }
  });

  if (options.ejson) {
    var ret = {
      packages: matchingPackages,
      releases: matchingReleases
    };
    Console.rawInfo(await formatEJSON(ret));
    return 0;
  }

  var output = false;
  if (!_.isEqual(matchingPackages, [])) {
    output = true;
    Console.info("Matching packages:");
    utils.printPackageList(matchingPackages);
  }

  if (!_.isEqual(matchingReleases, [])) {
    output = true;
    Console.info("Matching releases:");
    utils.printPackageList(matchingReleases);
  }

  if (!output) {
    Console.error(pattern + ': nothing found');
    catalogUtils.explainIfRefreshFailed();
  } else {
    Console.info(
      "You can use", Console.command("'meteor show'"),
      "to get more information on a specific item.");
  }
});
