const MigrationModel = require('../models/migration');
const fs = require('fs');
const path = require('path');
const Joi = require('joi');
const debug = require('debug')('talk:services:migration');
const sc = require('snake-case');
const {talk: {migration: {minVersion}}} = require('../package.json');

const migrationTemplate = `module.exports = {
  async up() {
    
  }
};

`;

class MigrationService {

  /**
   * Creates a new migration file.
   * 
   * @param {String} name name of the migration
   */
  static async create(name) {
    if (!name || typeof name !== 'string' || name.length === 0) {
      throw new Error('name must be defined');
    }

    // Create a new Migration based on the current time.
    let version = Math.round(Date.now() / 1000, 0);
    let filename = path.join(__dirname, '..', 'migrations', `${version}_${sc(name)}.js`);
    fs.writeFileSync(filename, migrationTemplate, 'utf8');

    console.log(`Created migration ${version} in ${filename}`);
  }

  /**
   * Returns a list of all pending migrations.
   */
  static async listPending() {

    // Get all the migration files.
    let migrationFiles = fs.readdirSync(path.join(__dirname, '..', 'migrations'));

    // Ensure that all migrations follow this format.
    const migrationSchema = Joi.object({
      up: Joi.func().required(),
      down: Joi.func()
    });

    // Extract the version from the filename with this regex.
    const versionRe = /(\d+)_([\S_]+)\.js/;

    // Get the latest version.
    let latestVersion = await MigrationService.latestVersion();

    // Parse the migrations from the file listing.
    let migrations = migrationFiles
      .map((filename) => {

        // Parse the version from the filename.
        let matches = filename.match(versionRe);
        if (matches.length !== 3) {
          return null;
        }
        let version = parseInt(matches[1]);

        // Don't rerun migrations from versions already ran.
        if (version <= latestVersion) {
          return null;
        }

        // Read the migration from the filesystem.
        let migration = require(`../migrations/${filename}`);
        Joi.assert(migration, migrationSchema, `Migration ${filename} does did not pass validation`);

        return {
          filename,
          version,
          migration
        };
      })
      .filter((migration) => migration !== null)
      .sort((a, b) => {
        if (a.version < b.version) {
          return -1;
        }

        if (a.version > b.version) {
          return 1;
        }

        return 0;
      });
    
    return migrations;
  }

  /**
   * Runs an list of migrations.
   * 
   * @param {Array} migrations a list of migrations returned by `listPending`
   */
  static async run(migrations) {
    if (migrations.length === 0) {
      console.log('No migrations to run!');
      return;
    }

    for (let {filename, version, migration} of migrations) {
      try {
        console.log(`Starting migration ${filename}`);
        await migration.up();
        console.log(`Finished migration ${filename}`);
      } catch (e) {
        console.error(`Migration ${filename} failed`);
        throw e;
      }
      
      try {
        console.log(`Recording migration ${filename}`);

        // Record that the migration was finished.
        let m = new MigrationModel({version});
        await m.save();

        console.log(`Finished recording migration ${filename}`);
      } catch (e) {
        console.error(`Migration ${filename} could not be recorded`);
        throw e;
      }
    }

    console.log(`Database now at migration version ${migrations[migrations.length - 1].version}`);
  }

  /**
   * Returns the latest migration version number that has been applied to the
   * database, null if none were found.
   */
  static async latestVersion() {

    // Load the latest migration details from the database.
    let latestMigration = await MigrationModel
      .find({})
      .sort({version: -1})
      .limit(1)
      .exec();

    // If there weren't any migrations at all, then error out.
    if (latestMigration.length === 0) {
      return null;
    }
    
    // If the latest migration does not match the required version, then error
    // out.
    return latestMigration[0].version;
  }

  static async verify() {

    // If the requiredVersion isn't specified or is 0, then don't complain.
    if (typeof minVersion !== 'number' || minVersion === 0) {
      return;
    }

    // If the latest migration does not match the required version, then error
    // out.
    let latestVersion = await MigrationService.latestVersion();
    if (!latestVersion || latestVersion < minVersion) {
      throw new Error(`A database migration is required, version required ${minVersion}, found ${latestVersion}. Please run \`./bin/cli migration run\``);
    }

    debug(`minimum migration version ${minVersion} was met with version ${latestVersion}`);

    return latestVersion;
  }
}

module.exports = MigrationService;
