const fs = require('fs');
const os = require('os');
const path = require('path');
const spawn = require('child_process').spawn;
const debug = require('debug')('flakiness');

const rmAsync = promisify(require('rimraf'));
const mkdtempAsync = promisify(fs.mkdtemp);
const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);

const TMP_FOLDER = path.join(os.tmpdir(), 'flakiness_tmp_folder-');

const RED_COLOR = '\x1b[31m';
const GREEN_COLOR = '\x1b[32m';
const YELLOW_COLOR = '\x1b[33m';
const RESET_COLOR = '\x1b[0m';

const DASHBOARD_VERSION = 1;
const DASHBOARD_FILENAME = 'dashboard.json';
const DASHBOARD_MAX_BUILDS = 100;

class FlakinessDashboard {
  static async getCommitDetails(repoPath, ref = 'HEAD') {
    const {stdout: timestamp} = await spawnAsyncOrDie('git', 'show', '-s', '--format=%ct', ref, {cwd: repoPath});
    const {stdout: sha} = await spawnAsyncOrDie('git', 'rev-parse', ref, {cwd: repoPath});
    return {timestamp: timestamp * 1000, sha: sha.trim()};
  }

  constructor({build, commit, dashboardRepo}) {
    if (!commit)
      throw new Error('"options.commit" must be specified!');
    if (!commit.sha)
      throw new Error('"options.commit.sha" must be specified!');
    if (!commit.timestamp)
      throw new Error('"options.commit.timestamp" must be specified!');
    if (!build)
      throw new Error('"options.build" must be specified!');
    if (!build.url)
      throw new Error('"options.build.url" must be specified!');
    if (!dashboardRepo.branch)
      throw new Error('"options.dashboardRepo.branch" must be specified!');
    this._dashboardRepo = dashboardRepo;
    this._build = new Build(Date.now(), build.url, commit, []);
  }

  reportTestResult(test) {
    this._build._tests.push(test);
  }

  setBuildResult(result) {
    this._build._result = result;
  }

  async uploadAndCleanup() {
    console.log(`\n${YELLOW_COLOR}=== UPLOADING Flakiness Dashboard${RESET_COLOR}`);
    const startTimestamp = Date.now();
    const branch = this._dashboardRepo.branch.toLowerCase().replace(/\s/g, '-').replace(/[^-0-9a-zа-яё]/ig, '');
    console.log(`  > Dashboard URL: ${this._dashboardRepo.url}`);
    console.log(`  > Dashboard Branch: ${branch}`);
    const git = await Git.initialize(this._dashboardRepo.url, branch, this._dashboardRepo.username, this._dashboardRepo.email, this._dashboardRepo.password);
    console.log(`  > Dashboard Checkout: ${git.path()}`);

    // Do at max 7 attempts to upload changes to github.
    let success = false;
    const MAX_ATTEMPTS = 7;
    for (let i = 0; !success && i < MAX_ATTEMPTS; ++i) {
      await saveBuildToDashboard(git.path(), this._build);
      // if push went through - great! We're done!
      if (await git.commitAllAndPush(`update dashboard\n\nbuild: ${this._build._url}`)) {
        success = true;
        console.log(`  > Push attempt ${YELLOW_COLOR}${i + 1}${RESET_COLOR} of ${YELLOW_COLOR}${MAX_ATTEMPTS}${RESET_COLOR}: ${GREEN_COLOR}SUCCESS${RESET_COLOR}`);
      } else {
        // Otherwise - wait random time between 3 and 11 seconds.
        const cooldown = 3000 + Math.round(Math.random() * 1000) * 8;
        console.log(`  > Push attempt ${YELLOW_COLOR}${i + 1}${RESET_COLOR} of ${YELLOW_COLOR}${MAX_ATTEMPTS}${RESET_COLOR}: ${RED_COLOR}FAILED${RESET_COLOR}, cooldown ${YELLOW_COLOR}${cooldown / 1000}${RESET_COLOR} seconds`);
        await new Promise(x => setTimeout(x, cooldown));
        // Reset our generated dashboard and pull from origin.
        await git.hardResetToOriginMaster();
        await git.pullFromOrigin();
      }
    }
    await rmAsync(git.path());
    console.log(`  > TOTAL TIME: ${YELLOW_COLOR}${(Date.now() - startTimestamp) / 1000}${RESET_COLOR} seconds`);
    if (success)
      console.log(`${YELLOW_COLOR}=== COMPLETE${RESET_COLOR}`);
    else
      console.log(`${RED_COLOR}=== FAILED${RESET_COLOR}`);
    console.log('');
  }
}

