/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';
const BBPromise = require('bluebird');
const argv = require('minimist')(process.argv.slice(2));
const assert = require('assert');
const extend = require('util')._extend;
const gulp = require('gulp-help')(require('gulp'));
const request = BBPromise.promisify(require('request'));
const colors = require('ansi-colors');
const log = require('fancy-log');

const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;

const isDryrun = argv.dryrun;

const issuesOptions = {
  url: 'https://api.github.com/repos/ampproject/amphtml/issues',
  headers: {
    'User-Agent': 'amp-changelog-gulp-task',
    'Accept': 'application/vnd.github.v3+json',
  },
  qs: {
    'access_token': GITHUB_ACCESS_TOKEN,
  },
};

const milestoneOptions = {
  url: 'https://api.github.com/repos/ampproject/amphtml/milestones',
  headers: {
    'User-Agent': 'amp-changelog-gulp-task',
    'Accept': 'application/vnd.github.v3+json',
  },
  qs: {
    'access_token': GITHUB_ACCESS_TOKEN,
  },
};

// 4 is the number for Milestone 'Backlog Bugs'
const MILESTONE_BACKLOG_BUGS = 4;
// 11 is the number for Milestone '3P Implementation'
const MILESTONE_3P_IMPLEMENTATION = 11;
// 12 is the number for Milestone 'Docs Updates'
const MILESTONE_DOCS_UPDATES = 12;
// By default we will assign 'Pending Triage' milestone, number 20
const MILESTONE_PENDING_TRIAGE = 20;
// 22 is the number for Milestone 'Prioritized FRs'
const MILESTONE_PRIORITIZED_FRS = 22;
// 23 is the number for Milestone 'New FRs'
const MILESTONE_NEW_FRS = 23;
// 25 is the number for Milestone 'Good First Issues (GFI)'
const MILESTONE_GREAT_ISSUES = 25;
// days for biweekly updates
const BIWEEKLY_DAYS = 14;
// days for quarterly updates
const QUARTERLY_DAYS = 89;
// we need around 14 batches to get more than 1k issues
const NUM_BATCHES = 14;

// We start processing the issues by checking token first
function processIssues() {
  if (!GITHUB_ACCESS_TOKEN) {
    log(colors.red('You have not set the ' +
        'GITHUB_ACCESS_TOKEN env var.'));
    log(colors.green('See https://help.github.com/articles/' +
        'creating-an-access-token-for-command-line-use/ ' +
        'for instructions on how to create a github access token. We only ' +
        'need `public_repo` scope.'));
    return;
  }
  return updateGitHubIssues().then(function() {
    log(colors.blue('automation applied'));
  });
}
/**
 * Fetches issues?page=${opt_page}
 *
 * @param {number=} opt_page
 * @return {!Promise<!Array<}
 */
function getIssues(opt_page) {
  const options = extend({}, issuesOptions);
  options.qs = {
    'state': 'open',
    'page': opt_page,
    'per_page': 100,
    'access_token': GITHUB_ACCESS_TOKEN,
  };
  return request(options).then(res => {
    const issues = JSON.parse(res.body);
    assert(Array.isArray(issues), 'issues must be an array.');
    return issues;
  });
}
/**
 * Function goes through all the gitHub issues,
 * gets all the Labels we are interested in,
 * depending if missing milestone or label,
 * tasks applied as per design go/ampgithubautomation
 */
