const express = require('express');
const router = express.Router();
const config = require('config');
const fs = require('fs').promises;

const ModelPath = '../../models/';
const User = require(ModelPath + 'User.js');
const Invite = require(ModelPath + 'Invite.js');

const passport = require('passport');

const canonicalizeRequest = require('../../util/canonicalize').canonicalizeRequest;
const requireAuth = require('../../util/auth').requireAuth;
const wrap = require('../../util/wrap.js');
const bodyVerifier = require('../../util/verifyBody').bodyVerifier;
const rateLimit = require('express-rate-limit');

// Wraps passport.authenticate to return a promise
const authenticate = (req, res, next) => {
    return new Promise((resolve) => {
        passport.authenticate('local', (err, user) => {
            resolve(user);
        })(req, res, next);
    });
};

// Wraps passport session creation for async usage
const login = (user, req) => {
    return new Promise((resolve) => {
        req.login(user, resolve);
    });
};

// Query the database for a valid invite code. An error message property is set if invalid.
const validateInvite = wrap(async (req, res, next) => {
    const invite = await Invite.findOne({code: req.body.invite}).catch(next);

    if (!invite) {
        // Log failure
        await fs.appendFile('auth.log', `${new Date().toISOString()} register ${req.ip}\n`);
        return res.status(422).json({message: 'Invalid invite code.'});
    }

    if (invite.used)
        return res.status(422).json({message: 'Invite already used.'});

    if (invite.expires != null && invite.expires < Date.now())
        return res.status(422).json({message: 'Invite expired.'});

    req.invite = invite;
    next();
});

// Check if the requested username is valid
const validateUsername = wrap(async (req, res, next) => {
    const username = req.body.username;

    const count = await User.countDocuments({username: username}).catch(next);
    if (count !== 0)
        return res.status(422).json({message: 'Username in use.'});

    next();
});

const registerLimiter = config.get('RateLimit.enable')
    ? rateLimit({
        windowMs: config.get('RateLimit.register.window') * 1000,
        max: config.get('RateLimit.register.max'),
        skipSuccessfulRequests: true
    })
    : (req, res, next) => { next(); };
const registerProps = [
    {
        name: 'displayname',
        type: 'string',
        maxLength: config.get('User.Username.maxLength'),
        sanitize: true,
        restrict: new RegExp(config.get('User.Username.restrictedChars')),
    },
    {name: 'password', type: 'string'},
    {name: 'invite', type: 'string'}];
router.post('/register',
    registerLimiter,
    bodyVerifier(registerProps), canonicalizeRequest,
    validateInvite, validateUsername,
    wrap(async (req, res, next) => {
    // Update the database
    await Promise.all([
        User.register({
            username: req.body.username,
            displayname: req.body.displayname,
            scope: req.invite.scope,
            date: Date.now()
        }, req.body.password).catch(next),
        Invite.updateOne({code: req.invite.code}, {recipient: req.body.username, used: Date.now()}).catch(next)
    ]);

    res.status(200).json({'message': 'Registration successful.'});
}));

console.log(config.get('RateLimit'));
const loginLimiter = config.get('RateLimit.enable')
    ? rateLimit({
        windowMs: config.get('RateLimit.login.window') * 1000,
        max: config.get('RateLimit.login.max'),
        skipSuccessfulRequests: true
    })
    : (req, res, next) => { next(); };
const loginProps = [
    {name: 'username', type: 'string', optional: true},
    {name: 'displayname', type: 'string', optional: true},
    {name: 'password', type: 'string'}];
router.post('/login',
    loginLimiter,
    bodyVerifier(loginProps),
    canonicalizeRequest,
    wrap(async (req, res, next) => {
    // Authenticate
    const user = await authenticate(req, res, next);
    if (!user) {
        // Log failure
        await fs.appendFile('auth.log', `${new Date().toISOString()} login ${req.ip}\n`);
        return res.status(401).json({'message': 'Unauthorized.'});
    }

    // Create session
    await login(user, req);

    // Set session vars
    req.session.passport.displayname = user.displayname;
    req.session.passport.scope = user.scope;

    res.status(200).json({'message': 'Logged in.'});
}));

router.post('/logout', function (req, res) {
    if (!req.isAuthenticated())
        return res.status(400).json({message: 'Not logged in.'});

    req.logout();
    res.status(200).json({'message': 'Logged out.'});
});

router.get('/whoami', requireAuth(), (req, res) => {
    res.status(200).json({
        username: req.username,
        displayname: req.displayname,
        scope: req.scope,
        key: req.key
    });
});

module.exports = router;