text: const express = require("express");
  const { restrict, checkAccess } = require("../lib/auth");
  const escape = require("html-entities").AllHtmlEntities;
  const colors = require("colors");
  const bcrypt = require("bcryptjs");
  const moment = require("moment");
  const fs = require("fs");
  const path = require("path");
  const multer = require("multer");
  const mime = require("mime-type/with-db");
  const csrf = require("csurf");
  const util = require("util");
  const stream = require("stream");
  const { validateJson } = require("../lib/schema");
  const {
    clearSessionValue,
    mongoSanitize,
    getThemes,
    getId,
    allowedMimeType,
    fileSizeLimit,
    checkDirectorySync,
    sendEmail,
  } = require("../lib/common");
  const { getConfig, updateConfig } = require("../lib/config");
  const { sortMenu, getMenu, newMenu, updateMenu, deleteMenu, orderMenu } = require("../lib/menu");
  const ObjectId = require("mongodb").ObjectID;
  const router = express.Router();
  const csrfProtection = csrf({ cookie: true });
  
  // Regex
  const emailRegex = /\S+@\S+\.\S+/;
  const numericRegex = /^\d*\.?\d*$/;
  
  // Admin section
  router.get("/admin", restrict, (req, res, next) => {
    res.redirect("/admin/dashboard");
  });
  
  // logout
  router.get("/admin/logout", (req, res) => {
    req.session.user = null;
    req.session.message = null;
    req.session.messageType = null;
    res.redirect("/");
  });
  
  // Used for tests only
  if (process.env.NODE_ENV === "test") {
    router.get("/admin/csrf", csrfProtection, (req, res, next) => {
      res.json({
        csrf: req.csrfToken(),
      });
    });
  }
  
  // login form
  router.get("/admin/login", async (req, res) => {
    const db = req.app.db;
  
    const userCount = await db.users.countDocuments({});
    // we check for a user. If one exists, redirect to login form otherwise setup
    if (userCount && userCount > 0) {
      // set needsSetup to false as a user exists
      req.session.needsSetup = false;
      res.render("login", {
        title: "Login",
        referringUrl: req.header("Referer"),
        config: req.app.config,
        message: clearSessionValue(req.session, "message"),
        messageType: clearSessionValue(req.session, "messageType"),
        helpers: req.handlebars.helpers,
        showFooter: "showFooter",
      });
    } else {
      // if there are no users set the "needsSetup" session
      req.session.needsSetup = true;
      res.redirect("/admin/setup");
    }
  });
  
  // login the user and check the password
  router.post("/admin/login_action", async (req, res) => {
    const db = req.app.db;
  
    const user = await db.users.findOne({ userEmail: mongoSanitize(req.body.email) });
    if (!user || user === null) {
      res.status(400).json({ message: "A user with that email does not exist." });
      return;
    }
  
    // we have a user under that email so we compare the password
    bcrypt.compare(req.body.password, user.userPassword).then((result) => {
      if (result) {
        req.session.user = req.body.email;
        req.session.usersName = user.usersName;
        req.session.userId = user._id.toString();
        req.session.isAdmin = user.isAdmin;
        res.status(200).json({ message: "Login successful" });
        return;
      }
      // password is not correct
      res.status(400).json({ message: "Access denied. Check password and try again." });
    });
  });
  
  // setup form is shown when there are no users setup in the DB
  router.get("/admin/setup", async (req, res) => {
    const db = req.app.db;
  
    const userCount = await db.users.countDocuments({});
    // dont allow the user to "re-setup" if a user exists.
    // set needsSetup to false as a user exists
    req.session.needsSetup = false;
    if (userCount === 0) {
      req.session.needsSetup = true;
      res.render("setup", {
        title: "Setup",
        config: req.app.config,
        helpers: req.handlebars.helpers,
        message: clearSessionValue(req.session, "message"),
        messageType: clearSessionValue(req.session, "messageType"),
        showFooter: "showFooter",
      });
      return;
    }
    res.redirect("/admin/login");
  });
  
  // insert a user
  router.post("/admin/setup_action", async (req, res) => {
    const db = req.app.db;
  
    const doc = {
      usersName: req.body.usersName,
      userEmail: req.body.userEmail,
      userPassword: bcrypt.hashSync(req.body.userPassword, 10),
      isAdmin: true,
      isOwner: true,
    };
  
    // check for users
    const userCount = await db.users.countDocuments({});
    if (userCount === 0) {
      // email is ok to be used.
      try {
        await db.users.insertOne(doc);
        res.status(200).json({ message: "User account inserted" });
        return;
      } catch (ex) {
        console.error(colors.red(`Failed to insert user: ${ex}`));
        res.status(200).json({ message: "Setup failed" });
        return;
      }
    }
    res.status(200).json({ message: "Already setup." });
  });
  
  // dashboard
  router.get("/admin/dashboard", csrfProtection, restrict, async (req, res) => {
    const db = req.app.db;
  
    // Collate data for dashboard
    const dashboardData = {
      productsCount: await db.products.countDocuments({
        productPublished: true,
      }),
      ordersCount: await db.orders.countDocuments({}),
      ordersAmount: await db.orders
        .aggregate([
          { $match: {} },
          {
            $group: { _id: null, sum: { $sum: "$orderTotal" } },
          },
        ])
        .toArray(),
      productsSold: await db.orders
        .aggregate([
          { $match: {} },
          {
            $group: { _id: null, sum: { $sum: "$orderProductCount" } },
          },
        ])
        .toArray(),
      topProducts: await db.orders
        .aggregate([
          { $project: { _id: 0 } },
          { $project: { o: { $objectToArray: "$orderProducts" } } },
          { $unwind: "$o" },
          {
            $group: {
              _id: "$o.v.title",
              productImage: { $last: "$o.v.productImage" },
              count: { $sum: "$o.v.quantity" },
            },
          },
          { $sort: { count: -1 } },
          { $limit: 5 },
        ])
        .toArray(),
    };
  
    // Fix aggregate data
    if (dashboardData.ordersAmount.length > 0) {
      dashboardData.ordersAmount = dashboardData.ordersAmount[0].sum;
    }
    if (dashboardData.productsSold.length > 0) {
      dashboardData.productsSold = dashboardData.productsSold[0].sum;
    } else {
      dashboardData.productsSold = 0;
    }
  
    res.render("dashboard", {
      title: "Cart dashboard",
      session: req.session,
      admin: true,
      dashboardData,
      themes: getThemes(),
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      csrfToken: req.csrfToken(),
    });
  });
  
  // settings
  router.get("/admin/settings", csrfProtection, restrict, (req, res) => {
    res.render("settings", {
      title: "Cart settings",
      session: req.session,
      admin: true,
      themes: getThemes(),
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      footerHtml: typeof req.app.config.footerHtml !== "undefined" ? escape.decode(req.app.config.footerHtml) : null,
      googleAnalytics:
        typeof req.app.config.googleAnalytics !== "undefined" ? escape.decode(req.app.config.googleAnalytics) : null,
      csrfToken: req.csrfToken(),
    });
  });
  
  // create API key
  router.post("/admin/createApiKey", restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
    const result = await db.users.findOneAndUpdate(
      {
        _id: ObjectId(req.session.userId),
        isAdmin: true,
      },
      {
        $set: {
          apiKey: new ObjectId(),
        },
      },
      {
        returnOriginal: false,
      }
    );
  
    if (result.value && result.value.apiKey) {
      res.status(200).json({ message: "API Key generated", apiKey: result.value.apiKey });
      return;
    }
    res.status(400).json({ message: "Failed to generate API Key" });
  });
  
  // settings update
  router.post("/admin/settings/update", restrict, checkAccess, (req, res) => {
    const result = updateConfig(req.body);
    if (result === true) {
      req.app.config = getConfig();
      res.status(200).json({ message: "Cập nhật thàng công" });
      return;
    }
    res.status(400).json({ message: "Permission denied" });
  });
  
  // settings menu
  router.get("/admin/settings/menu", csrfProtection, restrict, async (req, res) => {
    const db = req.app.db;
    res.render("settings-menu", {
      title: "Cart menu",
      session: req.session,
      admin: true,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      menu: sortMenu(await getMenu(db)),
      csrfToken: req.csrfToken(),
    });
  });
  
  // page list
  router.get("/admin/settings/pages", csrfProtection, restrict, async (req, res) => {
    const db = req.app.db;
    const pages = await db.pages.find({}).toArray();
  
    res.render("settings-pages", {
      title: "Static pages",
      pages: pages,
      session: req.session,
      admin: true,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      menu: sortMenu(await getMenu(db)),
      csrfToken: req.csrfToken(),
    });
  });
  
  // pages new
  router.get("/admin/settings/pages/new", csrfProtection, restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    res.render("settings-page", {
      title: "Static pages",
      session: req.session,
      admin: true,
      button_text: "Create",
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      menu: sortMenu(await getMenu(db)),
      csrfToken: req.csrfToken(),
    });
  });
  
  // pages editor
  router.get("/admin/settings/pages/edit/:page", csrfProtection, restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
    const page = await db.pages.findOne({ _id: getId(req.params.page) });
    const menu = sortMenu(await getMenu(db));
    if (!page) {
      res.status(404).render("error", {
        title: "404 Error - Page not found",
        config: req.app.config,
        message: "404 Error - Page not found",
        helpers: req.handlebars.helpers,
        showFooter: "showFooter",
        menu,
      });
      return;
    }
  
    res.render("settings-page", {
      title: "Static pages",
      page: page,
      button_text: "Update",
      session: req.session,
      admin: true,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      menu,
      csrfToken: req.csrfToken(),
    });
  });
  
  // insert/update page
  router.post("/admin/settings/page", restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    const doc = {
      pageName: req.body.pageName,
      pageSlug: req.body.pageSlug,
      pageEnabled: req.body.pageEnabled,
      pageContent: req.body.pageContent,
    };
  
    if (req.body.pageId) {
      // existing page
      const page = await db.pages.findOne({ _id: getId(req.body.pageId) });
      if (!page) {
        res.status(400).json({ message: "Page not found" });
        return;
      }
  
      try {
        const updatedPage = await db.pages.findOneAndUpdate(
          { _id: getId(req.body.pageId) },
          { $set: doc },
          { returnOriginal: false }
        );
        res.status(200).json({ message: "Cập nhật thành cồng", pageId: req.body.pageId, page: updatedPage.value });
      } catch (ex) {
        res.status(400).json({ message: "Error updating page. Please try again." });
      }
    } else {
      // insert page
      try {
        const newDoc = await db.pages.insertOne(doc);
        res.status(200).json({ message: "Trang mới đã được tạo", pageId: newDoc.insertedId });
        return;
      } catch (ex) {
        res.status(400).json({ message: "Error creating page. Please try again." });
      }
    }
  });
  
  // delete a page
  router.post("/admin/settings/page/delete", restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    const page = await db.pages.findOne({ _id: getId(req.body.pageId) });
    if (!page) {
      res.status(400).json({ message: "Page not found" });
      return;
    }
  
    try {
      await db.pages.deleteOne({ _id: getId(req.body.pageId) }, {});
      res.status(200).json({ message: "Xóa thành côn" });
      return;
    } catch (ex) {
      res.status(400).json({ message: "Error deleting page. Please try again." });
    }
  });
  
  // new menu item
  router.post("/admin/settings/menu/new", restrict, checkAccess, (req, res) => {
    const result = newMenu(req);
    if (result === false) {
      res.status(400).json({ message: "Failed creating menu." });
      return;
    }
    res.status(200).json({ message: "Tạo mới thành công." });
  });
  
  // update existing menu item
  router.post("/admin/settings/menu/update", restrict, checkAccess, (req, res) => {
    const result = updateMenu(req);
    if (result === false) {
      res.status(400).json({ message: "Failed updating menu." });
      return;
    }
    res.status(200).json({ message: "Cập nhật thành công" });
  });
  
  // delete menu item
  router.post("/admin/settings/menu/delete", restrict, checkAccess, (req, res) => {
    const result = deleteMenu(req, req.body.menuId);
    if (result === false) {
      res.status(400).json({ message: "Failed deleting menu." });
      return;
    }
    res.status(200).json({ message: "Xóa thành công." });
  });
  
  // We call this via a Ajax call to save the order from the sortable list
  router.post("/admin/settings/menu/saveOrder", restrict, checkAccess, (req, res) => {
    const result = orderMenu(req, res);
    if (result === false) {
      res.status(400).json({ message: "Failed saving menu order" });
      return;
    }
    res.status(200).json({});
  });
  
  // validate the permalink
  router.post("/admin/validatePermalink", async (req, res) => {
    // if doc id is provided it checks for permalink in any products other that one provided,
    // else it just checks for any products with that permalink
    const db = req.app.db;
  
    let query = {};
    if (typeof req.body.docId === "undefined" || req.body.docId === "") {
      query = { productPermalink: req.body.permalink };
    } else {
      query = { productPermalink: req.body.permalink, _id: { $ne: getId(req.body.docId) } };
    }
  
    const products = await db.products.countDocuments(query);
    if (products && products > 0) {
      res.status(400).json({ message: "Permalink already exists" });
      return;
    }
    res.status(200).json({ message: "Validate thành công" });
  });
  
  // Discount codes
  router.get("/admin/settings/discounts", csrfProtection, restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    const discounts = await db.discounts.find({}).toArray();
  
    res.render("settings-discounts", {
      title: "Discount code",
      config: req.app.config,
      session: req.session,
      discounts,
      admin: true,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      csrfToken: req.csrfToken(),
    });
  });
  
  // Edit a discount code
  router.get("/admin/settings/discount/edit/:id", csrfProtection, restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    const discount = await db.discounts.findOne({ _id: getId(req.params.id) });
  
    res.render("settings-discount-edit", {
      title: "Discount code edit",
      session: req.session,
      admin: true,
      discount,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      csrfToken: req.csrfToken(),
    });
  });
  
  // Update discount code
  router.post("/admin/settings/discount/update", restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    // Doc to insert
    const discountDoc = {
      discountId: req.body.discountId,
      code: req.body.code,
      type: req.body.type,
      value: parseInt(req.body.value),
      start: moment(req.body.start, "DD/MM/YYYY HH:mm").toDate(),
      end: moment(req.body.end, "DD/MM/YYYY HH:mm").toDate(),
    };
  
    // Validate the body again schema
    const schemaValidate = validateJson("editDiscount", discountDoc);
    if (!schemaValidate.result) {
      res.status(400).json(schemaValidate.errors);
      return;
    }
  
    // Check start is after today
    if (moment(discountDoc.start).isBefore(moment())) {
      res.status(400).json({ message: "Discount start date needs to be after today" });
      return;
    }
  
    // Check end is after the start
    if (!moment(discountDoc.end).isAfter(moment(discountDoc.start))) {
      res.status(400).json({ message: "Discount end date needs to be after start date" });
      return;
    }
  
    // Check if code exists
    const checkCode = await db.discounts.countDocuments({
      code: discountDoc.code,
      _id: { $ne: getId(discountDoc.discountId) },
    });
    if (checkCode) {
      res.status(400).json({ message: "Discount code already exists" });
      return;
    }
  
    // Remove discountID
    delete discountDoc.discountId;
  
    try {
      await db.discounts.updateOne({ _id: getId(req.body.discountId) }, { $set: discountDoc }, {});
      res.status(200).json({ message: "Successfully saved", discount: discountDoc });
    } catch (ex) {
      res.status(400).json({ message: "Failed to save. Please try again" });
    }
  });
  
  // Create a discount code
  router.get("/admin/settings/discount/new", csrfProtection, restrict, checkAccess, async (req, res) => {
    res.render("settings-discount-new", {
      title: "Discount code create",
      session: req.session,
      admin: true,
      message: clearSessionValue(req.session, "message"),
      messageType: clearSessionValue(req.session, "messageType"),
      helpers: req.handlebars.helpers,
      config: req.app.config,
      csrfToken: req.csrfToken(),
    });
  });
  
  // Create a discount code
  router.post("/admin/settings/discount/create", csrfProtection, restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    // Doc to insert
    const discountDoc = {
      code: req.body.code,
      type: req.body.type,
      value: parseInt(req.body.value),
      start: moment(req.body.start, "DD/MM/YYYY HH:mm").toDate(),
      end: moment(req.body.end, "DD/MM/YYYY HH:mm").toDate(),
    };
  
    // Validate the body again schema
    const schemaValidate = validateJson("newDiscount", discountDoc);
    if (!schemaValidate.result) {
      res.status(400).json(schemaValidate.errors);
      return;
    }
  
    // Check if code exists
    const checkCode = await db.discounts.countDocuments({
      code: discountDoc.code,
    });
    if (checkCode) {
      res.status(400).json({ message: "Discount code already exists" });
      return;
    }
  
    // Check start is after today
    if (moment(discountDoc.start).isBefore(moment())) {
      res.status(400).json({ message: "Ngày bắt đầu giảm giá phải sau ngày hôm nay" });
      return;
    }
  
    // Check end is after the start
    if (!moment(discountDoc.end).isAfter(moment(discountDoc.start))) {
      res.status(400).json({ message: "Ngày kết thúc giảm giá phải sau ngày bắt đầu" });
      return;
    }
  
    // Insert discount code
    const discount = await db.discounts.insertOne(discountDoc);
    res.status(200).json({ message: "Tạo mã giảm giá thành công", discountId: discount.insertedId });
  });
  
  // Delete discount code
  router.delete("/admin/settings/discount/delete", restrict, checkAccess, async (req, res) => {
    const db = req.app.db;
  
    try {
      await db.discounts.deleteOne({ _id: getId(req.body.discountId) }, {});
      res.status(200).json({ message: "Xóa mã giảm giá thành công" });
      return;
    } catch (ex) {
      res.status(400).json({ message: "Error deleting discount code. Please try again." });
    }
  });
  
  // upload the file
  const upload = multer({ dest: "public/uploads/" });
  router.post("/admin/file/upload", restrict, checkAccess, upload.single("uploadFile"), async (req, res) => {
    const db = req.app.db;
  
    if (req.file) {
      const file = req.file;
  
      // Get the mime type of the file
      const mimeType = mime.lookup(file.originalname);
  
      // Check for allowed mime type and file size
      if (!allowedMimeType.includes(mimeType) || file.size > fileSizeLimit) {
        // Remove temp file
        fs.unlinkSync(file.path);
  
        // Return error
        res.status(400).json({ message: "File type not allowed or too large. Please try again." });
        return;
      }
  
      // get the product form the DB
      const product = await db.products.findOne({ _id: getId(req.body.productId) });
      if (!product) {
        // delete the temp file.
        fs.unlinkSync(file.path);
  
        // Return error
        res.status(400).json({ message: "File upload error. Please try again." });
        return;
      }
  
      const productPath = product._id.toString();
      const uploadDir = path.join("public/uploads", productPath);
  
      // Check directory and create (if needed)
      checkDirectorySync(uploadDir);
  
      // Setup the new path
      const imagePath = path.join("/uploads", productPath, file.originalname.replace(/ /g, "_"));
  
      // save the new file
      const dest = fs.createWriteStream(path.join(uploadDir, file.originalname.replace(/ /g, "_")));
      const pipeline = util.promisify(stream.pipeline);
  
      try {
        await pipeline(fs.createReadStream(file.path), dest);
  
        // delete the temp file.
        fs.unlinkSync(file.path);
  
        // if there isn't a product featured image, set this one
        if (!product.productImage) {
          await db.products.updateOne(
            { _id: getId(req.body.productId) },
            { $set: { productImage: imagePath } },
            { multi: false }
          );
        }
        res.status(200).json({ message: "Thêm ảnh thành công" });
      } catch (ex) {
        console.log("Failed to upload the file", ex);
        res.status(400).json({ message: "File upload error. Please try again." });
      }
    } else {
      // Return error
      console.log("fail", req.file);
      res.status(400).json({ message: "File upload error. Please try again." });
    }
  });
  
  // delete a file via ajax request
  router.post("/admin/testEmail", restrict, (req, res) => {
    const config = req.app.config;
    // TODO: Should fix this to properly handle result
    sendEmail(config.emailAddress, "expressCart test email", "Your email settings are working");
    res.status(200).json({ message: "Test email sent" });
  });
  
  router.post("/admin/searchall", restrict, async (req, res, next) => {
    const db = req.app.db;
    const searchValue = req.body.searchValue;
    const limitReturned = 5;
  
    // Empty arrays
    let customers = [];
    let orders = [];
    let products = [];
  
    // Default queries
    const customerQuery = {};
    const orderQuery = {};
    const productQuery = {};
  
    // If an ObjectId is detected use that
    if (ObjectId.isValid(req.body.searchValue)) {
      // Get customers
      customers = await db.customers
        .find({
          _id: ObjectId(searchValue),
        })
        .limit(limitReturned)
        .sort({ created: 1 })
        .toArray();
  
      // Get orders
      orders = await db.orders
        .find({
          _id: ObjectId(searchValue),
        })
        .limit(limitReturned)
        .sort({ orderDate: 1 })
        .toArray();
  
      // Get products
      products = await db.products
        .find({
          _id: ObjectId(searchValue),
        })
        .limit(limitReturned)
        .sort({ productAddedDate: 1 })
        .toArray();
  
      return res.status(200).json({
        customers,
        orders,
        products,
      });
    }
  
    // If email address is detected
    if (emailRegex.test(req.body.searchValue)) {
      customerQuery.email = searchValue;
      orderQuery.orderEmail = searchValue;
    } else if (numericRegex.test(req.body.searchValue)) {
      // If a numeric value is detected
      orderQuery.amount = req.body.searchValue;
      productQuery.productPrice = req.body.searchValue;
    } else {
      // String searches
      customerQuery.$or = [
        { firstName: { $regex: new RegExp(searchValue, "img") } },
        { lastName: { $regex: new RegExp(searchValue, "img") } },
      ];
      orderQuery.$or = [
        { orderFirstname: { $regex: new RegExp(searchValue, "img") } },
        { orderLastname: { $regex: new RegExp(searchValue, "img") } },
      ];
      productQuery.$or = [
        { productTitle: { $regex: new RegExp(searchValue, "img") } },
        { productDescription: { $regex: new RegExp(searchValue, "img") } },
      ];
    }
  
    // Get customers
    if (Object.keys(customerQuery).length > 0) {
      customers = await db.customers.find(customerQuery).limit(limitReturned).sort({ created: 1 }).toArray();
    }
  
    // Get orders
    if (Object.keys(orderQuery).length > 0) {
      orders = await db.orders.find(orderQuery).limit(limitReturned).sort({ orderDate: 1 }).toArray();
    }
  
    // Get products
    if (Object.keys(productQuery).length > 0) {
      products = await db.products.find(productQuery).limit(limitReturned).sort({ productAddedDate: 1 }).toArray();
    }
  
    return res.status(200).json({
      customers,
      orders,
      products,
    });
  });
  
  module.exports = router;
  