function updateGitHubIssues() {
  let promise = Promise.resolve();
  const arrayPromises = [];
  // we need to pull issues in batches
  for (let batch = 1; batch < NUM_BATCHES; batch++) {
    arrayPromises.push(getIssues(batch));
  }
  return BBPromise.all(arrayPromises)
      .then(requests => [].concat.apply([], requests))
      .then(issues => {
        const allIssues = issues;
        allIssues.forEach(function(issue) {
          const labels = issue.labels;
          const pullRequest = issue.pull_request;
          let issueType;
          const milestone = issue.milestone;
          let milestoneTitle;
          let milestoneState;
          let hasPriority = false;
          let hasCategory = false;
          let issueNewMilestone = MILESTONE_PENDING_TRIAGE;
          const assignee = issue.assignee;
          let assigneeName = '';
          const issueLastUpdate = issue.updated_at;
          let biweeklyUpdate = true;
          let quartelyUpdate = true;
          // if an issue is a pull request, we'll skip it
          if (pullRequest) {
            if (isDryrun) {
              log(colors.red(issue.number + ' is a pull request'));
            }
            return;
          }
          if (getLastUpdate(issueLastUpdate) > QUARTERLY_DAYS) {
            quartelyUpdate = false;
            biweeklyUpdate = false;
          } else if (getLastUpdate(issueLastUpdate) > BIWEEKLY_DAYS) {
            biweeklyUpdate = false;
          }
          // Get the assignee
          if (assignee) {
            assigneeName = '@' + assignee.login;
          }
          // Get the title and state of the milestone
          if (milestone) {
            milestoneTitle = milestone.title;
            milestoneState = milestone.state;
            issueNewMilestone = milestone.number;
          }
          // promise starts
          promise = promise.then(function() {
            log('Update ' + issue.number);
            const updates = [];
            // Get the labels we want to check
            labels.forEach(function(label) {
              if (label) {
                // Check if the issues has type
                if (label.name.startsWith('Type') ||
                    label.name.startsWith('Related')) {
                  issueType = label.name;
                }
                // Check if the issues has Priority
                if (label.name.startsWith('P0') ||
                    label.name.startsWith('P1') ||
                    label.name.startsWith('P2') ||
                    label.name.startsWith('P3')) {
                  hasPriority = true;
                  if (label.name.startsWith('P0') ||
                      label.name.startsWith('P1')) {
                    if (biweeklyUpdate == false) {
                      biweeklyUpdate = true;
                      updates.push(applyComment(issue, 'This is a high priority'
                          + ' issue but it hasn\'t been updated in awhile. ' +
                          assigneeName + ' Do you have any updates?'));
                    }
                  } else if (label.name.startsWith('P2') &&
                      quartelyUpdate == false) {
                    quartelyUpdate = true;
                    updates.push(applyComment(issue, 'This issue hasn\'t been '
                        + ' updated in awhile. ' +
                    assigneeName + ' Do you have any updates?'));
                  }
                }
                if (label.name.startsWith('Category') ||
                    label.name.startsWith('Related to') ||
                    label.name.startsWith('GFI') ||
                    label.name.startsWith('good first issue')) {
                  hasCategory = true;
                }
              }
            });
            // Milestone task: move issues from closed milestone
            if (milestone) {
              if (milestoneState === 'closed') {
                issueNewMilestone = MILESTONE_BACKLOG_BUGS;
                updates.push(applyMilestone(issue, issueNewMilestone));
              }
            }
            if (issueNewMilestone === MILESTONE_PENDING_TRIAGE) {
              if (quartelyUpdate == false) {
                quartelyUpdate = true;
                updates.push(applyComment(issue, 'This issue seems to be in ' +
                    ' Pending Triage for awhile. ' +
                    assigneeName + ' Please triage this to ' +
                    'an appropriate milestone.'));
              }
            }
            // if issueType is not null, add correct milestones
            if (issueType != null) {
              if (issueNewMilestone === MILESTONE_PENDING_TRIAGE ||
                  milestone == null) {
                if (issueType === 'Type: Feature Request') {
                  issueNewMilestone = MILESTONE_NEW_FRS;
                  updates.push(applyMilestone(issue, issueNewMilestone));
                } else if (issueType === 'Related to: Documentation' ||
                    issueType === 'Type: Design Review' ||
                    issueType === 'Type: Status Update') {
                  issueNewMilestone = MILESTONE_DOCS_UPDATES;
                  updates.push(applyMilestone(issue, issueNewMilestone));
                } else if (issueType === 'Type: Bug' ||
                    issueType === 'Related to: Flaky Tests') {
                  issueNewMilestone = MILESTONE_BACKLOG_BUGS;
                  updates.push(applyMilestone(issue, issueNewMilestone));
                } else if (milestone == null) {
                  updates.push(applyMilestone(issue, issueNewMilestone));
                }
              }
            } else if (milestone == null) {
              updates.push(applyMilestone(issue, issueNewMilestone));
            } else if (issueNewMilestone === MILESTONE_PRIORITIZED_FRS ||
                issueNewMilestone === MILESTONE_NEW_FRS) {
              updates.push(applyLabel(issue, 'Type: Feature Request'));
            } else if (issueNewMilestone === MILESTONE_BACKLOG_BUGS ||
                milestoneTitle.startsWith('Sprint')) {
              updates.push(applyLabel(issue, 'Type: Bug'));
            }
            // Apply default priority if no priority
            if (hasPriority == false &&
                issueNewMilestone != MILESTONE_NEW_FRS &&
                issueNewMilestone !== MILESTONE_3P_IMPLEMENTATION &&
                issueNewMilestone !== MILESTONE_PENDING_TRIAGE &&
                milestone != null) {
              updates.push(applyLabel(issue, 'P2: Soon'));
            }
            // Add comment with missing Category
            if (hasCategory == false) {
              if (issueNewMilestone === MILESTONE_PENDING_TRIAGE ||
                  issueNewMilestone === MILESTONE_DOCS_UPDATES ||
                  issueNewMilestone == null ||
                  issueNewMilestone === MILESTONE_GREAT_ISSUES) {
                if (isDryrun) {
                  log(colors.green('No comment needed '
                      + ' for #' + issue.number));
                }
              } else {
                updates.push(applyComment(issue,
                    'This issue doesn\'t have a category'
                    + ' which makes it harder for us to keep track of it. ' +
                    assigneeName + ' Please add an appropriate category.'));
              }
            }
            return Promise.all(updates);
          });
        });
        return promise;
      });
}

