'use strict';

/** **************************************
 * Reason: After the api v3 maintenance migration, some challenge tasks
 * became unlinked from their challenges. We're still not sure why,
 * but this re-links them
 *
 * Note: We ran this on a local backup of the DB, and from that, grabbed
 * the ids of the tasks that could be fixed and the updates that would
 * be applied to them. We only ran the `updateTasks` promise task.
 *
 * IMPORTANT - Setting challenge.broken to null caused issues
 * see https://github.com/HabitRPG/habitrpg/issues/7546
 ***************************************/

const authorName = 'Blade';
const authorUuid = '75f270e8-c5db-4722-a5e6-a83f1b23f76b';

const MongoClient = require('mongodb').MongoClient;
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');

// PROD: Enable prod db
// const NODE_DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
const NODE_DB_URI = 'mongodb://localhost/new-prod-copy';

// Cached ids from running the findBrokenChallengeTasks query on a local copy of the db
// These are all the ids that _are_ fixable
const TASK_IDS = require('../fixable_task_ids.json');
const TASK_UPDATE_DATA = require('../challenge_fixes.json');

let db;
let count = 0;

let timer = setInterval(function () {
  count++;
  if (count % 30 === 0) {
    logger.warn('Process has been running for', count / 60, 'minutes');
  }
}, 1000);

connectToDb()
  // .then(findBrokenChallengeTasks)
  // .then(getDataFromTasks)
  // .then(getUserChallenges)
  // .then(getChallengeTasks)
  // .then(correctUserTasks)
  .then(updateTasks)
  .then(closeDb)
  .catch(reportError);

function connectToDb () {
  return new Promise((resolve, reject) => {
    MongoClient.connect(NODE_DB_URI, (err, database) => {
      if (err) {
        logger.error('Uh oh... Problem connecting to the new database');
        return reject(err);
      }

      logger.success('Connected to the database');

      db = database;

      resolve(db);
    });
  });
}

function reportError (err) {
  logger.error('Uh oh, an error occurred');
  closeDb();
  throw err;
}

function unique (array) {
  return Array.from(new Set(array));
}

function findBrokenChallengeTasks () {
  logger.info('Looking for broken tasks...');

  // return db.collection('tasks').find({'challenge.broken': 'CHALLENGE_TASK_NOT_FOUND'}).toArray()
  return db.collection('tasks').find({_id: { $in: TASK_IDS }}).toArray()
    .then((tasks) => {
      logger.success('Found', tasks.length, 'broken tasks.');
      return Promise.resolve(tasks);
    });
}

function getDataFromTasks (tasks) {
  logger.info('Collecting data about the tasks...');

  let userTasks = {};

  tasks.forEach((task) => {
    let userId = task.userId;

    if (!userTasks[userId]) {
      userTasks[userId] = [];
    }
    userTasks[userId].push(task);
  });

  let users = unique(tasks.map(task => task.userId));

  return Promise.resolve({
    users,
    userTasks,
    tasks,
  });
}

function getUserChallenges (data) {
  logger.info('Collecting user challenges...');

  return db.collection('users').find({_id: { $in: data.users }}, {challenges: 1}).toArray().then((docs) => {
    logger.success('Found', docs.length, 'users from broken challenge tasks.');

    let challenges = [];
    docs.forEach((user) => {
      challenges.push(...user.challenges);
    });

    challenges = unique(challenges);

    let userChallenges = {};

    docs.forEach((user) => {
      let userId = user._id;
      if (!userChallenges[userId]) {
        userChallenges[userId] = [];
      }
      userChallenges[userId].push.apply(userChallenges[userId], user.challenges);
    });

    data.userChallenges = userChallenges;
    data.challenges = challenges;

    logger.success('Found', challenges.length, 'unique challenges.');

    return Promise.resolve(data);
  });
}

