// comment.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const path = require('path');

const mongoose = require('mongoose');

const Comment = mongoose.model('Comment'); // jshint ignore:line

const pug = require('pug');
const striptags = require('striptags');

const { SiteService, SiteError } = require('../../lib/site-lib');

class CommentService extends SiteService {

  constructor (dtp) {
    super(dtp, module.exports);
    this.populateComment = [
      {
        path: 'author',
        select: '_id username username_lc displayName picture',
      },
      {
        path: 'replyTo',
      },
    ];
    this.populateCommentWithResource = [
      {
        path: 'author',
        select: '_id username username_lc displayName picture',
      },
      {
        path: 'replyTo',
      },
      {
        path: 'resource',
        populate: [
          {
            path: 'author',
            select: '_id username username_lc displayName picture',
            strictPopulate: false,
          },
        ],
      },
    ];
  }

  async start ( ) {
    await super.start();

    this.templates = { };
    this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug'));

    this.templates.commentList = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-list-standalone.pug'));
    this.templates.replyList = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'reply-list-standalone.pug'));
  }

  async renderTemplate (templateName, viewModel) {
    return this.templates[templateName](viewModel);
  }

  async populateCommentId (req, res, next, commentId) {
    try {
      res.locals.comment = await this.getById(commentId);
      if (!res.locals.comment) {
        throw new SiteError(404, 'Comment not found');
      }
      return next();
    } catch (error) {
      this.log.error('failed to populate comment', { commentId, error });
      return next(error);
    }
  }

  commentCreateHandler (resourceType, resourceKey) {
    const { displayEngine: displayEngineService } = this.dtp.services;
    return async (req, res, next) => {
      try {
        res.locals.comment = await this.create(
          req.user,
          resourceType,
          res.locals[resourceKey],
          req.body,
        );

        let viewModel = Object.assign({ }, req.app.locals);
        viewModel = Object.assign(viewModel, res.locals);
        const html = await this.renderTemplate('comment', viewModel);

        const displayList = displayEngineService.createDisplayList('announcement-comment');
        displayList.setInputValue('textarea#content', '');
        displayList.setTextContent('#comment-character-count', '0');

        if (req.body.replyTo) {
          const replyListSelector = `.dtp-reply-list-container[data-comment-id="${req.body.replyTo}"]`;
          displayList.addElement(replyListSelector, 'afterBegin', html);
          displayList.removeAttribute(replyListSelector, 'hidden');
        } else {
          displayList.addElement('ul#post-comment-list', 'afterBegin', html);
        }

        displayList.showNotification(
          'Comment created',
          'success',
          'bottom-center',
          4000,
        );

        res.status(200).json({ success: true, displayList });
      } catch (error) {
        this.log.error('failed to process comment', {
          resourceType,
          resourceId: res.locals[resourceKey]._id,
          error,
        });
        return next(error);
      }
    };
  }

  async create (author, resourceType, resource, commentDefinition) {
    const NOW = new Date();
    let comment = new Comment();
    
    comment.created = NOW;
    comment.resourceType = resourceType;
    comment.resource = resource._id;
    comment.authorType = author.type;
    comment.author = author._id;
    if (commentDefinition.replyTo) {
      comment.replyTo = mongoose.Types.ObjectId(commentDefinition.replyTo);
    }
    if (commentDefinition.content) {
      comment.content = striptags(commentDefinition.content.trim());
    }

    comment.flags = {
      isNSFW: commentDefinition.isNSFW === 'on',
    };

    await comment.save();

    const model = mongoose.model(resourceType);
    await model.updateOne(
      { _id: resource._id },
      {
        $inc: { 'stats.commentCount': 1 },
      },
    );

    /*
     * increment the reply count of every parent comment until you reach a
     * comment with no parent.
     */

    let replyTo = comment.replyTo;
    while (replyTo) {
      await Comment.updateOne(
        { _id: replyTo },
        {
          $inc: { 'commentStats.replyCount': 1 },
        },
      );
      let parent = await Comment.findById(replyTo).select('replyTo').lean();
      if (parent.replyTo) {
        replyTo = parent.replyTo;
      } else {
        replyTo = false;
      }
    }

    comment = comment.toObject();
    comment.author = author;

    return comment;
  }

  async update (comment, commentDefinition) {
    const updateOp = { $set: { } };

    if (!commentDefinition.content || (commentDefinition.content.length === 0)) {
      throw new SiteError(406, 'The comment cannot be empty');
    }
    updateOp.$set.content = striptags(commentDefinition.content.trim());
    updateOp.$push = {
      contentHistory: {
        created: new Date(),
        content: comment.content,
      },
    };
    this.log.info('updating comment content', { commentId: comment._id });
    await Comment.updateOne({ _id: comment._id }, updateOp);
  }

  async setStatus (comment, status) {
    await Comment.updateOne({ _id: comment._id }, { $set: { status } });
  }

  /**
   * Pushes the current comment content to the contentHistory array, sets the
   * content field to 'Content removed' and updates the comment status to the
   * status provided. This preserves the comment content, but removes it from
   * public view.
   * @param {Document} comment 
   * @param {String} status 
   */
  async remove (comment, status = 'removed') {
    const { contentReport: contentReportService } = this.dtp.services;
    await Comment.updateOne(
      { _id: comment._id },
      {
        $set: {
          status,
          content: 'Comment removed',
        },
        $push: {
          contentHistory: {
            created: new Date(),
            content: comment.content,
          },
        },
      },
    );
    await contentReportService.removeForResource(comment);
  }

  async getById (commentId) {
    const comment = await Comment
      .findById(commentId)
      .populate(this.populateComment)
      .lean();
    return comment;
  }

  async getForResource (resource, statuses, pagination) {
    const comments = await Comment
      .find({ // index: 'comment_replies'
        resource: resource._id,
        replyTo: { $exists: false },
        status: { $in: statuses },
      })
      .sort({ created: -1 })
      .skip(pagination.skip)
      .limit(pagination.cpp)
      .populate(this.populateComment)
      .lean();
    return comments;
  }

  async getForAuthor (author, pagination) {
    const comments = await Comment
      .find({ // index: 'comment_author_by_type'
        authorType: author.type,
        author: author._id,
        status: { $in: ['published', 'mod-warn'] },
      })
      .sort({ created: -1 })
      .skip(pagination.skip)
      .limit(pagination.cpp)
      .populate(this.populateCommentWithResource)
      .lean();
    return comments;
  }

  async getReplies (comment, pagination) {
    const replies = await Comment
      .find({
        replyTo: comment._id,
        status: {
          $in: ['published', 'mod-warn', 'mod-removed']
        }
      })
      .sort({ created: -1 })
      .skip(pagination.skip)
      .limit(pagination.cpp)
      .populate(this.populateComment)
      .lean();
    return replies;
  }

  async getContentHistory (comment, pagination) {
    /*
     * Extract a page from the contentHistory using $slice on the array
     */
    const fullComment = await Comment
      .findOne(
        { _id: comment._id },
        {
          contentHistory: {
            $sort: { created: 1 },
            $slice: [pagination.skip, pagination.cpp],
          },
        }
      )
      .select('contentHistory').lean();
    if (!fullComment) {
      throw new SiteError(404, 'Comment not found');
    }
    return fullComment.contentHistory || [ ];
  }

  /**
   * Deletes all comments filed against a given resource. Will also get their
   * replies as those are also filed against a resource and will match.
   * @param {Resource} resource The resource for which all comments are to be
   * deleted (physically removed from database).
   */
  async deleteForResource (resource) {
    const { contentReport: contentReportService } = this.dtp.services;
    this.log.info('deleting all comments for resource', { resourceId: resource._id });
    await Comment
      .find({ resource: resource._id })
      .cursor()
      .eachAsync(async (comment) => {
        await contentReportService.removeForResource(comment);
        await Comment.deleteOne({ _id: comment._id });
      }, 4);
  }
}

module.exports = {
  slug: 'comment',
  name: 'comment',
  create: (dtp) => { return new CommentService(dtp); },
};