/**
 * @param {string} issue
 * @param {number} milestoneNumber
 * @return {!Promise<*>}
 */
function applyMilestone(issue, milestoneNumber) {
  const options = extend({}, milestoneOptions);
  options.qs = {
    'state': 'open',
    'per_page': 100,
    'access_token': GITHUB_ACCESS_TOKEN,
  };

  issue.milestone = milestoneNumber;
  if (isDryrun) {
    log(colors.green('Milestone applied ' + milestoneNumber +
        ' for #' + issue.number));
    return;
  } else {
    return createGithubRequest('/issues/' + issue.number,'PATCH',
        issue.milestone, 'milestone');
  }
}

/**
 * @param {string} issue
 * @param {string} label
 * @return {!Promise<*>}
 */
function applyLabel(issue, label) {
  const options = extend({}, issuesOptions);
  options.qs = {
    'state': 'open',
    'per_page': 100,
    'access_token': GITHUB_ACCESS_TOKEN,
  };
  if (isDryrun) {
    log(colors.green('Label applied ' +
        label + ' for #' + issue.number));
    return;
  } else {
    return createGithubRequest('/issues/' + issue.number + '/labels','POST',
        [label], 'label');
  }

}

/**
 * @param {string} issue
 * @param {string} comment
 * @return {!Promise<*>}
 */
function applyComment(issue, comment) {
  const options = extend({}, issuesOptions);
  options.qs = {
    'state': 'open',
    'per_page': 100,
    'access_token': GITHUB_ACCESS_TOKEN,
  };
  // delay the comment request so we don't reach github rate limits requests
  const promise = new Promise(resolve => setTimeout(resolve, 120000));
  return promise.then(function() {
    if (isDryrun) {
      log(colors.blue('waited 2 minutes to avoid gh rate limits'));
      log(colors.green('Comment applied after ' +
          'waiting 2 minutes to avoid github rate limits: ' + comment +
          ' for #' + issue.number));
      return;
    } else {
      createGithubRequest('/issues/' + issue.number + '/comments','POST',
          comment, 'comment');
    }
  });
}
// calculate number of days since the latest update
function getLastUpdate(issueLastUpdate) {
  const t = new Date();
  const splits = issueLastUpdate.split('-', 3);
  const exactDay = splits[2].split('T', 1);
  const firstDate = Date.UTC(splits[0],splits[1],exactDay[0]);
  const secondDate = Date.UTC(t.getFullYear(),t.getMonth() + 1,t.getDate());
  const diff = Math.abs((firstDate.valueOf() -
      secondDate.valueOf()) / (24 * 60 * 60 * 1000));
  return diff;
}

/**
 * Function pushes the updates requested based on the path received
 * @param {string} path
 * @param {string=} opt_method
 * @param {*} opt_data
 * @param {string} typeRequest
 * @return {!Promise<*>}
 */
function createGithubRequest(path, opt_method, opt_data, typeRequest) {
  const options = {
    url: 'https://api.github.com/repos/ampproject/amphtml' + path,
    body: {},
    headers: {
      'User-Agent': 'amp-changelog-gulp-task',
      'Accept': 'application/vnd.github.v3+json',
    },
    qs: {
      'access_token': GITHUB_ACCESS_TOKEN,
    },
  };
  if (opt_method) {
    options.method = opt_method;
  }
  if (opt_data) {
    options.json = true;
    if (typeRequest === 'milestone') {
      options.body.milestone = opt_data;
    } else if (typeRequest === 'comment') {
      options.body.body = opt_data;
    } else {
      options.body = opt_data;
    }
  }
  return request(options);
}

gulp.task(
    'process-github-issues',
    'Automatically updates the labels '
    + 'and milestones of all open issues at github.com/ampproject/amphtml.',
    processIssues,
    {
      options: {
        dryrun: '  Generate process but don\'t push it out',
      },
    }
);
