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

'use strict';

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

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

const mongoose = require('mongoose');
const ObjectId = mongoose.Types.ObjectId;

const Page = mongoose.model('Page');

class PageService extends SiteService {

  constructor (dtp) {
    super(dtp, module.exports);
  }

  async menuMiddleware (req, res, next) {
    try {
      let mainMenu = await this.getMainMenuPages();       
      
      if (!mainMenu) {
      
        await this.cacheMainMenuPages();
        mainMenu = await this.getMainMenuPages();
      }
      res.locals.mainMenu = mainMenu;
      return next();
    } catch (error) {
      this.log.error('failed to build page menu', { error });
      return next();
    }
  }

  async createPlaceholder (author) {
    const NOW = new Date();

    if (!author.flags.isAdmin) {
      throw new SiteError(403, 'You are not permitted to author pages');
    }

    let page = new Page();
    page.created = NOW;
    page.authorType = author.type;
    page.author = author._id;
    page.title = "New Draft page";
    page.slug = `draft-page-${page._id}`;
    await page.save();

    page = page.toObject();
    page.author = author; // self-populate instead of calling db

    return page;
  }

  async create (author, pageDefinition) {
    if (!author.permissions.canAuthorPages) {
      throw new SiteError(403, 'You are not permitted to author pages');
    }

    const page = new Page();
    page.title = striptags(pageDefinition.title.trim());
    page.slug = this.createPageSlug(page._id, page.title);
    page.content = pageDefinition.content.trim();
    page.status = pageDefinition.status || 'draft';

    page.menu = {
      icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
      label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
      order: parseInt(pageDefinition.menuOrder || '0', 10),
    };

    if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
      page.menu.parent = pageDefinition.parentPageId;
    }
    await page.save();

    await this.cacheMainMenuPages();
    