function getChallengeTasks (data) {
  logger.info('Looking up original challenge tasks...');

  return db.collection('tasks').find({userId: null, 'challenge.id': { $in: data.challenges }}, ['text', 'type', 'challenge', '_legacyId']).toArray().then((docs) => {
    logger.success('Found', docs.length, 'challenge tasks.');

    let challengeTasks = {};

    docs.forEach((task) => {
      let chalId = task.challenge.id;
      if (!challengeTasks[chalId]) {
        challengeTasks[chalId] = [];
      }
      challengeTasks[chalId].push(task);
    });
    data.challengeTasks = challengeTasks;

    return Promise.resolve(data);
  });
}

function correctUserTasks (data) {
  logger.info('Correcting user tasks...');

  let tasksToUpdate = {};
  let duplicateTasks = {};

  for (let user in data.userChallenges) {
    if (user === authorUuid) {
      logger.success('Processing data for', authorName);
    }
    if (data.userChallenges.hasOwnProperty(user)) {
      let challenges = data.userChallenges[user];

      challenges.forEach((chal) => {
        let challengeTasks = data.challengeTasks[chal];
        let userTasks = data.userTasks[user];

        if (challengeTasks) {
          challengeTasks.forEach((challengeTask) => {
            let text = challengeTask.text;
            let type = challengeTask.type;
            let taskId = challengeTask._id;
            let legacyId = challengeTask._legacyId;

            let foundTask = userTasks.find((task) => {
              return TASK_IDS.indexOf(task._id) > -1 && task._legacyId === legacyId && task.type === type && task.text === text;
            });

            if (foundTask && !tasksToUpdate[foundTask._id]) {
              tasksToUpdate[foundTask._id] = {
                id: chal,
                broken: null, // NOTE: this caused a lot of problems
                taskId,
              };
            } else if (foundTask && taskId !== tasksToUpdate[foundTask._id].taskId) {
              logger.error('Duplicate task found, id:', foundTask._id);
              duplicateTasks[foundTask._id] = duplicateTasks[foundTask._id] || [tasksToUpdate[foundTask._id].taskId];
              duplicateTasks[foundTask._id].push(taskId);
            }
          });
        }
      });
    }
  }

  let numberOfDuplicateTasksFound = Object.keys(duplicateTasks).length;

  if (numberOfDuplicateTasksFound > 0) {
    logger.error('Found', numberOfDuplicateTasksFound, 'duplicate taks');
  }


  data.tasksToUpdate = tasksToUpdate;

  return Promise.resolve(data);
}

function updateTasks (data) {
  let tasksToUpdate = TASK_UPDATE_DATA;
  let taskIdsToUpdate = Object.keys(tasksToUpdate);
  let queue = new TaskQueue(Promise, 300);
  let promiseCount = 0;

  logger.info('About to update', taskIdsToUpdate.length, 'user tasks');

  function updateTaskById (taskId) {
    promiseCount++;

    if (promiseCount % 500 === 0) {
      logger.info(promiseCount, 'updates started');
    }

    return db.collection('tasks').findOneAndUpdate({_id: taskId, 'challenge.broken': 'CHALLENGE_TASK_NOT_FOUND'}, {$set: {challenge: tasksToUpdate[taskId]}}, {returnOriginal: false});
  }

  return Promise.map(taskIdsToUpdate, queue.wrap(updateTaskById)).then((result) => {
    let updates = result.filter(res => res && res.lastErrorObject.updatedExisting);
    let failures = result.filter(res => res && !res.lastErrorObject.updatedExisting);

    logger.success(updates.length, 'tasks have been fixed');

    if (failures.length > 0) {
      logger.error(failures.length, 'tasks could not be updated');
      logger.error('Manually check these results');
      logger.error(failures);
    }

    return Promise.resolve(data);
  });
}

function closeDb (data) {
  logger.success(`The process took ${  count  } seconds`);

  clearInterval(timer);

  db.close();
}
