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

'use strict';

const express = require('express');
const mongoose = require('mongoose');

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

class UserController extends SiteController {

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

  async start ( ) {
    const { dtp } = this;
    const {
      csrfToken: csrfTokenService,
      limiter: limiterService,
      otpAuth: otpAuthService,
      session: sessionService,
    } = dtp.services;

    const upload = this.createMulter('user', {
      limits: {
        fileSize: 1024 * 1000 * 5, // 5MB
      },
    });

    const router = express.Router();
    dtp.app.use('/user', router);

    const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });

    const otpSetup = otpAuthService.middleware('Account', {
      adminRequired: false,
      otpRequired: true,
      otpRedirectURL: async (req) => { return `/user/${req.user.username}`; },
    });
    const otpMiddleware = otpAuthService.middleware('Account', {
      adminRequired: false,
      otpRequired: false,
      otpRedirectURL: async (req) => { return `/user/${req.user.username}`; },
    });

    router.use(
      async (req, res, next) => {
        try {
          res.locals.currentView = 'user';
          res.locals.pageTitle = 'Manage your user account.';
          return next();
        } catch (error) {
          return next(error);
        }
      },
    );

    async function checkProfileOwner (req, res, next) {
      if (!req.user || !req.user._id.equals(res.locals.userProfile._id)) {
        return next(new SiteError(403, 'This is not your user account or profile'));
      }
      return next();
    }

    router.param('localUsername', this.populateLocalUsername.bind(this));
    router.param('coreUsername', this.populateCoreUsername.bind(this));

    router.param('localUserId', this.populateLocalUserId.bind(this));
    router.param('coreUserId', this.populateCoreUserId.bind(this));

    router.post(
      '/core/:coreUserId/settings',
      limiterService.createMiddleware(limiterService.config.user.postUpdateCoreSettings),
      checkProfileOwner,
      upload.none(),
      this.postUpdateCoreSettings.bind(this),
    );

    router.post(
      '/:localUserId/profile-photo',
      limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
      checkProfileOwner,
      upload.single('imageFile'),
      this.postProfilePhoto.bind(this),
    );

    router.post(
      '/:localUserId/settings',
      limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
      checkProfileOwner,
      upload.none(),
      this.postUpdateSettings.bind(this),
    );

    router.post(
      '/',
      limiterService.createMiddleware(limiterService.config.user.postCreate),
      csrfTokenService.middleware({ name: 'user-create' }),
      this.postCreateUser.bind(this),
    );

    router.get(
      '/core/:coreUserId/settings',
      limiterService.createMiddleware(limiterService.config.user.getSettings),
      authRequired,
      otpMiddleware,
      checkProfileOwner,
      this.getCoreUserSettingsView.bind(this),
    );
    router.get(
      '/core/:coreUserId',
      limiterService.createMiddleware(limiterService.config.user.getUserProfile),
      authRequired,
      otpMiddleware,
      this.getUserView.bind(this),
    );

    router.get(
      '/:localUserId/otp-setup',
      limiterService.createMiddleware(limiterService.config.user.getOtpSetup),
      otpSetup,
      this.getOtpSetup.bind(this),
    );
    router.get(
      '/:localUserId/otp-disable',
      limiterService.createMiddleware(limiterService.config.user.getOtpDisable),
      authRequired,
      this.getOtpDisable.bind(this),
    );

    router.get(
      '/:localUsername/settings',
      limiterService.createMiddleware(limiterService.config.user.getSettings),
      authRequired,
      otpMiddleware,
      checkProfileOwner,
      this.getUserSettingsView.bind(this),
    );
    router.get(
      '/:localUsername',
      limiterService.createMiddleware(limiterService.config.user.getUserProfile),
      authRequired,      
      otpMiddleware,
      this.getUserView.bind(this),
    );

    router.delete(
      '/:localUserId/profile-photo',
      limiterService.createMiddleware(limiterService.config.user.deleteProfilePhoto),
      authRequired,      
      checkProfileOwner,
      this.deleteProfilePhoto.bind(this),
    );
  }

  async populateCoreUsername (req, res, next, coreUsername) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      res.locals.username = userService.filterUsername(coreUsername);
      res.locals.userProfileId = await userService.getCoreUserId(res.locals.username);
      if (!res.locals.userProfileId) {
        throw new SiteError(404, 'Core member not found');
      }
      // manually chain over to the ID parameter resolver
      return this.populateCoreUserId(req, res, next, res.locals.userProfileId);
    } catch (error) {
      this.log.error('failed to populate core username', { coreUsername, error });
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'populateCoreUsername',
        message: `failed to populate Core user: ${error.message}`,
        data: { coreUsername, error },
      });
      return next(error);
    }
  }

  async populateCoreUserId (req, res, next, coreUserId) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      res.locals.userProfileId = mongoose.Types.ObjectId(coreUserId);

      if (req.user && (req.user.type === 'CoreUser') && req.user._id.equals(res.locals.userProfileId)) {
        res.locals.userProfile = await userService.getCoreUserAccount(res.locals.userProfileId);
      } else {
        res.locals.userProfile = await userService.getCoreUserProfile(res.locals.userProfileId);
      }

      if (!res.locals.userProfile) {
        throw new SiteError(404, 'Core member not found');
      }

      return next();
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'populateCoreUserId',
        message: `failed to populate core user: ${error.message}`,
        data: { coreUserId, error },
      });
      return next(error);
    }
  }

  async populateLocalUsername (req, res, next, username) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      res.locals.username = userService.filterUsername(username);

      res.locals.userProfileId = await userService.getLocalUserId(res.locals.username);
      if (!res.locals.userProfileId) {
        throw new SiteError(404, 'Local member not found');
      }

      if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
        res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
      } else {
        res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
      }
      return next();
    } catch (error) {
      this.log.error('failed to populate local username', { username, error });
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'populateLocalUsername',
        message: `failed to populate local user: ${error.message}`,
        data: { username, error },
      });
      return next(error);
    }
  }

  async populateLocalUserId (req, res, next, userId) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      res.locals.userProfileId = mongoose.Types.ObjectId(userId);

      if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
        res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
      } else {
        res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
      }

      if (!res.locals.userProfile) {
        throw new SiteError(404, 'Local member not found');
      }

      return next();
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'populateLocalUserId',
        message: `failed to populate local user: ${error.message}`,
        data: { userId, error },
      });
      return next(error);
    }
  }

  async postCreateUser (req, res, next) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      // verify that the request has submitted a captcha
      if ((typeof req.body.captcha !== 'string') || req.body.captcha.length === 0) {
        throw new SiteError(403, 'Invalid signup attempt');
      }
      // verify that the session has a signup captcha
      if (!req.session.captcha || !req.session.captcha.signup) {
        throw new SiteError(403, 'Invalid signup attempt');
      }
      // verify that the captcha from the form matches the captcha in the signup session flow
      if (req.body.captcha !== req.session.captcha.signup) {
        throw new SiteError(403, 'The captcha value is not correct');
      }

      // create the user account
      res.locals.user = await userService.create(req.body);

      const form = Object.assign(req.body);
      if (form.password) {
        delete form.password;
      }
      if (form.passwordv) {
        delete form.passwordv;
      }

      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'postCreateUser',
        data: {
          form,
          user: {
            _id: res.locals.user._id,
            username: res.locals.user.username,
          },
        },
      });

      // log the user in
      req.login(res.locals.user, (error) => {
        if (error) {
          loganService.sendRequestEvent(module.exports, req, {
            level: 'error',
            event: 'postCreateUser',
            message: `failed to start user session: ${error.message}`,
            data: { error },
          });
          return next(error);
        }
        loganService.sendRequestEvent(module.exports, req, {
          level: 'info',
          event: 'postCreateUser',
          message: 'user session started',
        });
        res.redirect(`/user/${res.locals.user.username}`);
      });
    } catch (error) {
      this.log.error('failed to create new user', { error });
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'postCreateUser',
        message: `failed to create user account: ${error.message}`,
        data: { definition: req.body, error },
      });
      return next(error);
    }
  }

  async postProfilePhoto (req, res) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      await userService.updatePhoto(req.user, req.file);

      const displayList = this.createDisplayList('profile-photo');
      displayList.showNotification(
        'Profile photo updated successfully.',
        'success',
        'bottom-center',
        2000,
      );

      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'postProfilePhoto',
        message: 'profile photo updated',
      });

      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'postProfilePhoto',
        message: `failed to update profile photo: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async postHeaderImage (req, res) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      await userService.updateHeaderImage(req.user, req.file);

      const displayList = this.createDisplayList('header-image');
      displayList.showNotification(
        'Header image updated successfully.',
        'success',
        'bottom-center',
        2000,
      );

      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'postHeaderImage',
        message: 'header image updated',
      });

      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'postHeaderImage',
        message: `failed to update header image: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async postUpdateCoreSettings (req, res) {
    const {
      coreNode: coreNodeService,
      logan: loganService,
    } = this.dtp.services;
    try {
      await coreNodeService.updateUserSettings(req.user, req.body);

      const displayList = this.createDisplayList('app-settings');
      displayList.reload();

      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'postUpdateCoreSettings',
        message: 'CoreUser settings updated',
      });

      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'postUpdateCoreSettings',
        message: `failed to update CoreUser settings: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async postUpdateSettings (req, res) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      await userService.updateSettings(req.user, req.body);

      const displayList = this.createDisplayList('app-settings');
      displayList.reload();

      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'postUpdateSettings',
        message: 'account settings updates',
      });

      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'postUpdateSettings',
        message: `failed to update account settings: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async getOtpSetup (req, res) {
    res.render('user/otp-setup-complete');
  }

  async getOtpDisable (req, res) {
    const {
      logan: loganService,
      otpAuth: otpAuthService,
    } = this.dtp.services;
    try {
      await otpAuthService.destroyOtpSession(req, 'Account');
      await otpAuthService.removeForUser(req.user, 'Account');
      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'getOtpDisable',
        message: 'one-time passwords disabled',
      });
      res.render('user/otp-disabled');
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'getOtpDisable',
        message: `failed to disable OTP: ${error.message}`,
        data: { error },
      });
      res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async getCoreUserSettingsView (req, res, next) {
    const {
      logan: loganService,
      otpAuth: otpAuthService,
    } = this.dtp.services;
    try {
      res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
      res.locals.startTab = req.query.st || 'watch';
      res.render('user/settings-core');
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'getCoreUserSettingsView',
        message: `failed to render the view: ${error.message}`,
        data: { error },
      });
      return next(error);
    }
  }

  async getUserSettingsView (req, res, next) {
    const {
      logan: loganService,
      otpAuth: otpAuthService,
    } = this.dtp.services;
    try {
      res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
      res.locals.startTab = req.query.st || 'watch';
      res.render('user/settings');
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'getUserSettingsView',
        message: `failed to render the view: ${error.message}`,
        data: { error },
      });
      return next(error);
    }
  }

  async getUserView (req, res, next) {
    const {
      comment: commentService,
      logan: loganService,
    } = this.dtp.services;
    try {
      res.locals.pageTitle = `${res.locals.userProfile.username}'s Profile`;
      res.locals.pagination = this.getPaginationParameters(req, 20);
      res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination);
      res.render('user/profile');
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'getUserView',
        message: `failed to render the view: ${error.message}`,
        data: { error },
      });
      return next(error);
    }
  }

  async deleteProfilePhoto (req, res) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      const displayList = this.createDisplayList('app-settings');
      await userService.removePhoto(req.user);
      displayList.showNotification(
        'Profile photo removed successfully.',
        'success',
        'bottom-center',
        2000,
      );
      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'deleteProfilePhoto',
        message: 'profile photo removed',
      });
      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'deleteProfilePhoto',
        message: `failed to remove profile photo: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }

  async deleteHeaderImage (req, res) {
    const {
      logan: loganService,
      user: userService,
    } = this.dtp.services;
    try {
      const displayList = this.createDisplayList('remove-header-image');
      await userService.removeHeaderImage(req.user);
      displayList.showNotification(
        'Header image removed successfully.',
        'success',
        'bottom-center',
        2000,
      );
      loganService.sendRequestEvent(module.exports, req, {
        level: 'info',
        event: 'deleteHeaderImage',
        message: 'profile header image removed',
      });
      res.status(200).json({ success: true, displayList });
    } catch (error) {
      loganService.sendRequestEvent(module.exports, req, {
        level: 'error',
        event: 'deleteHeaderImage',
        message: `failed to remove header image: ${error.message}`,
        data: { error },
      });
      return res.status(error.statusCode || 500).json({
        success: false,
        message: error.message,
      });
    }
  }
}

module.exports = {
  logId: 'ctl:user',
  index: 'user',
  className: 'UserController',
  create: async (dtp) => { return new UserController(dtp); },
};