    return page.toObject();
  }

  async update (user, page, pageDefinition) {
    const NOW = new Date();
    const updateOp = {
      $set: {
        updated: NOW,
      },
    };

    // if (!user.permissions.canAuthorPages) {
    //   throw new SiteError(403, 'You are not permitted to author or change pages.');
    // }

    if (pageDefinition.title) {
      updateOp.$set.title = striptags(pageDefinition.title.trim());
    }
    if (pageDefinition.slug) {
      let pageSlug = striptags(slug(pageDefinition.slug.trim())).split('-');
      while (ObjectId.isValid(pageSlug[pageSlug.length - 1])) {
        pageSlug.pop();
      }
      pageSlug = pageSlug.splice(0, 4);
      pageSlug.push(page._id.toString());
      updateOp.$set.slug = `${pageSlug.join('-')}`;
    }
    if (pageDefinition.summary) {
      updateOp.$set.summary = striptags(pageDefinition.summary.trim());
    }
    if (pageDefinition.content) {
      updateOp.$set.content = pageDefinition.content.trim();
    }

    updateOp.$set.menu = {
      icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
      label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
      order: parseInt(pageDefinition.menuOrder || '0', 10),
    };

    if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
      updateOp.$set.menu.parent = pageDefinition.parentPageId;
    }

    // if old status is not published and new status is published, we have to
    // verify that the calling user has Publisher privileges.
    if ((page.status !== 'published') && (pageDefinition.status === 'published')) {
      if (!user.permissions.canPublishPages) {
        throw new SiteError(403, 'You are not permitted to publish pages');
      }
    }
    updateOp.$set.status = striptags(pageDefinition.status.trim());
    
    await Page.updateOne(
      { _id: page._id },
      updateOp,
      { upsert: true },
    );
    
    await this.cacheMainMenuPages();
  }

  async getPages (pagination, status = ['published']) {
    if (!Array.isArray(status)) {
      status = [status];
    }
    const pages = await Page
      .find({ status: { $in: status } })
      .sort({ created: -1 })
      .skip(pagination.skip)
      .limit(pagination.cpp)
      .lean();
    return pages;
  }

  async getById (pageId, populateParent) {
    if (populateParent) {
      const page = await Page
      .findById(pageId)
      .select('+content +menu')
      .populate('menu.parent')
      .lean();
      return page;
    } else {
      const page = await Page
      .findById(pageId)
      .select('+content')
      .lean();
      return page;
    }
  }

  async getBySlug (pageSlug) {
    const slugParts = pageSlug.split('-');
    const pageId = slugParts[slugParts.length - 1];
    return this.getById(pageId);
  }

  async isParentPage (page) {
    if (page) {
      page = [page];
    }
    const parentPage = await Page.distinct( 'menu.parent', { "menu.parent" : { $in : page } } );

    return parentPage.length > 0;
  }

  async getAvailablePages (excludedPageIds) {
    let search = { };

    if (excludedPageIds) {
      search._id = { $nin: excludedPageIds };
    }
    const pages = (await Page.find(search).lean()).filter((page) => { 
      if (page.menu && !page.menu.parent ) { 
        return page; 
      }
    });
    return pages;
  }

  async deletePage (page, options) {
    options = Object.assign({ updateCache: true }, options);

    this.log.info('deleting page', { pageId: page._id, options });
    await Page.deleteOne({ _id: page._id });

    if (options.updateCache) {
      await this.cacheMainMenuPages();
    }
  }

  async removeForAuthor (author) {
    /*
     * Execute the updates without page cache updates
     */
    await Page
      .find({ author: author._id })
      .cursor()
      .eachAsync(async (page) => {
        try {
          await this.deletePage(page, { updateCache: false });
        } catch (error) {
          this.log.error('failed to remove page for author', { error });
          // fall through
        }
      });

    /*
     * and update the page cache once, instead.
     */
    await this.cacheMainMenuPages();
  }

  createPageSlug (pageId, pageTitle) {
    if ((typeof pageTitle !== 'string') || (pageTitle.length < 1)) {
      throw new Error('Invalid input for making a page slug');
    }
    const pageSlug = slug(pageTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-');
    return `${pageSlug}-${pageId}`;
  }

  async cacheMainMenuPages ( ) {
    let mainMenu = [];

    try {
      await Page
        .find({ status: 'published' })
        .select('slug menu')
        .populate({path: 'menu.parent'})
        .lean()
        .cursor()
        .eachAsync(async (page) => {
          if (page.menu.parent) {
            let parent = page.menu.parent;
            if (parent.status === 'published') {
              let parentPage = mainMenu.find(item => item.slug === parent.slug);
              if (parentPage) {
                let childPage = {
                  url: `/page/${page.slug}`,
                  slug: page.slug,
                  icon: page.menu.icon,
                  label: page.menu.label,
                  order: page.menu.order,
                };
                parentPage.children.splice(childPage.order, 0, childPage);
              }
              else {
                let parentPage = {
                  url: `/page/${parent.slug}`,
                  slug: parent.slug,
                  icon: parent.menu.icon,
                  label: parent.menu.label,
                  order: parent.menu.order,
                  children: [],
                };
                let childPage = {
                  url: `/page/${page.slug}`,
                  slug: page.slug,
                  icon: page.menu.icon,
                  label: page.menu.label,
                  order: page.menu.order,
                };
                parentPage.children.splice(childPage.order, 0, childPage);
                mainMenu.splice(parentPage.order, 0, parentPage);
              }
            } else {
              let menuPage = { 
                url: `/page/${page.slug}`,
                slug: page.slug,
                icon: page.menu.icon,
                label: page.menu.label,
                order: page.menu.order,
                children: [],
              };
              mainMenu.splice(menuPage.order, 0, menuPage);
            }
          } else {
            let isPageInMenu = mainMenu.find(item => item.slug === page.slug);

            if (!isPageInMenu) {   
              let menuPage = { 
                url: `/page/${page.slug}`,
                slug: page.slug,
                icon: page.menu.icon,
                label: page.menu.label,
                order: page.menu.order,
                children: [],
              };
              mainMenu.push(menuPage);
            }
          }
        });

      /*
       * Sort the menu data
       */
      mainMenu.sort((a, b) => a.order - b.order);
      for (const menu of mainMenu) {
        if (menu.children) {
          menu.children.sort((a, b) => a.order - b.order);
        }
      }

      /*
       * Update the cache
       */
      await this.dtp.services.cache.setObject("mainMenu", mainMenu);
    } catch (error) {
      this.dtp.log.error('failed to build page menu', { error });
    }
  }
  
  async getMainMenuPages() {
    return this.dtp.services.cache.getObject("mainMenu");
  }
}
 

module.exports = {
  logId: 'svc:page',
  index: 'page',
  className: 'PageService',
  create: (dtp) => { return new PageService(dtp); },
};