async function saveBuildToDashboard(dashboardPath, build) {
  const filePath = path.join(dashboardPath, DASHBOARD_FILENAME);
  let data = null;
  try {
    data = JSON.parse(await readFileAsync(filePath));
  } catch (e) {
    // Looks like there's no dashboard yet - create one.
    data = {builds: []};
  }
  if (!data.builds)
    throw new Error('Unrecognized dashboard format!');
  data.builds.push({
    version: DASHBOARD_VERSION,
    result: build._result,
    timestamp: build._timestamp,
    url: build._url,
    commit: build._commit,
    tests: build._tests,
  });
  if (data.builds.length > DASHBOARD_MAX_BUILDS)
    data.builds = data.builds.slice(data.builds.length - DASHBOARD_MAX_BUILDS);
  await writeFileAsync(filePath, JSON.stringify(data));
}

class Build {
  constructor(timestamp, url, commit, tests) {
    this._timestamp = timestamp;
    this._url = url;
    this._commit = commit;
    this._tests = tests;
    this._result = undefined;
  }
}

module.exports = {FlakinessDashboard};

function promisify(nodeFunction) {
  function promisified(...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...result) {
        if (err)
          return reject(err);
        if (result.length === 1)
          return resolve(result[0]);
        return resolve(result);
      }
      nodeFunction.call(null, ...args, callback);
    });
  }
  return promisified;
}

class Git {
  static async initialize(url, branch, username, email, password) {
    let schemeIndex = url.indexOf('://');
    if (schemeIndex === -1)
      throw new Error(`Malformed URL "${url}": expected to start with "https://"`);
    schemeIndex += '://'.length;
    url = url.substring(0, schemeIndex) + username + ':' + password + '@' + url.substring(schemeIndex);
    const repoPath = await mkdtempAsync(TMP_FOLDER);
    // Check existance of a remote branch for this bot.
    const {stdout} = await spawnAsync('git', 'ls-remote', '--heads', url, branch);
    // If there is no remote branch for this bot - create one.
    if (!stdout.includes(branch)) {
      await spawnAsyncOrDie('git', 'clone', '--no-checkout', '--depth=1', url, repoPath);

      await spawnAsyncOrDie('git', 'checkout', '--orphan', branch, {cwd: repoPath});
      await spawnAsyncOrDie('git', 'reset', '--hard', {cwd: repoPath});
    } else {
      await spawnAsyncOrDie('git', 'clone', '--single-branch', '--branch', `${branch}`, '--depth=1', url, repoPath);
    }
    await spawnAsyncOrDie('git', 'config', 'user.email', `"${email}"`, {cwd: repoPath});
    await spawnAsyncOrDie('git', 'config', 'user.name', `"${username}"`, {cwd: repoPath});
    return new Git(repoPath, url, branch, username);
  }

  async commitAllAndPush(message) {
    await spawnAsyncOrDie('git', 'add', '.', {cwd: this._repoPath});
    await spawnAsyncOrDie('git', 'commit', '-m', `${message}`, '--author', '"playwright-flakiness <aslushnikov+playwrightflakiness@gmail.com>"', {cwd: this._repoPath});
    const {code} = await spawnAsync('git', 'push', 'origin', this._branch, {cwd: this._repoPath});
    return code === 0;
  }

  async hardResetToOriginMaster() {
    await spawnAsyncOrDie('git', 'reset', '--hard', `origin/${this._branch}`, {cwd: this._repoPath});
  }

  async pullFromOrigin() {
    await spawnAsyncOrDie('git', 'pull', 'origin', this._branch, {cwd: this._repoPath});
  }

  constructor(repoPath, url, branch, username) {
    this._repoPath = repoPath;
    this._url = url;
    this._branch = branch;
    this._username = username;
  }

  path() {
    return this._repoPath;
  }
}

async function spawnAsync(command, ...args) {
  let options = {};
  if (args.length && args[args.length - 1].constructor.name !== 'String')
    options = args.pop();
  const cmd = spawn(command, args, options);
  let stdout = '';
  let stderr = '';
  cmd.stdout.on('data', data => stdout += data);
  cmd.stderr.on('data', data => stderr += data);
  const code = await new Promise(x => cmd.once('close', x));
  if (stdout)
    debug(stdout);
  if (stderr)
    debug(stderr);
  return {code, stdout, stderr};
}

async function spawnAsyncOrDie(command, ...args) {
  const {code, stdout, stderr} = await spawnAsync(command, ...args);
  if (code !== 0)
    throw new Error(`Failed to executed: "${command} ${args.join(' ')}".\n\n=== STDOUT ===\n${stdout}\n\n\n=== STDERR ===\n${stderr}`);
  return {stdout, stderr};
}
