const map = require('lodash/map');
const retry = require('async-retry');
const generate = require('nanoid/generate');
const markdown = require('markdown').markdown;
const async = require('async');
const ExpirationUnits = require('../expirationunits');


class Code
{
  constructor(values)
  {
    var self = this;

    self.id = values.id || values._id || null;
    self.userId = values.userId || null;
    self.created = values.created || new Date();
    self.firstUpload = values.firstUpload || null;
    self.expiration = values.expiration || null;
    self.expirationDate = values.expirationDate || null;
    self.description = values.description || null;
    self.message = values.message || null;
    self.messageHTML = values.messageHTML || null;
    self.history = values.history || [{
      userId: self.userId,
      date: self.created
    }];
  }
}


class CodeRepository
{
  constructor(store)
  {
    var self = this;
    self.store = store;
  }


  init()
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      resolve();
    });
  }


  async insert(code)
  {
    var self = this;

    return await retry(async bail =>
    {
      var codeId = generate(config.code.alphabet, config.code.length);

      if ((await self.get(codeId)) !== null)
        throw new Error('Code ' + codeId + ' already exists');

      let now = new Date();

      self.store.insert({
        _id: codeId,
        userId: code.userId,
        created: code.created || now,
        firstUpload: code.firstUpload,
        expiration: code.expiration,
        expirationDate: code.expirationDate,
        description: code.description,
        message: code.message,
        messageHTML: self.getMessageHTML(code.message),
        history: [
          {
            userId: code.userId,
            date: code.created || now
          }
        ]
      })

      return codeId;
    }, {
      retries: 100,
      minTimeout: 0,
      maxTimeout: 0
    });
  }


  update(code)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.update({ _id: code.id }, { $set: {
        expiration: code.expiration,
        firstUpload: code.firstUpload,
        expirationDate: code.firstUpload != null && code.expiration != null ? ExpirationUnits.apply(code.expiration, code.firstUpload) : null,
        description: code.description,
        message: code.message,
        messageHTML: self.getMessageHTML(code.message)
      }},
      (err, numAffected) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        if (numAffected == 0)
        {
          reject();
        }

        resolve();
      });
    });
  }


  getUploadExpirationDate(codeId)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.findOne({ _id: codeId }, (err, doc) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        if (doc !== null)
        {
          if (doc.firstUpload == null)
          {
            let now = new Date();
            let expirationDate = doc.expiration != null ? ExpirationUnits.apply(doc.expiration, now) : null;

            self.store.update({ _id: codeId }, { $set: {
              firstUpload: now,
              expirationDate: expirationDate
            }},
            (err, numAffected) =>
            {
              if (err)
              {
                reject(err);
                return;
              }

              if (numAffected == 0)
              {
                reject();
              }

              resolve(expirationDate);
            });
          }
          else
            resolve(doc.expirationDate);
        }
        else
          resolve(null);
      });
    });
  }


  move(codeId, userId)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.update({ _id: codeId }, {
        $set: {
          userId: userId
        },
        $push: {
          history: {
            userId: userId,
            date: new Date()
          }
        }
      },
      (err, numAffected) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        if (numAffected == 0)
        {
          reject();
        }

        resolve();
      });
    });
  }


  list(userId)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.find(userId != null ? { userId: userId } : {}, (err, docs) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        resolve(map(docs, (doc) => new Code(doc)));
      });
    });
  }


  getDescriptions()
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.find({}, (err, docs) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        var descriptions = {};
        docs.forEach((dbCode) =>
        {
          if (dbCode.description)
            descriptions[dbCode._id] = dbCode.description;
        });

        resolve(descriptions);
      });
    });
  }


  get(codeId)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.findOne({ _id: codeId }, (err, doc) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        resolve(doc !== null ? new Code(doc) : null);
      });
    });
  }


  getMessageHTML(message)
  {
    return message !== null ? markdown.toHTML(message) : null;
  }


  delete(codeId)
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      self.store.remove({ _id: codeId }, (err, numRemoved) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        resolve();
      });
    });
  }


  deleteExpired()
  {
    var self = this;

    return new Promise((resolve, reject) =>
    {
      let now = new Date();

      self.store.find({ $where: function() { return this.expirationDate !== null && this.expirationDate < now }}, (err, docs) =>
      {
        if (err)
        {
          reject(err);
          return;
        }

        async.eachOfSeries(docs,
          async (doc) =>
          {
            console.log('Expired code: ' + doc._id);
            await self.delete(doc._id);
          },
          (err) =>
          {
            if (err)
              reject(err);
            else
              resolve();
          });
        });
    });
  }
}


module.exports = {
  Code,
  CodeRepository
}