/// <reference path="../../../../definitions/node.d.ts" />
"use strict";
const path = require('path');
const fs = require('fs');
const os = require('os');
const util = require('util');
const tcm = require('./taskcommand');
const trm = require('./toolrunner');
const mock = require('./mock');
(function (TaskResult) {
    TaskResult[TaskResult["Succeeded"] = 0] = "Succeeded";
    TaskResult[TaskResult["Failed"] = 1] = "Failed";
})(exports.TaskResult || (exports.TaskResult = {}));
var TaskResult = exports.TaskResult;
//-----------------------------------------------------
// String convenience
//-----------------------------------------------------
function startsWith(str, start) {
    return str.slice(0, start.length) == start;
}
function endsWith(str, start) {
    return str.slice(-str.length) == str;
}
//-----------------------------------------------------
// General Helpers
//-----------------------------------------------------
exports._outStream = process.stdout;
exports._errStream = process.stderr;
function _writeError(str) {
    exports._errStream.write(str + os.EOL);
}
exports._writeError = _writeError;
function _writeLine(str) {
    exports._outStream.write(str + os.EOL);
}
exports._writeLine = _writeLine;
function setStdStream(stdStream) {
    exports._outStream = stdStream;
}
exports.setStdStream = setStdStream;
function setErrStream(errStream) {
    exports._errStream = errStream;
}
exports.setErrStream = setErrStream;
//-----------------------------------------------------
// Results and Exiting
//-----------------------------------------------------
function setResult(result, message) {
    debug('task result: ' + TaskResult[result]);
    command('task.complete', { 'result': TaskResult[result] }, message);
    if (result == TaskResult.Failed) {
        _writeError(message);
    }
    if (result == TaskResult.Failed) {
        process.exit(0);
    }
}
exports.setResult = setResult;
//
// Catching all exceptions
//
process.on('uncaughtException', (err) => {
    setResult(TaskResult.Failed, 'Unhandled:' + err.message);
});
function exitOnCodeIf(code, condition) {
    if (condition) {
        setResult(TaskResult.Failed, 'failure return code: ' + code);
    }
}
exports.exitOnCodeIf = exitOnCodeIf;
//
// back compat: should use setResult
//
function exit(code) {
    setResult(code, 'return code: ' + code);
}
exports.exit = exit;
//-----------------------------------------------------
// Loc Helpers
//-----------------------------------------------------
var locStringCache = {};
var resourceFile;
var libResourceFileLoaded = false;
function loadLocStrings(resourceFile) {
    var locStrings = {};
    if (resourceFile && fs.existsSync(resourceFile)) {
        debug('load loc strings from: ' + resourceFile);
        var resourceJson = require(resourceFile);
        if (resourceJson && resourceJson.hasOwnProperty('messages')) {
            for (var key in resourceJson.messages) {
                if (typeof (resourceJson.messages[key]) === 'object') {
                    if (resourceJson.messages[key].loc && resourceJson.messages[key].loc.toString().length > 0) {
                        locStrings[key] = resourceJson.messages[key].loc.toString();
                    }
                    else if (resourceJson.messages[key].fallback) {
                        locStrings[key] = resourceJson.messages[key].fallback.toString();
                    }
                }
                else if (typeof (resourceJson.messages[key]) === 'string') {
                    locStrings[key] = resourceJson.messages[key];
                }
            }
        }
    }
    return locStrings;
}
function setResourcePath(path) {
    if (process.env['TASKLIB_INPROC_UNITS']) {
        resourceFile = null;
        libResourceFileLoaded = false;
        locStringCache = {};
    }
    if (!resourceFile) {
        resourceFile = path;
        debug('set resource file to: ' + resourceFile);
        var locStrs = loadLocStrings(resourceFile);
        for (var key in locStrs) {
            debug('cache loc string: ' + key);
            locStringCache[key] = locStrs[key];
        }
    }
    else {
        warning('resource file is already set to: ' + resourceFile);
    }
}
exports.setResourcePath = setResourcePath;
function loc(key) {
    // we can't do ...param if we target ES6 and node 5.  This is what <=ES5 compiles down to.
    var param = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        param[_i - 1] = arguments[_i];
    }
    if (!libResourceFileLoaded) {
        // merge loc strings from vsts-task-lib.
        var libResourceFile = path.join(__dirname, 'lib.json');
        var libLocStrs = loadLocStrings(libResourceFile);
        for (var libKey in libLocStrs) {
            debug('cache vsts-task-lib loc string: ' + libKey);
            locStringCache[libKey] = libLocStrs[libKey];
        }
        libResourceFileLoaded = true;
    }
    var locString;
    ;
    if (locStringCache.hasOwnProperty(key)) {
        locString = locStringCache[key];
    }
    else {
        if (!resourceFile) {
            warning('resource file haven\'t been set, can\'t find loc string for key: ' + key);
        }
        else {
            warning('can\'t find loc string for key: ' + key);
        }
        locString = key;
    }
    if (param.length > 0) {
        return util.format.apply(this, [locString].concat(param));
    }
    else {
        return locString;
    }
}
exports.loc = loc;
//-----------------------------------------------------
// Input Helpers
//-----------------------------------------------------
function getVariable(name) {
    var varval = process.env[name.replace(/\./g, '_').toUpperCase()];
    debug(name + '=' + varval);
    var mocked = mock.getResponse('getVariable', name);
    return mocked || varval;
}
exports.getVariable = getVariable;
function setVariable(name, val) {
    if (!name) {
        _writeError('name required: ' + name);
        exit(1);
    }
    var varValue = val || '';
    process.env[name.replace(/\./g, '_').toUpperCase()] = varValue;
    debug('set ' + name + '=' + varValue);
    command('task.setvariable', { 'variable': name || '' }, varValue);
}
exports.setVariable = setVariable;
function getInput(name, required) {
    var inval = process.env['INPUT_' + name.replace(' ', '_').toUpperCase()];
    if (required && !inval) {
        setResult(TaskResult.Failed, 'Input required: ' + name);
    }
    debug(name + '=' + inval);
    return inval;
}
exports.getInput = getInput;
function getBoolInput(name, required) {
    return getInput(name, required) == "true";
}
exports.getBoolInput = getBoolInput;
function setEnvVar(name, val) {
    if (val) {
        process.env[name] = val;
    }
}
exports.setEnvVar = setEnvVar;
//
// Split - do not use for splitting args!  Instead use arg() - it will split and handle
//         this is for splitting a simple list of items like targets
//
function getDelimitedInput(name, delim, required) {
    var inval = getInput(name, required);
    if (!inval) {
        return [];
    }
    return inval.split(delim);
}
exports.getDelimitedInput = getDelimitedInput;
function filePathSupplied(name) {
    // normalize paths
    var pathValue = this.resolve(this.getPathInput(name) || '');
    var repoRoot = this.resolve(this.getVariable('build.sourcesDirectory') || '');
    var supplied = pathValue !== repoRoot;
    debug(name + 'path supplied :' + supplied);
    return supplied;
}
exports.filePathSupplied = filePathSupplied;
function getPathInput(name, required, check) {
    var inval = getInput(name, required);
    if (inval) {
        if (check) {
            checkPath(inval, name);
        }
        if (inval.indexOf(' ') > 0) {
            if (!startsWith(inval, '"')) {
                inval = '"' + inval;
            }
            if (!endsWith(inval, '"')) {
                inval += '"';
            }
        }
    }
    else if (required) {
        setResult(TaskResult.Failed, 'Input required: ' + name); // exit
    }
    debug(name + '=' + inval);
    return inval;
}
exports.getPathInput = getPathInput;
//-----------------------------------------------------
// Endpoint Helpers
//-----------------------------------------------------
function getEndpointUrl(id, optional) {
    var urlval = getVariable('ENDPOINT_URL_' + id);
    debug(id + '=' + urlval);
    if (!optional && !urlval) {
        _writeError('Endpoint not present: ' + id);
        exit(1);
    }
    return urlval;
}
exports.getEndpointUrl = getEndpointUrl;
function getEndpointDataParameter(id, key, optional) {
    var dataParam = getVariable('ENDPOINT_DATA_' + id + '_' + key.toUpperCase());
    debug(id + '=' + dataParam);
    if (!optional && !dataParam) {
        _writeError('Endpoint data not present: ' + id);
        exit(1);
    }
    return dataParam;
}
exports.getEndpointDataParameter = getEndpointDataParameter;
function getEndpointAuthorizationScheme(id, optional) {
    var authScheme = getVariable('ENDPOINT_AUTH_SCHEME_' + id);
    debug(id + '=' + authScheme);
    if (!optional && !authScheme) {
        _writeError('Endpoint auth not present: ' + id);
        exit(1);
    }
    return authScheme;
}
exports.getEndpointAuthorizationScheme = getEndpointAuthorizationScheme;
function getEndpointAuthorizationParameter(id, key, optional) {
    var authParam = getVariable('ENDPOINT_AUTH_PARAMETER_' + id + '_' + key.toUpperCase());
    debug(id + '=' + authParam);
    if (!optional && !authParam) {
        _writeError('Endpoint auth not present: ' + id);
        exit(1);
    }
    return authParam;
}
exports.getEndpointAuthorizationParameter = getEndpointAuthorizationParameter;
function getEndpointAuthorization(id, optional) {
    var aval = getVariable('ENDPOINT_AUTH_' + id);
    debug(id + '=' + aval);
    if (!optional && !aval) {
        setResult(TaskResult.Failed, 'Endpoint not present: ' + id);
    }
    var auth;
    try {
        auth = JSON.parse(aval);
    }
    catch (err) {
        setResult(TaskResult.Failed, 'Invalid endpoint auth: ' + aval); // exit
    }
    return auth;
}
exports.getEndpointAuthorization = getEndpointAuthorization;
//-----------------------------------------------------
// Fs Helpers
//-----------------------------------------------------
class FsStats {
    setAnswers(mockResponses) {
        this.m_isFile = mockResponses['isFile'] || false;
        this.m_isDirectory = mockResponses['isDirectory'] || false;
        this.m_isBlockDevice = mockResponses['isBlockDevice'] || false;
        this.m_isCharacterDevice = mockResponses['isCharacterDevice'] || false;
        this.m_isSymbolicLink = mockResponses['isSymbolicLink'] || false;
        this.m_isFIFO = mockResponses['isFIFO'] || false;
        this.m_isSocket = mockResponses['isSocket'] || false;
        this.dev = mockResponses['dev'];
        this.ino = mockResponses['ino'];
        this.mode = mockResponses['mode'];
        this.nlink = mockResponses['nlink'];
        this.uid = mockResponses['uid'];
        this.gid = mockResponses['gid'];
        this.rdev = mockResponses['rdev'];
        this.size = mockResponses['size'];
        this.blksize = mockResponses['blksize'];
        this.blocks = mockResponses['blocks'];
        this.atime = mockResponses['atime'];
        this.mtime = mockResponses['mtime'];
        this.ctime = mockResponses['ctime'];
        this.m_isSocket = mockResponses['isSocket'];
    }
    isFile() {
        return this.m_isFile;
    }
    isDirectory() {
        return this.m_isDirectory;
    }
    isBlockDevice() {
        return this.m_isBlockDevice;
    }
    isCharacterDevice() {
        return this.m_isCharacterDevice;
    }
    isSymbolicLink() {
        return this.m_isSymbolicLink;
    }
    isFIFO() {
        return this.m_isFIFO;
    }
    isSocket() {
        return this.m_isSocket;
    }
}
exports.FsStats = FsStats;
function stats(path) {
    var fsStats = new FsStats();
    fsStats.setAnswers(mock.getResponse('stats', path) || {});
    return fsStats;
}
exports.stats = stats;
function exist(path) {
    return mock.getResponse('exist', path) || false;
}
exports.exist = exist;
function writeFile(file, data, options) {
    //do nothing
}
exports.writeFile = writeFile;
function osType() {
    return mock.getResponse('osType', 'osType');
}
exports.osType = osType;
function cwd() {
    return mock.getResponse('cwd', 'cwd');
}
exports.cwd = cwd;
//-----------------------------------------------------
// Cmd Helpers
//-----------------------------------------------------
function command(command, properties, message) {
    var taskCmd = new tcm.TaskCommand(command, properties, message);
    _writeLine(taskCmd.toString());
}
exports.command = command;
function warning(message) {
    command('task.issue', { 'type': 'warning' }, message);
}
exports.warning = warning;
function error(message) {
    command('task.issue', { 'type': 'error' }, message);
}
exports.error = error;
function debug(message) {
    command('task.debug', null, message);
}
exports.debug = debug;
var _argStringToArray = function (argString) {
    var args = argString.match(/([^" ]*("[^"]*")[^" ]*)|[^" ]+/g);
    for (var i = 0; i < args.length; i++) {
        args[i] = args[i].replace(/"/g, "");
    }
    return args;
};
function cd(path) {
}
exports.cd = cd;
function pushd(path) {
}
exports.pushd = pushd;
function popd() {
}
exports.popd = popd;
//------------------------------------------------
// Validation Helpers
//------------------------------------------------
function checkPath(p, name) {
    debug('check path : ' + p);
    if (!p || !mock.getResponse('checkPath', p)) {
        setResult(TaskResult.Failed, 'not found ' + name + ': ' + p); // exit
    }
}
exports.checkPath = checkPath;
//-----------------------------------------------------
// Shell/File I/O Helpers
// Abstract these away so we can
// - default to good error handling
// - inject system.debug info
// - have option to switch internal impl (shelljs now)
//-----------------------------------------------------
function mkdirP(p) {
    debug('creating path: ' + p);
}
exports.mkdirP = mkdirP;
function resolve() {
    // we can't do ...param if we target ES6 and node 5.  This is what <=ES5 compiles down to.
    //return the posix implementation in the mock, so paths will be consistent when L0 tests are run on Windows or Mac/Linux
    var absolutePath = path.posix.resolve.apply(this, arguments);
    debug('Absolute path for pathSegments: ' + arguments + ' = ' + absolutePath);
    return absolutePath;
}
exports.resolve = resolve;
function which(tool, check) {
    var response = mock.getResponse('which', tool);
    if (check) {
        checkPath(response, tool);
    }
    return response;
}
exports.which = which;
function ls(options, paths) {
    var response = mock.getResponse('ls', paths[0]);
    if (!response) {
        return [];
    }
    return response;
}
exports.ls = ls;
function cp(options, source, dest) {
    console.log('###copying###');
    debug('copying ' + source + ' to ' + dest);
}
exports.cp = cp;
function find(findPath) {
    return mock.getResponse('find', findPath);
}
exports.find = find;
function rmRF(path) {
    var response = mock.getResponse('rmRF', path);
    if (!response['success']) {
        setResult(1, response['message']);
    }
}
exports.rmRF = rmRF;
function mv(source, dest, force, continueOnError) {
    debug('moving ' + source + ' to ' + dest);
    return true;
}
exports.mv = mv;
function glob(pattern) {
    debug('glob ' + pattern);
    var matches = mock.getResponse('glob', pattern);
    debug('found ' + matches.length + ' matches');
    if (matches.length > 0) {
        var m = Math.min(matches.length, 10);
        debug('matches:');
        if (m == 10) {
            debug('listing first 10 matches as samples');
        }
        for (var i = 0; i < m; i++) {
            debug(matches[i]);
        }
    }
    return matches;
}
exports.glob = glob;
function globFirst(pattern) {
    debug('globFirst ' + pattern);
    var matches = glob(pattern);
    if (matches.length > 1) {
        warning('multiple workspace matches.  using first.');
    }
    debug('found ' + matches.length + ' matches');
    return matches[0];
}
exports.globFirst = globFirst;
//-----------------------------------------------------
// Exec convenience wrapper
//-----------------------------------------------------
function exec(tool, args, options) {
    var toolPath = which(tool, true);
    var tr = createToolRunner(toolPath);
    if (args) {
        tr.arg(args);
    }
    return tr.exec(options);
}
exports.exec = exec;
function execSync(tool, args, options) {
    var toolPath = which(tool, true);
    var tr = createToolRunner(toolPath);
    if (args) {
        tr.arg(args);
    }
    return tr.execSync(options);
}
exports.execSync = execSync;
function createToolRunner(tool) {
    var tr = new trm.ToolRunner(tool);
    tr.on('debug', (message) => {
        debug(message);
    });
    return tr;
}
exports.createToolRunner = createToolRunner;
function tool(tool) {
    return createToolRunner(tool);
}
exports.tool = tool;
//-----------------------------------------------------
// Matching helpers
//-----------------------------------------------------
function match(list, pattern, options) {
    return mock.getResponse('match', pattern) || [];
}
exports.match = match;
function matchFile(list, pattern, options) {
    return mock.getResponse('match', pattern) || [];
}
exports.matchFile = matchFile;
function filter(pattern, options) {
    return mock.getResponse('filter', pattern) || [];
}
exports.filter = filter;
//-----------------------------------------------------
// Test Publisher
//-----------------------------------------------------
class TestPublisher {
    constructor(testRunner) {
        this.testRunner = testRunner;
    }
    publish(resultFiles, mergeResults, platform, config, runTitle, publishRunAttachments) {
        var properties = {};
        properties['type'] = this.testRunner;
        if (mergeResults) {
            properties['mergeResults'] = mergeResults;
        }
        if (platform) {
            properties['platform'] = platform;
        }
        if (config) {
            properties['config'] = config;
        }
        if (runTitle) {
            properties['runTitle'] = runTitle;
        }
        if (publishRunAttachments) {
            properties['publishRunAttachments'] = publishRunAttachments;
        }
        if (resultFiles) {
            properties['resultFiles'] = resultFiles;
        }
        command('results.publish', properties, '');
    }
}
exports.TestPublisher = TestPublisher;
//-----------------------------------------------------
// Code Coverage Publisher
//-----------------------------------------------------
class CodeCoveragePublisher {
    constructor() {
    }
    publish(codeCoverageTool, summaryFileLocation, reportDirectory, additionalCodeCoverageFiles) {
        var properties = {};
        if (codeCoverageTool) {
            properties['codecoveragetool'] = codeCoverageTool;
        }
        if (summaryFileLocation) {
            properties['summaryfile'] = summaryFileLocation;
        }
        if (reportDirectory) {
            properties['reportdirectory'] = reportDirectory;
        }
        if (additionalCodeCoverageFiles) {
            properties['additionalcodecoveragefiles'] = additionalCodeCoverageFiles;
        }
        command('codecoverage.publish', properties, "");
    }
}
exports.CodeCoveragePublisher = CodeCoveragePublisher;
//-----------------------------------------------------
// Code coverage Publisher
//-----------------------------------------------------
class CodeCoverageEnabler {
    constructor(buildTool, ccTool) {
        this.buildTool = buildTool;
        this.ccTool = ccTool;
    }
    enableCodeCoverage(buildProps) {
        buildProps['buildtool'] = this.buildTool;
        buildProps['codecoveragetool'] = this.ccTool;
        command('codecoverage.enable', buildProps, "");
    }
}
exports.CodeCoverageEnabler = CodeCoverageEnabler;
//-----------------------------------------------------
// Tools
//-----------------------------------------------------
exports.TaskCommand = tcm.TaskCommand;
exports.commandFromString = tcm.commandFromString;
exports.ToolRunner = trm.ToolRunner;
trm.debug = debug;


'use strict';

// Adapted from https://github.com/polvo-labs/card-type/blob/aaab11f80fa1939bccc8f24905a06ae3cd864356/src/cardType.js#L37-L42
function matchesRange(cardNumber, min, max) {
  var maxLengthToCheck = String(min).length;
  var substr = cardNumber.substr(0, maxLengthToCheck);
  var integerRepresentationOfCardNumber = parseInt(substr, 10);

  min = parseInt(String(min).substr(0, substr.length), 10);
  max = parseInt(String(max).substr(0, substr.length), 10);

  return integerRepresentationOfCardNumber >= min && integerRepresentationOfCardNumber <= max;
}

function matchesPattern(cardNumber, pattern) {
  pattern = String(pattern);

  return pattern.substring(0, cardNumber.length) === cardNumber.substring(0, pattern.length);
}

function matches(cardNumber, pattern) {
  if (Array.isArray(pattern)) {
    return matchesRange(cardNumber, pattern[0], pattern[1]);
  }

  return matchesPattern(cardNumber, pattern);
}

module.exports = matches;


const _ = require('lodash')
const env = require('../lib/env')
const jsonInPlace = require('json-in-place')
const semver = require('semver')
const getRangedVersion = require('../lib/get-ranged-version')

function seperateNormalAndMonorepos (packageFiles) {
  const resultsByRepo = groupPackageFilesByRepo(packageFiles)

  return _.partition(resultsByRepo, (result) => {
    // The repo is a monorepo if:
    // - there’s more more than one file in the result array and
    //   - their file paths aren’t all equal
    //   - their file paths are all equal, but they’re not `package.json`
    // - there’s only one file, and its path is _not_ `package.json`
    return (result.length > 1 && hasDifferentFilenames(result)) ||
    (result.length === 1 && result[0].value.filename !== 'package.json')
  })
}

function groupPackageFilesByRepo (packageFiles) {
  return _.groupBy(packageFiles, 'value.fullName')
}

function hasDifferentFilenames (group) {
  if (group.length === 1) return true
  const uniqueFilenames = _.uniq(_.map(group, g => g.value.filename))
  if (uniqueFilenames.length > 1) return true
  if (uniqueFilenames[0] !== 'package.json') return true
  return false
}

const order = {
  'dependencies': 1,
  'devDependencies': 2,
  'optionalDependencies': 3
}

function getHighestPriorityDependency (dependencies) {
  const types = dependencies.map(d => d.type)
  return types.sort((depA, depB) => order[depA] - order[depB])[0]
}

function sortByDependency (packageA, packageB) {
  return order[packageA.value.type] - order[packageB.value.type]
}

function filterAndSortPackages (packageFiles) {
  return packageFiles
    .filter(pkg => pkg.value.type !== 'peerDependencies')
    .sort(sortByDependency)
}

function getSatisfyingVersions (versions, pkg) {
  return Object.keys(versions)
    .filter(version => semver.satisfies(version, pkg.value.oldVersion))
    .sort(semver.rcompare)
}

function getOldVersionResolved (satisfyingVersions, distTags, distTag) {
  return satisfyingVersions[0] === distTags[distTag]
    ? satisfyingVersions[1]
    : satisfyingVersions[0]
}

function getJobsPerGroup ({
  config,
  monorepo,
  dependencyUpdatedAt,
  distTags,
  distTag,
  dependency,
  versions,
  account,
  repositoryId,
  isFromHook,
  log
}) {
  const satisfyingVersions = getSatisfyingVersions(versions, monorepo[0])
  const oldVersionResolved = getOldVersionResolved(satisfyingVersions, distTags, distTag)
  const types = monorepo.map((x) => { return { type: x.value.type, filename: x.value.filename } })

  if (_.isEmpty(config) || _.isEmpty(config.groups)) return []

  const packageFiles = monorepo.map(result => result.value.filename)
  const groups = _.compact(_.map(config.groups, (group, key) => {
    let result = {}
    result[key] = group
    if (_.intersection(group.packages, packageFiles).length) {
      return result
    }
  }))
  return groups.map((group) => {
    // only include changes from
    const groupName = Object.keys(group)[0]
    const relevantMonorepoChangeFiles = monorepo.filter(change => {
      return group[groupName].packages.includes(change.value.filename)
    })
    const version = distTags[distTag]
    if (semver.prerelease(version) && !semver.prerelease(relevantMonorepoChangeFiles[0].value.oldVersion)) {
      log.info(`exited: ${dependency} ${version} is a prerelease on latest and user does not use prereleases`)
      return
    }
    return {
      data: Object.assign({
        name: 'create-group-version-branch',
        group,
        version,
        dependency,
        dependencyUpdatedAt,
        versions,
        repositoryId,
        oldVersionResolved,
        installation: account.installation,
        accountId: account.id || account._id,
        types,
        oldVersion: monorepo[0].value.oldVersion,
        monorepo: relevantMonorepoChangeFiles,
        isFromHook
      })
    }
  })
}

function createTransformFunction (type, dependency, version, log) {
  return (packageJson) => {
    try {
      var json = JSON.parse(packageJson)
      var parsed = jsonInPlace(packageJson)
    } catch (e) {
      return // ignore parse errors
    }
    const oldPkgVersion = _.get(json, [type, dependency])
    if (!oldPkgVersion) {
      log.warn(`exited: could not find old package version for dependency ${dependency}`, { newVersion: version, packageJson: json })
      return
    }

    if (semver.ltr(version, oldPkgVersion)) { // no downgrades
      log.warn('exited: would be a downgrade', { dependency, newVersion: version, oldVersion: oldPkgVersion })
      return
    }

    parsed.set([type, dependency], getRangedVersion(version, oldPkgVersion))
    return parsed.toString()
  }
}

// 'legacy' repoDocs have only true/false set at repository.files['yarn.lock'] ect
// 'newer' repoDocs have an array (empty or with the paths)
// packageFilename: path of pckage.json
const getLockfilePath = function (files, packageFilename) {
  const convertedFiles = _.flatten(Object.keys(files).map(key => {
    if (files[key] === true) return key
    else return files[key]
  }))

  const hasPackageLock = _.includes(convertedFiles, packageFilename.replace('package.json', 'package-lock.json'))
  if (hasPackageLock) return packageFilename.replace('package.json', 'package-lock.json')

  const hasYarnLock = _.includes(convertedFiles, packageFilename.replace('package.json', 'yarn.lock'))
  if (hasYarnLock) return packageFilename.replace('package.json', 'yarn.lock')

  return null
}

const generateGitHubCompareURL = function (fullName, branch, compareWith) {
  // Discussion: https://github.com/greenkeeperio/greenkeeper/issues/682
  // https://github.com/$USER/$REPO/compare/$REV_A...$REV_B
  return `${env.GITHUB_URL}/${fullName}/compare/${encodeURIComponent(branch)}...${encodeURIComponent(fullName.split('/')[0])}:${encodeURIComponent(compareWith)}`
}

const getNodeVersionsFromTravisYML = function (yml) {
  let lines = yml.split('\n')
  const nodeJSIndex = lines.findIndex((line) => {
    return line.replace(/\s/g, '').includes('node_js:')
  })
  let results = {
    startIndex: nodeJSIndex,
    endIndex: nodeJSIndex,
    versions: []
  }
  if (nodeJSIndex === -1) return results
  // Check whether there’s a single node version on the same line: `node_js: 8` instead of an array
  if (lines[nodeJSIndex].replace(/\s/g, '') === 'node_js:') {
    // this is our multi node config
    let lastgetNodeVersionIndex = lines.slice(nodeJSIndex + 1).findIndex((line) => {
      // node_js block ends either in an empty line or a new key, which must include a `:`
      return line.match(/:/) || line.trim().length === 0
    })
    if (lastgetNodeVersionIndex === -1) {
      lastgetNodeVersionIndex = lines.length
    }
    results.endIndex = nodeJSIndex + lastgetNodeVersionIndex
    /*
      This returns an array of node version lines (with dashes and whitespace!):
      [
        "- '4'",
        "- '6'",
        "- '8'",
        "- 'node'"
      ]

    */
    results.versions = lines.slice(nodeJSIndex + 1, nodeJSIndex + lastgetNodeVersionIndex + 1)
  } else {
    // this is our single node version from inline format: `node_js: 8`
    results.versions = new Array(lines[nodeJSIndex].replace(/node_js:/g, '').replace(/\s/g, ''))
  }

  return results
}

// version is a string: 'v8.10', '- "10"', 'Dubnium', "- 'lts/*'" etc
// returns a boolean
// versionOnly: true will _not_ match placeholder strings for current versions, such as `stable`
const hasNodeVersion = function (version, newVersion, newCodeName, versionOnly = false) {
  let matches = [`lts/${newCodeName}`, newCodeName, newVersion] // eslint-disable-line
  const tags = ['node', 'stable', 'lts/\\*']
  if (!versionOnly) {
    matches = matches.concat(tags)
  }
  const overallResult = !!matches.find((match) => {
    // first regex matches in array form ('- 10'), second regex matches the inline form ('10')
    return !!(version.match(RegExp(`([^.])(${match})`, 'i')) || version.match(RegExp(`^${match}`, 'i')))
  })
  return overallResult
}

// existingVersionStrings is just an array of strings: ['- 7', '- 8'] or ['9']  or ['v8.10']
// returns an index
const getNodeVersionIndex = function (existingVersionStrings, newVersion, newCodeName, versionOnly = false) {
  if (!existingVersionStrings || existingVersionStrings.length === 0) return -1
  return existingVersionStrings.findIndex((version) => {
    return hasNodeVersion(version, newVersion, newCodeName, versionOnly)
  })
}

// Stop! YAMLtime!
// existingVersions is the output of getNodeVersionsFromTravisYML
const addNodeVersionToTravisYML = function (travisYML, newVersion, newCodeName, existingVersions) {
  // Should only add the new version if it is not present in any form
  if (existingVersions.versions.length === 0) return travisYML
  const nodeVersionIndex = getNodeVersionIndex(existingVersions.versions, newVersion, newCodeName)
  const travisYMLLines = travisYML.split('\n')
  // We only need to do something if the new version isn’t present
  if (nodeVersionIndex === -1) {
    let delimiter = ''
    let leadingSpaces = ''
    if (existingVersions.versions && existingVersions.versions.length > 0) {
      if (existingVersions.versions[0].match(/"/)) {
        delimiter = '"'
      }
      if (existingVersions.versions[0].match(/'/)) {
        delimiter = "'"
      }
      leadingSpaces = existingVersions.versions[0].match(/^([ ]*)/)[1]
    }
    // splice the new version back onto the end of the node version list in the original travisYMLLines array,
    // unless it wasn’t an array but an inline definition of a single version, eg: `node_js: 9`
    if (existingVersions.versions.length === 1 && existingVersions.startIndex === existingVersions.endIndex) {
      // A single node version was defined in inline format, now we want to define two versions in array format
      travisYMLLines.splice(existingVersions.startIndex, 1, 'node_js:')
      travisYMLLines.splice(existingVersions.startIndex + 1, 0, `${leadingSpaces}- ${existingVersions.versions[0]}`)
      travisYMLLines.splice(existingVersions.startIndex + 2, 0, `${leadingSpaces}- ${delimiter}${newVersion}${delimiter}`)
    } else {
      // Multiple node versions were defined in array format
      travisYMLLines.splice(existingVersions.endIndex + 1, 0, `${leadingSpaces}- ${delimiter}${newVersion}${delimiter}`)
    }
  }
  return travisYMLLines.join('\n')
}
const updateNodeVersionToNvmrc = function (newVersion) {
  return `${newVersion}\n`
}

// existingVersions is the output of getNodeVersionsFromTravisYML
const removeNodeVersionFromTravisYML = function (travisYML, newVersion, newCodeName, existingVersions) {
  // Should only remove the old version if it is actually present in any form
  if (existingVersions.versions.length === 0) return travisYML
  const nodeVersionIndex = getNodeVersionIndex(existingVersions.versions, newVersion, newCodeName, true)
  let travisYMLLines = travisYML.split('\n')
  // We only need to do something if the old version is present
  if (nodeVersionIndex !== -1) {
    // If it’s the only version we don’t want to remove it
    if (existingVersions.versions.length !== 1) {
      // Multiple node versions were defined in array format
      // set lines we want to remove to undefined in existingVersion.versions and filter them out afterwards
      const updatedVersionsArray = _.filter(existingVersions.versions.map((version) => {
        return hasNodeVersion(version, newVersion, newCodeName, true) ? undefined : version
      }), Boolean)
      // splice the updated existingversions into travisymllines
      travisYMLLines.splice(existingVersions.startIndex + 1, existingVersions.endIndex - existingVersions.startIndex, updatedVersionsArray)
      // has an array in an array, needs to be flattened
      travisYMLLines = _.flatten(travisYMLLines)
    }
  }
  return travisYMLLines.join('\n')
}

const addNewLowestAndDeprecate = function ({
  travisYML,
  nodeVersion,
  codeName,
  newLowestVersion,
  newLowestCodeName
}) {
  let versions = getNodeVersionsFromTravisYML(travisYML)
  const updatedTravisYaml = addNodeVersionToTravisYML(travisYML, newLowestVersion, newLowestCodeName, versions)
  versions = getNodeVersionsFromTravisYML(updatedTravisYaml)
  return removeNodeVersionFromTravisYML(updatedTravisYaml, nodeVersion, codeName, versions)
}

const hasTooManyPackageJSONs = function (repo) {
  return repo.packages && Object.keys(repo.packages).length > 300
}

module.exports = {
  seperateNormalAndMonorepos,
  getJobsPerGroup,
  filterAndSortPackages,
  getSatisfyingVersions,
  getOldVersionResolved,
  getHighestPriorityDependency,
  createTransformFunction,
  generateGitHubCompareURL,
  getNodeVersionsFromTravisYML,
  hasNodeVersion,
  getNodeVersionIndex,
  addNodeVersionToTravisYML,
  removeNodeVersionFromTravisYML,
  updateNodeVersionToNvmrc,
  addNewLowestAndDeprecate,
  hasTooManyPackageJSONs,
  getLockfilePath
}


const _ = require('lodash')
const Log = require('gk-log')

const { getInfos, getFormattedDependencyURL } = require('./get-infos')
const openIssue = require('./open-issue')
const statsd = require('./statsd')
const upsert = require('./upsert')
const dbs = require('./dbs')
const githubQueue = require('./github-queue')
const { generateGitHubCompareURL } = require('../utils/utils')

module.exports = async function (
  { installationId, accountId, repository, branchDoc, combined }
) {
  const { repositories, npm } = await dbs()
  const logs = dbs.getLogsDb()
  const log = Log({ logsDb: logs, accountId, repoSlug: repository.full_name, context: 'handle-branch-status' })
  const [owner, repo] = repository.full_name.split('/')
  const repositoryId = String(repository.id)
  const {
    purpose,
    dependency,
    monorepoGroupName,
    version,
    oldVersionResolved,
    base,
    head,
    dependencyType
  } = branchDoc
  const packageUpdateList = branchDoc.packageUpdateList || `The \`${dependencyType.replace('ies', 'y')}\` **${dependency}** was updated from \`${oldVersionResolved}\` to \`${version}\`.`
  const ghqueue = githubQueue(installationId)

  log.info('started', { branchDoc: { purpose,
    dependency,
    monorepoGroupName,
    version,
    oldVersionResolved } })

  const dependencyKey = monorepoGroupName || dependency
  const issue = _.get(
    await repositories.query('issue_open_by_dependency', {
      key: [repositoryId, dependencyKey],
      include_docs: true
    }),
    'rows[0].doc'
  )
  const { number } = issue || {}

  const change = {
    statuses: combined.statuses,
    processed: true,
    state: combined.state
  }

  if (!issue && combined.state === 'success') {
    try {
      await ghqueue.write(github => github.gitdata.deleteRef({
        owner,
        repo,
        ref: 'heads/' + head
      }))
    } catch (e) {
      if (e.status !== 422) throw e
    }

    await upsert(
      repositories,
      branchDoc._id,
      Object.assign(
        {
          referenceDeleted: true
        },
        change
      )
    )
    return
  }

  branchDoc = await upsert(repositories, branchDoc._id, change)

  const compareURL = generateGitHubCompareURL(repository.full_name, base, head)

  if (purpose === 'pin') {
    if (!issue) throw new Error('Inconsistent state')

    await ghqueue.write(github => github.issues.createComment({
      owner,
      repo,
      number,
      body: combined.state === 'success'
        ? `After pinning ${branchDoc.group ? `group **${branchDoc.group}** ` : ''}to **${version}** your tests are passing again. [Downgrade this dependency 📌](${compareURL}).`
        : `After pinning ${branchDoc.group ? `group **${branchDoc.group}** ` : ''}to **${version}** your tests are still failing. The reported issue _might_ not affect your project. These imprecisions are caused by inconsistent test results.`
    }))

    log.info(`Created issue comment: Pin dependency to version ${version}`)

    statsd.increment('issue_comments')

    return
  }

  const { versions } = await npm.get(dependency)
  const diffBase = issue
    ? _.get(issue, 'comments.length') ? _.last(issue.comments) : issue.version
    : oldVersionResolved

  const repositoryURL = _.get(versions, `['${version}'].repository.url`, '')
  const dependencyLink = getFormattedDependencyURL({ repositoryURL })
  const { release, diffCommits } = await getInfos({
    installationId,
    dependency,
    monorepoGroupName,
    version,
    diffBase,
    versions
  })

  if (!issue && combined.state === 'failure') {
    await openIssue({
      installationId,
      owner,
      repo,
      repositoryId,
      accountId,
      version,
      dependency: dependencyKey,
      dependencyType,
      oldVersionResolved,
      base,
      head,
      dependencyLink,
      release,
      diffCommits,
      statuses: combined.statuses,
      monorepoGroupName,
      packageUpdateList
    })

    log.info('Created failure issue')
    return
  }
  const bodyDetails = _.compact(['\n', release, diffCommits]).join('\n')
  const commitAmount = packageUpdateList.match(/was updated from/g).length
  const failureCommentBody = (packageUpdateList + `\n\n**Your tests are still failing with ${commitAmount <= 1 ? 'this version' : 'these versions'}.** [Compare changes](${compareURL})\n${bodyDetails}`).trim()

  if (combined.state === 'failure') {
    if (hasVersionComment(issue, version)) {
      return
    }
    await ghqueue.write(github => github.issues.createComment({
      owner,
      repo,
      number,
      body: failureCommentBody
    }))

    statsd.increment('issue_comments')

    await upsert(repositories, issue._id, {
      comments: [...(issue.comments || []), version]
    })

    log.info('Commented on open issue: tests are still failing.')
    return
  }

  const successCommentBody = `${packageUpdateList}\n\nYour tests ${branchDoc.group ? `for group **${branchDoc.group}** ` : ''}are passing again with this update. [Explicitly upgrade ${branchDoc.group ? `**${branchDoc.group}** ` : ''}to ${commitAmount <= 1 ? 'this version' : 'these versions'} 🚀](${compareURL})\n\n${bodyDetails}`.trim()

  await ghqueue.write(github => github.issues.createComment({
    owner,
    repo,
    number,
    body: successCommentBody
  }))
  log.info('Commented on issue: tests are passing again.')

  statsd.increment('issue_comments')

  // Not closing the issue, so decision whether to explicitly upgrade or just close is with the user
  // await github.issues.update({owner, repo, number, state: 'closed'})

  function hasVersionComment (issue, version) {
    if (!issue.version && !issue.comments) {
      log.error('no version information on issue document', { issue })
      return false
    }
    return issue.version === version || (issue.comments && issue.comments.includes(version))
  }
}


const _ = require('lodash')
const crypto = require('crypto')

const updatedAt = require('./updated-at')
const {
  getFiles,
  formatPackageJson,
  getGreenkeeperConfigFile,
  getPackagePathsFromConfigFile
} = require('./get-files')
const { validate } = require('./validate-greenkeeper-json')

module.exports = {
  createDocs,
  updateRepoDoc,
  updateDoc
}

// trigger (several) initial-subgroup-pr(s):
// - if a package.json is added/renamed/moved in the greenkeeper.json
// - if a greenkeeper.json is added

async function updateRepoDoc ({ installationId, doc, filePaths, log }) {
  const fullName = doc.fullName
  const oldGreenkeeperConfig = doc.greenkeeper
  // set a default empty config so the job can continue if the file get fails for some reason
  let greenkeeperConfigFile = {}
  try {
    greenkeeperConfigFile = await getGreenkeeperConfigFile(installationId, fullName, log)
  } catch (e) {
    throw e
  } finally {
    if (!_.isEmpty(greenkeeperConfigFile)) {
      log.info('UpdateRepoDoc: Fetched greenkeeper.json from GitHub', greenkeeperConfigFile)
    }
    _.set(doc, ['greenkeeper'], greenkeeperConfigFile)
    const defaultFiles = {
      'package.json': [],
      'package-lock.json': [],
      'yarn.lock': [],
      'npm-shrinkwrap.json': []
    }
    let filePathsFromConfig = []

    if (!_.isEmpty(greenkeeperConfigFile)) {
      if (validate(greenkeeperConfigFile).error) {
        log.info('UpdateRepoDoc: setting file paths to the ones from the old greenkeeper.json')
        filePathsFromConfig = getPackagePathsFromConfigFile(oldGreenkeeperConfig)
      } else {
        log.info('UpdateRepoDoc: setting file paths to the ones found via greenkeeper.json')
        filePathsFromConfig = getPackagePathsFromConfigFile(greenkeeperConfigFile)
      }
    }

    // try to get file paths from either the autodiscovered filePaths
    // or from the greenkeeper.json
    if (!_.isEmpty(filePaths)) {
      log.info('UpdateRepoDoc: setting file paths to the ones found per autodiscovery')
      filePathsFromConfig = getPackagePathsFromConfigFile({ groups: { default: { packages: filePaths } } })
    }
    log.info('UpdateRepoDoc: requesting files from GitHub', { files: filePathsFromConfig })
    const filesFromConfig = _.isEmpty(filePathsFromConfig)
      ? await getFiles({ installationId, fullName, sha: doc.headSha, log })
      : await getFiles({ installationId, fullName, files: filePathsFromConfig, sha: doc.headSha, log })

    const files = _.merge(filesFromConfig, defaultFiles)
    // handles multiple paths for files like this:
    // files: {
    //   package.json: ['package.json', 'backend/package.json', 'frontend/package.json']
    //   package-lock.json: ['package-lock.json', 'backend/package-lock.json']
    //   npm-shrinkwrap.json: [],
    //   yarn.lock: []
    // }
    doc.files = _.mapValues(files, fileType => fileType
      .filter(file => !!file.content)
      .map(file => file.path))

    // formats *all* the package.json files
    const pkg = formatPackageJson(files['package.json'])

    if (!pkg) {
      _.unset(doc, ['packages'])
    } else {
      _.set(doc, ['packages'], pkg)
    }

    log.info('UpdateRepoDoc: doc updated', { doc })
  }
}

function createDocs ({ repositories, accountId }) {
  return repositories.map(repo => updatedAt({
    _id: String(repo.id),
    type: 'repository',
    enabled: false,
    accountId,
    fullName: repo.full_name,
    private: repo.private,
    fork: repo.fork,
    hasIssues: repo.has_issues,
    accountToken: crypto.randomBytes(32).toString('hex'),
    packages: {}
  }))
}

function updateDoc (repositories, repository, repoDoc) {
  return repositories.put(
    updatedAt(
      Object.assign(repoDoc, {
        private: repository.private,
        fullName: repository.full_name,
        fork: repository.fork,
        hasIssues: repository.has_issues
      })
    )
  )
}


const _ = require('lodash')
const statsd = require('../lib/statsd')

const githubQueue = require('./github-queue')

const fileList = [
  'package.json',
  'package-lock.json',
  'npm-shrinkwrap.json',
  'yarn.lock'
]

async function getGithubFile (ghqueue, { path, owner, repo, sha, ref }, log) {
  let name = path
  if (path.includes('/')) name = path.split('/')[1]

  try {
    statsd.increment('get_github_file_small', { tag: name })
    if (ref) {
      return await ghqueue.read(github => github.repos.getContents({ path, owner, repo, ref }))
    }
    return await ghqueue.read(github => github.repos.getContents({ path, owner, repo }))
  } catch (error) {
    // Sometimes, the requested blob (eg. package-lock.json) is too large to fetch via the gitHub API,
    // but we can use the Git Data API to request blobs up to 100 MB in size.
    if (error.errors && error.errors.find(error => error.code === 'too_large')) {
      log.info('Could not receive contents from GitHub. File is too, trying the data api for larger files now', { error: error.message })
      statsd.increment('get_github_file_large', { tag: name })
      try {
        const allFiles = await ghqueue.read(github => github.gitdata.getTree({ owner, repo, tree_sha: sha, recursive: 1 }))
        const file = allFiles.tree.find(file => file.path === path)
        const blob = await ghqueue.read(github => github.gitdata.getBlob({ owner, repo, file_sha: file.sha }))

        return { name, path, content: blob.content }
      } catch (error) {
        log.info('Could not receive recursive tree from GitHub.', { error: error.message })
        return { name: path, path, content: false }
      }
    } else {
      return { name: path, path, content: false }
    }
  }
}

// Returns an array of objects, where each object describes a `package.json`:
// [{"content": "eyJuYW1lIjoidGVzdCJ9", "name": "package.json", "path": "frontend/package.json", "type": "file"}]
async function discoverPackageFiles ({ installationId, fullName, defaultBranch, log }) {
  const ghqueue = githubQueue(installationId)
  const relevantPackageFilePaths = await discoverPackageFilePaths({ installationId, fullName, defaultBranch, log })
  const [owner, repo] = fullName.split('/')
  // Fetch the content for each relevant package.json file
  const packageFiles = await Promise.all(
    relevantPackageFilePaths.map((path) => getGithubFile(ghqueue, { path, owner, repo }))
  )

  log.info(`got ${packageFiles.length} package.json files`)
  return packageFiles
}

// Returns an array of paths of `package.json` files:
// [ 'package.json', 'frontend/package.json', 'backend/package.json' ]
async function discoverPackageFilePaths ({ installationId, fullName, defaultBranch, log }) {
  // https://api.github.com/repos/neighbourhoodie/gk-test-lerna-yarn-workspaces/git/trees/master?recursive=1
  const [owner, repo] = fullName.split('/')
  const ghqueue = githubQueue(installationId)
  try {
    const result = (await ghqueue.read(github => github.gitdata.getTree({ owner, repo, tree_sha: defaultBranch, recursive: 1 })))
    const filesInRepo = result.tree && result.tree.length ? result.tree : []
    // Construct an array of all relevant package.json paths
    const relevantPackageFilePaths = filesInRepo.map((item) => {
      // Just pick out the paths, eg. `packages/retext-dutch/package.json`
      return item.path
    }).filter((item) => {
      // We don’t want any package.json files from `node_modules`
      return !(item.includes('node_modules') || item.includes('test/') || item.includes('tests/') || item.includes('elm-package.json')) &&
          item.match(/(.+\/package.json$|^package.json$)/)
    })
    log.info('relevant package file paths', { relevantPackageFilePaths })
    return relevantPackageFilePaths
  } catch (error) {
    log.warn(`An error occured when requesting the recursive git-tree of ${defaultBranch}`, { error: error.message })
    return []
  }
}

function addRelatedLockfilePaths (files) {
  // return standard file list
  if (_.isEmpty(files) || files === ['package.json']) return fileList
  // nothing to add
  if (_.isEqual(fileList, files)) return []
  let relatedLockfilePaths = []
  files.map(path => {
    relatedLockfilePaths.push(path.replace('package.json', 'package-lock.json'))
    relatedLockfilePaths.push(path.replace('package.json', 'yarn.lock'))
    relatedLockfilePaths.push(path.replace('package.json', 'npm-shrinkwrap.json'))
  })
  return relatedLockfilePaths
}

async function getFiles ({ installationId, fullName, files = fileList, sha, log }) {
  // Take the package.json paths and look for the lockfiles too!
  const filesAndLockfiles = files.concat(addRelatedLockfilePaths(files))
  const ghqueue = githubQueue(installationId)
  const [owner, repo] = fullName.split('/')
  const filesRequested = await Promise.all(
    filesAndLockfiles.map((path) => getGithubFile(ghqueue, { path, owner, repo, sha }, log))
  )

  // returns an object of all files that were
  // supplied grouped together by their name (`yarn.lock`):
  //
  // { 'package.json': [
  //   { path: 'package.json',
  //     name: 'package.json',
  //     content: 'eyJuYW1lIjoidGVzdCJ9', ... },
  //   { path: 'backend/package.json',
  //     name: 'package.json',
  //     content: 'eyJuYW1lIjoidGVzdCJ9', ... }]
  //   'yarn.lock': [
  //   { path: 'backend/yarn.lock',
  //     name: 'yarn.lock',
  //     content: 'djadhkjawdka', ...}]
  // }

  const groupedFiles = _.groupBy(filesRequested, file => file.name)
  return _.pick(groupedFiles, fileList)
}

async function getGreenkeeperConfigFile (installationId, fullName, log = console) {
  const ghqueue = githubQueue(installationId)
  const [owner, repo] = fullName.split('/')
  const path = 'greenkeeper.json'
  let greenkeeperConfigFile = {}
  let parsedConfigFile = {}
  log.info('Fetching greenkeeper.json from GitHub', { path, owner, repo })
  greenkeeperConfigFile = await getGithubFile(ghqueue, { path, owner, repo })
  if (!greenkeeperConfigFile.content) {
    log.info('No greenkeeper.json in repo')
    // return empty object, so job can continue
    return {}
  }

  try {
    parsedConfigFile = JSON.parse(Buffer.from(greenkeeperConfigFile.content, 'base64'))
  } catch (e) {
    log.error('Could not parse greenkeeper.json', { error: e })
    // throw error, so we can raise an issue
    const error = new Error('Could not parse `greenkeeper.json`, it appears to not be a valid JSON file.')
    error.name = 'GKConfigFileParseError'
    throw error
  }

  return parsedConfigFile
}

function getPackagePathsFromConfigFile (configFile) {
  if (_.isEmpty(configFile)) return null
  let pathList = []
  const groups = configFile.groups
  if (_.isEmpty(groups)) return null
  pathList = _.flatten(Object.keys(groups).map(group => groups[group].packages))

  return pathList
}

function formatPackageJson (content) {
  if (!content) return null
  const packages = {}

  content.map(packageJson => {
    try {
      var pkg = JSON.parse(Buffer.from(packageJson.content, 'base64'))
    } catch (e) {
      return null
    }
    packages[packageJson.path] = _.pick(pkg, [
      'name',
      'dependencies',
      'devDependencies',
      'optionalDependencies',
      'peerDependencies',
      'greenkeeper',
      'engines',
      'maintainers',
      'author'
    ])
  })

  // returns an Object with all the parsed package.json files:
  //
  // {
  //   "package.json": {},
  //   "backend/package.json": {}
  //   ...
  // }

  return packages
}

module.exports = {
  getFiles,
  getGithubFile,
  formatPackageJson,
  discoverPackageFiles,
  discoverPackageFilePaths,
  getGreenkeeperConfigFile,
  getPackagePathsFromConfigFile
}


const _ = require('lodash')
const md = require('./template')
const env = require('../lib/env')

module.exports = prBody

const branchFailed = () => md`
💥 Tests on this branch are failing. Here’s how to proceed.

To solve the issue, first find out which of the dependency’s updates is causing the problem. Then fix your code to accomodate the changes in the updated dependency. [next-update](https://www.npmjs.com/package/next-update) is a really handy tool to help you with this.

Then push your changes to this branch and merge it.
`

const enablePrivatePackage = ({ installationId, secret }) => `
📦 How to enable private scoped packages

Public scoped packages (\`@scope/name\`) work out of the box, but private scoped packages require an additional setup step in which you tell npm to alert Greenkeeper every time the package or scope you specify is updated.

This step only needs to be run once per Greenkeeper _installation_, meaning once per GitHub organisation or user account. You will need to run it again if you ever remove and re-add the Greenkeeper installation on org or user account level, but _not_ if you’re just adding, removing or resetting repositories.

- These commands can be run on your local machine
- You must be authenticated for the npm registry in the terminal session you’re using (see [npm login](https://docs.npmjs.com/creating-a-new-npm-user-account#testing-your-new-account-with-npm-login))
- This is a one-time operation, once you’ve registered the hook with npm, Greenkeeper will get updates until you tell npm to stop sending them, or you uninstall Greenkeeper on your org or user account.
- ⚠ Make sure you replace the placeholders below with the actual values you need:
  - \`SCOPED_PACKAGE_NAME\`: A full scoped package name, such as \`@megacorp/widget\`
  - \`SCOPE_NAME\`: Just the scope name: \`@megacorp\`
  - \`OWNER_NAME\`: An npm username: \`substack\`

\`\`\`bash
# Some of the things you can do with npm hooks:
# Add a single private scoped package
npm hook add SCOPED_PACKAGE_NAME https://${env.HOOKS_HOST}/npm/${installationId} ${secret}

# Add all private packages in a scope
npm hook add SCOPE_NAME https://${env.HOOKS_HOST}/npm/${installationId} ${secret}

# Add all private packages by a specific owner
npm hook add --type owner OWNER_NAME https://${env.HOOKS_HOST}/npm/${installationId} ${secret}
\`\`\`

For additional options and information, please consult the [npm CLI docs](
https://docs.npmjs.com/cli/hook.html).

If you are using npm version 5 or below, you need a separate tool called wombat to do this. Globally install wombat via npm and then use the same commands as above, but with \`wombat\` instead of \`npm\`. More in the [Wombat docs](https://www.npmjs.com/package/wombat).
`

const badgeAddedText = ({ badgeUrl }) => md`
🏷 How to check the status of this repository

Greenkeeper adds a badge to your README which indicates the status of this repository.

This is what your badge looks like right now :point_right:  ![Greenkeeper badge](${badgeUrl})
`

const travisModifiedText = () => md`
🏗 How to configure Travis CI

Greenkeeper has added a rule to your \`.travis.yml\` that whitelists Greenkeeper branches, which are created when your dependencies are updated. Travis CI will run your tests on these branches automatically to see if they still pass.

No additional setup is required 😊

`

const updatePullRequestText = ({ ghRepo, newBranch }) => md`
👩💻 How to update this pull request

\`\`\`bash
  # Change into your repository’s directory
  git fetch --all
  git checkout ${newBranch}
  npm install-test
  # Adapt your code until everything works again
  git commit -m 'chore: adapt code to updated dependencies'
  git push ${ghRepo.clone_url} ${newBranch}
\`\`\`
`

const howToIgnoreDependencies = ({ ghRepo, newBranch }) => md`
🙈 How to ignore certain dependencies

You may have good reasons for not wanting to update to a certain dependency right now. In this case, you can [change the dependency’s version string in the \`package.json\` file back to whatever you prefer](${ghRepo.html_url}/edit/${newBranch}/package.json).

To make sure Greenkeeper doesn’t nag you again on the next update, add a \`greenkeeper.ignore\` field to your \`package.json\`, containing a list of dependencies you don’t want to update.

\`\`\`js
// package.json
{
  …
  "greenkeeper": {
    "ignore": [
      "package-names",
      "you-want-me-to-ignore"
    ]
  }
}
\`\`\`
`

const howToIgnoreDependenciesInGroup = ({ ghRepo, newBranch }) => md`
🙈 How to ignore certain dependencies for this group

You may have good reasons for not wanting to update to a certain dependency right now. In this case, you can [change the dependency’s version string in the \`package.json\` file back to whatever you prefer](${ghRepo.html_url}/edit/${newBranch}/package.json).

To make sure Greenkeeper doesn’t nag you again on the next update of this group, you can add the dependency to this group’s \`ignore\` field in the \`greenkeeper.json\`, for example:
\`\`\`js
// greenkeeper.json
{
  "groups": {
    "frontend": {
      "packages": [
        "frontend/package.json",
        "admin-dashboard/package.json"
      ],
      "ignore": [
        "eslint",
        "standard"
      ]
    }
  }
}
\`\`\`
`

const howTheUpdatesWillLookLike = () => md`
✨ How do dependency updates work with Greenkeeper?

After you merge this pull request, **Greenkeeper will create a new branch whenever a  dependency is updated**, with the new version applied. The branch creation should trigger your testing services and check whether your code still works with the new dependency version. Depending on the the results of these tests Greenkeeper will try to open meaningful and helpful pull requests and issues, so your dependencies remain working and up-to-date.

\`\`\`diff
-  "underscore": "^1.6.0"
+  "underscore": "^1.7.0"
\`\`\`

The above example shows an in-range update. \`1.7.0\` is included in the old \`^1.6.0\` range, because of the [caret \`^\` character ](https://docs.npmjs.com/misc/semver#ranges).
When the test services report success Greenkeeper will silently delete the branch again, because no action needs to be taken – everything is fine.

However, should the tests fail, Greenkeeper will create an issue to inform you about the problem immediately.

This way, you’ll never be surprised by a dependency breaking your code. As long as everything still works, Greenkeeper will stay out of your way, and as soon as something goes wrong, you’ll be the first to know.

\`\`\`diff
-  "lodash": "^3.0.0"
+  "lodash": "^4.0.0"
\`\`\`

In this example, the new version \`4.0.0\` is _not_ included in the old \`^3.0.0\` range.
For version updates like these – let’s call them “out of range” updates – you’ll receive a pull request.

This means that **you no longer need to check for new versions manually** – Greenkeeper will keep you up to date automatically.

These pull requests not only serve as reminders to update: If you have solid tests and good coverage, and the pull requests passes those tests, you can very likely just merge it and release a new version of your software straight away :shipit:

To get a better idea of which ranges apply to which releases, check out the extremely useful [semver calculator](https://semver.npmjs.com/) provided by npm.
`

const faqText = () => md`
FAQ and help

There is a collection of [frequently asked questions](https://greenkeeper.io/faq.html). If those don’t help, you can always [ask the humans behind Greenkeeper](https://github.com/greenkeeperio/greenkeeper/issues/new).
`

// needs to handle files as an array of arrays!
function hasLockFileText (files) {
  if (!files) return
  const lockFiles = ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'].filter((key) => {
    if (_.isArray(files[key]) && files[key].length) {
      return true
    }
    if (files[key] === true) {
      return true
    }
    return false
  })
  if (lockFiles.length === 0) return
  if (lockFiles.includes('npm-shrinkwrap.json')) {
    return md`⚠ Greenkeeper has found a ${md.code('npm-shrinkwrap.json')} file in this repository. Please use [greenkeeper-lockfile](https://github.com/greenkeeperio/greenkeeper-lockfile) to make sure this gets updated as well.`
  }
  const lockFile = lockFiles[0]
  return md`🔒 Greenkeeper has found a ${md.code(lockFile)} file in this repository. Greenkeeper supports lockfile updates for public packages. If you use private packages in your repository, please use [greenkeeper-lockfile](https://github.com/greenkeeperio/greenkeeper-lockfile) to make sure these can get updated as well.`
}

const mainMessage = ({ enabled, depsUpdated, groupName }) => {
  if (groupName && depsUpdated) return md`This pull request **updates all your dependencies in the group \`${groupName}\` to their latest version**. Having them all up to date really is the best starting point for keeping up with new releases. As long as you have the group defined in your \`greenkeeper.json\`, Greenkeeper will look out for further dependency updates relevant to this group and make sure to always handle them together and in real-time.`
  if (enabled) return 'All of your dependencies are already up-to-date, so this repository was enabled right away. Good job :thumbsup:'
  if (depsUpdated) return 'This pull request **updates all your dependencies to their latest version**. Having them all up to date really is the best starting point for keeping up with new releases. Greenkeeper will look out for further dependency updates and make sure to handle them in isolation and in real-time, but only after **you merge this pull request**.'
  if (!enabled && !depsUpdated) return 'This pull request only adds the Greenkeeper badge to your readme file, since all of the dependencies were already up to date. Greenkeeper will look out for further dependency updates and make sure to handle them in isolation and in real-time, but only after **you merge this pull request**.'
  return '' // no updates, but private repository
}

const greenkeeperConfigInfoMessage = (info) => {
  if (!info) return ''
  let message = ''
  if (info.isMonorepo) {
    message += '📦 📦  Greenkeeper has detected multiple `package.json` files. '
  }
  if (info.action === 'new') {
    message += 'They have all been added to a new `greenkeeper.json` config file. They’ve been collected in a group called `default`, meaning that all of them will receive updates together. You can rename, add and remove groups and freely assign each `package.json` to whichever group you like. It’s common, for example, to have one `frontend` group and one `backend` group, each with a couple of `package.json` files. In any case, all files in a group will have their updates collected into single PRs and issues. '
  }
  if (info.action === 'updated') {
    message += 'Since this repo already has a `greenkeeper.json` config file with defined groups, Greenkeeper has only checked whether they’re still valid. '
    if (info.deletedPackageFiles.length > 0) {
      message += 'The follwing `package.json` files could no longer be found in the repo and have been removed from your groups config: `' + info.deletedPackageFiles.join(', ') + '`. '
    }
    if (info.deletedGroups.length > 0) {
      message += 'Also, groups which no longer have any entries have been removed: `' + info.deletedGroups.join(', ') + '`. '
    }
  }
  if (info.action === 'added-groups-only') {
    message += 'Since this repo already has a `greenkeeper.json` config file without any defined groups, Greenkeeper has  added all of the `package.json` files to a group called `default`, meaning that all of them will receive updates together. You can rename, add and remove groups and freely assign each `package.json` to whichever group you like. It’s common, for example, to have one `frontend` group and one `backend` group, each with a couple of `package.json` files. In any case, all files in a group will have their updates collected into single PRs and issues. '
  }
  return message
}

const closeMessages = (issueNumbers) => {
  if (issueNumbers && issueNumbers.length > 0) {
    return '\n\nCloses: #' + issueNumbers.join(', #')
  }
}

function prBody ({ ghRepo, success, secret, installationId, newBranch, badgeUrl, travisModified, enabled, depsUpdated, accountTokenUrl, files, greenkeeperConfigInfo, groupName, closes }) {
  return md`
${!groupName && `Let’s get started with automated dependency management for ${ghRepo.name} :muscle:`}

${!groupName && hasLockFileText(files)}

${mainMessage({ enabled, depsUpdated, groupName })}

${!groupName && !enabled && '**Important: Greenkeeper will only start watching this repository’s dependency updates after you merge this initial pull request**.'}

${!env.IS_ENTERPRISE && !groupName && secret && accountTokenUrl && `💸  **Warning** 💸 Enabling Greenkeeper on this repository by merging this pull request might increase your monthly payment. If you’re unsure, please [check your billing status](${accountTokenUrl}).`}

${!groupName && greenkeeperConfigInfoMessage(greenkeeperConfigInfo)}

---
${
  _.compact([
    depsUpdated && !success && branchFailed(),
    secret && enablePrivatePackage({ secret, installationId }),
    badgeUrl && badgeAddedText({ badgeUrl }),
    travisModified && travisModifiedText(),
    groupName ? howToIgnoreDependenciesInGroup({ ghRepo, newBranch }) : howToIgnoreDependencies({ ghRepo, newBranch }),
    updatePullRequestText({ ghRepo, newBranch }),
    howTheUpdatesWillLookLike(),
    faqText()
  ]).map(text => `<details>${text}</details>`)
}
${closeMessages(closes)}

---

Good luck with your project and see you soon :sparkles:

Your [Greenkeeper](https://greenkeeper.io) bot :palm_tree:
`
}


const _ = require('lodash')
const Log = require('gk-log')
const dbs = require('../lib/dbs')
const githubQueue = require('../lib/github-queue')
const yaml = require('js-yaml')
const jsonInPlace = require('json-in-place')
const semver = require('semver')
const createBranch = require('../lib/create-branch')
const getConfig = require('../lib/get-config')
const {
  hasNodeVersion,
  getNodeVersionIndex,
  getNodeVersionsFromTravisYML,
  addNodeVersionToTravisYML,
  updateNodeVersionToNvmrc
} = require('../utils/utils')
const upsert = require('../lib/upsert')
const issueContent = require('../content/nodejs-release-issue')

module.exports = async function ({ repositoryFullName, nodeVersion, codeName }) {
  nodeVersion = nodeVersion.toString()
  repositoryFullName = repositoryFullName.toLowerCase()
  // find the repository in the database
  const { repositories, installations } = await dbs()
  const repoDoc = _.get(
    await repositories.query('by_full_name', {
      key: repositoryFullName,
      include_docs: true
    }),
    'rows[0].doc'
  )

  if (!repoDoc) {
    const error = new Error(`The repository ${repositoryFullName} does not exist in the database`)
    error.status = 404
    throw error
  }

  const repositoryId = _.get(repoDoc, '_id')
  const accountId = repoDoc.accountId
  const logs = dbs.getLogsDb()
  const log = Log({ logsDb: logs, accountId, repoSlug: repoDoc.fullName, context: 'update-nodejs-version' })

  const existingBranches = await repositories.query('branch_by_dependency', {
    key: [repositoryId, `node-${nodeVersion}`, 'node-update'],
    include_docs: false
  })

  if (existingBranches && existingBranches.rows.length !== 0) {
    log.warn(`exited: branchDoc for update to ${nodeVersion} already exists`, existingBranches.rows[0])
    return
  }

  const installation = await installations.get(accountId)
  const installationId = installation.installation

  const config = getConfig(repoDoc)
  const { branchPrefix, label } = config
  log.info(`config for ${repoDoc.fullName}`, { config })
  if (config.disabled) {
    log.warn('exited: Greenkeeper is disabled for this repo in package.json')
    return
  }

  // 1. fetch .travis.yml
  function travisTransform (travisYML) {
    try {
      var travisJSON = yaml.safeLoad(travisYML, {
        schema: yaml.FAILSAFE_SCHEMA
      })
    } catch (e) {
      // ignore .travis.yml if it can not be parsed
      return
    }
    // No node versions specified in root level of travis YML
    // There may be node versions defined in the matrix or jobs keys, but those can become
    // far too complex for us to handle, so we don’t
    if (!_.get(travisJSON, 'node_js')) return

    const nodeVersionFromYaml = getNodeVersionsFromTravisYML(travisYML)
    const hasNodeVersion = getNodeVersionIndex(nodeVersionFromYaml.versions, nodeVersion, codeName) !== -1
    if (hasNodeVersion) return

    const updatedTravisYaml = addNodeVersionToTravisYML(travisYML, nodeVersion, codeName, nodeVersionFromYaml)
    return updatedTravisYaml
  }

  function nvmrcTransform (nvmrc) {
    if (!nvmrc) return nvmrc
    if (hasNodeVersion(nvmrc, nodeVersion, codeName)) return nvmrc

    const updatedNvmrc = updateNodeVersionToNvmrc(nodeVersion)
    return updatedNvmrc
  }

  let transforms = [
    {
      path: '.travis.yml',
      message: `Update to node ${nodeVersion} in .travis.yml`,
      transform: raw => travisTransform(raw)
    },
    {
      path: '.nvmrc',
      message: `Update to node ${nodeVersion} in .nvmrc`,
      transform: raw => nvmrcTransform(raw)
    }
  ]

  const packageJsonContents = _.get(repoDoc, ['packages'])
  const packagePaths = _.keys(packageJsonContents)

  let engineTransformMessages = {
    tooComplicated: 0,
    inRange: 0,
    updated: 0
  }

  // Check and possibly update all package.jsons
  packagePaths.map((packagePath) => {
    transforms.push({
      path: packagePath,
      message: `Update engines to node ${nodeVersion} in ${packagePath}`,
      transform: oldPkg => {
        const oldPkgParsed = JSON.parse(oldPkg)
        const inplace = jsonInPlace(oldPkg)
        const currentEngines = _.get(oldPkgParsed, 'engines.node')
        if (currentEngines) {
          if (semver.satisfies(semver.coerce(nodeVersion), currentEngines)) {
            engineTransformMessages.inRange++
          } else {
            if (currentEngines.match(/[=> <~]+/g)) {
              engineTransformMessages.tooComplicated++
              return
            }
            engineTransformMessages.updated++
            inplace.set('engines.node', nodeVersion)
            return inplace.toString()
          }
        }
      }
    })
  })

  const [owner, repo] = repoDoc.fullName.split('/')

  const ghRepo = await githubQueue(installationId).read(github => github.repos.get({ owner, repo })) // wrap in try/catch
  const branch = ghRepo.default_branch
  const newBranch = branchPrefix + 'update-to-node-' + nodeVersion

  log.info('github: repository info', {
    repositoryInfo: ghRepo,
    installationId,
    owner,
    repo,
    branch,
    newBranch,
    transforms
  })

  const sha = await createBranch({ // try/catch
    installationId,
    owner,
    repoName: repo,
    branch,
    newBranch,
    transforms,
    commitMessageTemplates: config.commitMessages
  })

  if (sha) {
    log.info('Created branch for node update')
    const travisModified = transforms[0].created
    const nvmrcModified = transforms[1].created

    const branchData = {
      type: 'branch',
      initial: false,
      sha,
      base: branch,
      head: newBranch,
      processed: false,
      travisModified,
      nvmrcModified,
      engineTransformMessages,
      repositoryId,
      dependency: `node-${nodeVersion}`,
      dependencyType: 'node-update'
    }

    log.info('Creating branch doc', branchData)
    await upsert(repositories, `${repositoryId}:branch:${sha}`, branchData)

    // 4. Write issue and save issue doc
    const body = issueContent({
      owner,
      repo,
      base: branch,
      head: newBranch,
      nodeVersion,
      codeName,
      travisModified,
      nvmrcModified,
      engineTransformMessages
    })
    const { number } = await githubQueue(installationId).write(github => github.issues.create({
      owner,
      repo,
      title: `Version ${nodeVersion} of node.js has been released`,
      body,
      labels: [label]
    }))

    await upsert(repositories, `${repositoryId}:issue:${number}`, {
      type: 'issue',
      repositoryId,
      number,
      state: 'open',
      dependency: `node-${nodeVersion}`,
      dependencyType: 'node-update'
    })
  }
}


const _ = require('lodash')
const dbs = require('../lib/dbs')
const env = require('../lib/env')
const updatedAt = require('../lib/updated-at')

const monorepoBaseDefinitions = require('greenkeeper-monorepo-definitions')

let dbDefinitions = JSON.parse(JSON.stringify(monorepoBaseDefinitions))
let dbDefinitionsLastUpdated = 0

async function getMonorepoDefinitions () {
  const { monorepo } = await dbs()

  if (dateMinusMinutes(5) > dbDefinitionsLastUpdated || env.NODE_ENV === 'testing') {
    const allDocs = await monorepo.allDocs({ include_docs: true })
    dbDefinitionsLastUpdated = new Date()
    _.forEach(allDocs.rows, row => {
      const doc = row.doc
      if (!doc.packages) { return }
      dbDefinitions[doc._id] = doc.packages
    })
  }

  return dbDefinitions
}

async function getMonorepoGroup (groupname) {
  const monorepoDefinitions = await getMonorepoDefinitions()
  return monorepoDefinitions[groupname] || undefined
}

async function getMonorepoGroupNameForPackage (packageName) {
  const monorepoDefinitions = await getMonorepoDefinitions()
  return Object.keys(monorepoDefinitions).find(group =>
    monorepoDefinitions[group].find(pkg => pkg === packageName))
}

async function isPartOfMonorepo (dependency) {
  return !!await module.exports.getMonorepoGroupNameForPackage(dependency)
}

async function hasAllMonorepoUdates (dependency, version) {
  const monorepoDefinitions = await getMonorepoDefinitions()
  const { npm } = await dbs()
  try {
    const group = await module.exports.getMonorepoGroupNameForPackage(dependency)
    // read all registry changes
    const docs = await npm.allDocs({
      include_docs: true,
      keys: monorepoDefinitions[group]
    })

    const docsWithoutErrors = docs.rows
      .filter(row => {
        return !row.error
      }) // filter out error docs
      .map(row => row.doc)

    // turn doc list into lookup table packageName -> packageInfo
    // TODO: maybe do this on startup for all groups
    const registryChanges = docsWithoutErrors.reduce((acc, doc) => {
      acc[doc._id] = doc
      return acc
    }, {})

    // check if we have other monorepo packages
    const receivedAllregistryChanges = monorepoDefinitions[group].reduce((acc, packageName) => {
      const pkg = registryChanges[packageName]
      // this is the main bit
      if (pkg && pkg.distTags['latest'] && pkg.distTags['latest'] !== version) {
        return false
      }
      return acc
    }, true)
    return receivedAllregistryChanges
  } catch (err) {
    throw err
  }
}

async function updateMonorepoReleaseInfo (dependency, distTags, distTag, versions) {
  const { npm } = await dbs()
  const version = distTags[distTag]
  const monorepoGroupname = await getMonorepoGroupNameForPackage(dependency)
  await npm.upsert(`monorepo:${monorepoGroupname}:${version}`, (oldDoc) => {
    const result = { distTags, distTag, dependency, versions }
    return Object.assign(result, updatedAt(oldDoc))
  })
}

async function deleteMonorepoReleaseInfo (dependency, version) {
  const { npm } = await dbs()
  const monorepoGroupname = await getMonorepoGroupNameForPackage(dependency)
  try {
    const docId = `monorepo:${monorepoGroupname}:${version}`
    const doc = await npm.get(docId)
    await npm.remove(doc)
  } catch (e) {
    if (e.name !== 'not_found') {
      throw e
    }
  }
}

async function pendingMonorepoReleases () {
  const { npm } = await dbs()
  // thx https://stackoverflow.com/questions/1197928/how-to-add-30-minutes-to-a-javascript-date-object#1214753
  const fiveMinutesAgo = dateMinusMinutes(5).toJSON()
  const result = await npm.query('monorepo-releases-by-time', {
    startkey: '1970-01-01T00:00:00.000Z',
    endkey: fiveMinutesAgo,
    include_docs: true
  })

  return result.rows.map(row => row.doc) || []
}

function dateMinusMinutes (minutes) {
  return new Date(new Date().getTime() - minutes * 60000)
}

// Given a groups array and a package path, returns the package file’s group, or undefined
function getGroupForPackageFile (groups, packageFilePath) {
  return Object.keys(groups).find((group) => {
    return groups[group].packages && groups[group].packages.includes(packageFilePath)
  })
}

// Given a groups array, a package path, and a dependency name,
// returns whether that dep is ignored for that package in any group
function isDependencyIgnoredInGroups (groups, packageFilePath, dependencyName) {
  const groupName = getGroupForPackageFile(groups, packageFilePath)
  return groupName && _.includes(groups[groupName].ignore, dependencyName)
}

module.exports = {
  isPartOfMonorepo,
  hasAllMonorepoUdates,
  getMonorepoGroup,
  getMonorepoGroupNameForPackage,
  updateMonorepoReleaseInfo,
  deleteMonorepoReleaseInfo,
  pendingMonorepoReleases,
  getGroupForPackageFile,
  isDependencyIgnoredInGroups
}


const url = require('url')

const _ = require('lodash')
const githubFromGit = require('github-url-from-git')

const getRelease = require('./get-release')
const getDiffCommits = require('./get-diff-commits')
const statsd = require('../lib/statsd')

// returns a url object if you pass in a GitHub repositoryURL,
// returns a string with an npm URL if you just pass in a dependency name
function getDependencyURL ({ repositoryURL, dependency }) {
  // githubURL is an object!
  const githubURL = url.parse(
    githubFromGit(repositoryURL) || ''
  )
  if (dependency && !githubURL.href) {
    return `https://www.npmjs.com/package/${dependency}`
  }
  return githubURL
}

// removes logins and hashes etc.
function getFormattedDependencyURL ({ repositoryURL, dependency }) {
  return url.format(getDependencyURL({ repositoryURL, dependency }))
}

async function getInfos (
  { installationId, dependency, version, diffBase, versions }
) {
  const infos = {}

  const baseVersionData = versions[diffBase]
  const versionData = versions[version]
  if (!baseVersionData || !versionData) return infos

  const depGhUrl = getDependencyURL({ repositoryURL: _.get(versionData, 'repository.url') })
  // remove the leading slash to get the slug
  // depGhUrl.pathname: '/colors/monorepo'
  const slug = depGhUrl.pathname && depGhUrl.pathname.replace(/^\//, '')
  if (!slug) return infos

  const [owner, repo] = slug.split('/')

  infos.release = await getRelease({
    installationId,
    owner,
    repo,
    version,
    sha: versionData.gitHead
  })

  if (!baseVersionData.gitHead || !versionData.gitHead) return infos

  infos.diffCommits = await getDiffCommits({
    installationId,
    owner,
    repo,
    base: baseVersionData.gitHead,
    head: versionData.gitHead
  })

  return infos
}

function resolver ({ dependency, version, diffBase }) {
  return `${dependency}${version}${diffBase}`
}

const memoizedGetInfos = _.memoize(getInfos, resolver)

if (process.env.NODE_ENV !== 'testing') {
  setInterval(() => {
    statsd.gauge('get_infos_cached', memoizedGetInfos.cache.size)
  }, 60000)
}

module.exports = {
  getInfos: memoizedGetInfos,
  getDependencyURL,
  getFormattedDependencyURL
}


const _ = require('lodash')
const semver = require('semver')
const getRangedVersion = require('../lib/get-ranged-version')
const env = require('../lib/env')
const registryUrl = env.NPM_REGISTRY

// expects an array of package.json paths, returns an array of unique dependencies from those package.jsons:
/*
  Returns:
  [{
    name: '@finnpauls/dep',
    version: '1.0.0',
    type: 'devDependencies'
  }]
*/
function getDependenciesFromPackageFiles (packagePaths, packageJsonContents) {
  return _.compact(_.uniqWith(_.flatten(packagePaths.map(path => {
    return _.flatten(
      ['dependencies', 'devDependencies', 'optionalDependencies'].map(type => {
        if (packageJsonContents[path]) {
          return _.map(packageJsonContents[path][type], (version, name) => ({ name, version, type }))
        }
      })
    )
  })), _.isEqual))
}

// add npm package data to dependency info from previous function
/*
  Returns:
  [{
    name: '@finnpauls/dep',
    version: '1.0.0',
    type: 'devDependencies',
    data: {
      'dist-tags': [Object]
    }
  }]
*/
async function addNPMPackageData (dependencyInfo, registryGet, log) {
  return Promise.mapSeries(dependencyInfo, async dep => {
    try {
      dep.data = await registryGet(registryUrl + dep.name.replace('/', '%2F'), {
      })
      return dep
    } catch (err) {
      log.error('npm: Could not get package data', { dependency: dep, error: err })
    }
  })
}

// get new version for all dependencies in files
/*
  Arguments:
  packagePaths: array of strings, eg. ['package.json', 'frontend/package.json']
  packageJsonContents: array of objects, eg. [{ devDependencies: { '@finnpauls/dep': '1.0.0'} }]
  registryGet: instance of promisified `npm-registry-client`
  ignore: array of strings, eg. ['eslint', 'standard'], from config (package.json, greenkeeper.json etc.)
  log: an instance of the logger

  Returns:
  [{
    name: '@finnpauls/dep',
    version: '1.0.0',
    type: 'devDependencies',
    data: {
      'dist-tags': [Object]
    },
    newVersion: '2.0.0'
  }]
*/
async function getUpdatedDependenciesForFiles ({ packagePaths, packageJsonContents, registryGet, ignore, log }) {
  const dependencyInfo = module.exports.getDependenciesFromPackageFiles(packagePaths, packageJsonContents, log)
  // Filter out ignored dependencies
  const unignoredDependencyInfo = dependencyInfo.filter((dep) => !ignore.includes(dep.name))
  log.info('dependencies found', { parsedDependencies: unignoredDependencyInfo, ignoredDependencies: ignore, packageJsonContents: packageJsonContents })
  let dependencies = await module.exports.addNPMPackageData(unignoredDependencyInfo, registryGet, log)
  let dependencyActionsLog = {}
  // add `newVersion` to each dependency object in the array
  const outputDependencies = _(dependencies)
    .filter(Boolean) // remove falsy values from input array
    .map(dependency => {
    // neither version nor range, so it's something weird (git url)
    // better not touch it
      if (!semver.validRange(dependency.version)) {
        dependencyActionsLog[dependency.name] = 'invalid range'
        return
      }
      // new version is prerelease
      const oldIsPrerelease = _.get(
        semver.parse(dependency.version),
        'prerelease.length'
      ) > 0

      let latest = _.get(dependency, 'data.dist-tags.latest')
      const prereleaseDiff = oldIsPrerelease &&
      semver.diff(dependency.version, latest) === 'prerelease'
      if (
        !prereleaseDiff &&
      _.get(semver.parse(latest), 'prerelease.length', 0) > 0
      ) {
        const versions = _.keys(_.get(dependency, 'data.versions'))
        latest = _.reduce(versions, function (current, next) {
          const parsed = semver.parse(next)
          if (!parsed) return current
          if (_.get(parsed, 'prerelease.length', 0) > 0) return current
          if (semver.gtr(next, current)) return next
          return current
        })
      }
      // no to need change anything :)
      if (semver.satisfies(latest, dependency.version)) {
        dependencyActionsLog[dependency.name] = 'satisfies semver'
        return
      }
      // no downgrades
      if (semver.ltr(latest, dependency.version)) {
        dependencyActionsLog[dependency.name] = 'would be a downgrade'
        return
      }
      dependency.newVersion = getRangedVersion(latest, dependency.version)
      dependencyActionsLog[dependency.name] = `updated to ${dependency.newVersion}`
      return dependency
    })
    .filter(Boolean) // remove falsy values from output array
    .value() // run lodash chain

  log.info('parsed dependency actions', { dependencyActionsLog })
  return outputDependencies
}

module.exports = {
  getUpdatedDependenciesForFiles,
  addNPMPackageData,
  getDependenciesFromPackageFiles
}


var phantom = require("phantom");
var fs = require("fs");

function writeToFile(data) {
    var stream = fs.createWriteStream(process.cwd() + "/src/utils/apiList.js");
    stream.once("open", function () {
        stream.write("// DO NOT MODIFY THIS FILE MANUALLY.\n");
        stream.write("// This file is generated by `npm run update-apilist`.\n");
        stream.write("// Note that only APIs that's documented in https://developers.google.com/doubleclick-gpt/reference is officially supported.\n");
        stream.write("\n");
        Object.keys(data.apis).forEach(function (key) {
            if (key === "gpt" || key === "pubads") {
                stream.write("export const " + key + "Version = " + data.version[key] + ";\n");
            }
            stream.write("export const " + key + "API = [" + "\n");
            data.apis[key].forEach(function (item, i) {
                stream.write("\t" + JSON.stringify(item).split(",").join(", ") + (i === data.apis[key].length - 1 ? "" : ",") + "\n");
            });
            stream.write("];\n");
        });
        stream.end();
    });
}

phantom.create().then(function (ph) {
    // a hack suggested here: https://github.com/amir20/phantomjs-node/issues/292
    function checkForData() {
        ph.windowProperty("DATA").then(function (data) {
            if (data !== undefined) {
                writeToFile(data);
                ph.exit();
            } else {
                setTimeout(checkForData, 100);
            }
        });
    }

    checkForData();

    ph.createPage().then(function (page) {
        page.property("onConsoleMessage", function (msg) {
            console.log(msg);
        });
        page.property("onCallback", function (data) {
            if (data) {
                DATA = data;
                page.close();
            }
        });
        page.open(process.cwd() + "/scripts/empty.html").then(function () {
            page.includeJs("http://www.googletagservices.com/tag/js/gpt.js").then(function () {
                setTimeout(function () {
                    page.evaluate(function () {
                        var EXCLUDES = ["constructor"].concat(Object.getOwnPropertyNames(Object.getPrototypeOf({})));
                        var adSlot;

                        function filterKeysByType(obj) {
                            var total = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
                            var filterTypes = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2];

                            return Object.getOwnPropertyNames(obj).filter(function (key) {
                                return (
                                    total.indexOf(key) === -1 &&
                                    EXCLUDES.indexOf(key) === -1 &&
                                    key.indexOf("_") !== 0 && // treat property starting with underscore as private
                                    key.length > 2 && // treat property with less than 2 chars as private
                                    obj.hasOwnProperty(key) &&
                                    filterTypes.length === 0 ? true : filterTypes.indexOf(typeof obj[key]) > -1
                                );
                            }).map(function (key) {
                                return [key, typeof obj[key]];
                            });
                        }

                        function aggregateApisByType(obj) {
                            var total = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];
                            var filterTypes = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2];
                            var keys = [];
                            while (obj !== null) {
                                var _keys;
                                var arr = filterKeysByType(obj, total, filterTypes);
                                (_keys = keys).push.apply(_keys, arr);
                                obj = Object.getPrototypeOf(obj);
                            }
                            keys = [].concat(keys);
                            return keys;
                        }

                        // extracts lists of methods from each service object.
                        function extractApis(services) {
                            var filterTypes = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1];

                            services = Array.isArray(services) ? services : [services];
                            var apis = services.reduce(function (total, service) {
                                var obj = service.constructor === Object ? service : Object.getPrototypeOf(service);
                                var keys = aggregateApisByType(obj, total, filterTypes);
                                total.push.apply(total, keys);
                                return total;
                            }, []);

                            return apis;
                        }

                        function checkPubadsReady() {
                            if (googletag && googletag.pubadsReady) {
                                console.log("gpt version: v" + googletag.getVersion(), ", pubads version: v" + googletag.pubads().getVersion());
                                if (typeof window.callPhantom === "function") {
                                    window.callPhantom({
                                        apis: {
                                            gpt: extractApis(googletag),
                                            pubads: extractApis(googletag.pubads()),
                                            slot: extractApis(adSlot)
                                        },
                                        version: {
                                            gpt: googletag.getVersion(),
                                            pubads: googletag.pubads().getVersion()
                                        }
                                    });
                                }
                            } else {
                                setTimeout(checkPubadsReady, 50);
                            }
                        }

                        googletag.cmd.push(function () {
                            adSlot = googletag.defineSlot("/123", [0, 0]).addService(googletag.pubads());
                            googletag.enableServices();
                            checkPubadsReady();
                        });
                    });
                }, 2000);
            });
        });
    });
});