'use strict';

const asyncp = require('paradox-asyncp');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const LocalAPIKeyStrategy = require('passport-localapikey').Strategy;
const session = require('express-session');
const _ = require('lodash');
const express = require('express');
const nunjucks = require('nunjucks');
const Promise = require('bluebird');
const EventEmitter = require('paradox-utils').EventEmitter;
const Dblize = require('paradox-dblize');
const Types = require('paradox-utils').SchemaTypes;
const uuidv4 = require('uuid/v4');
const SessionDataStore = require('./session-datastore');
const logger = require('paradox-logger')('login-manager');
const moment = require('moment');

const ExpressBrute = require('express-brute');

class LoginManager extends EventEmitter {

    constructor(cloud, options) {
        super();

        this.cloud = cloud;

        this.options = _.defaults(options, {
            prefix: '/app',
            initialize: true,
            loginUrl: '/login',
            logoutUrl: '/logout',
            successRedirect: '/admin/',
            redirect: false,
            users: [],
            tokens: [],
            store: 'nedb',
            loginEverywhere: true,
            anonymous: {
                id: 0,
                username: 'anonymous',
            },
            session: {
                secret: 'dd1a8513-bed6-40c3-a9e9-deed13212215',
                name: 'prdx-ssn',
                // store: new SessionStore(),
                proxy: true,
                resave: false,
                saveUninitialized: false,
                rolling: true,
            },
            bruteForce: {
                freeRetries: 5,
                minWait: 10 * 1000, // 10 seconds
                maxWait: 24 * 60 * 60 * 1000, // 24 hour
            },
            partition: null,
            models: {
                user: 'user',
                token: 'user-token',
                history: 'user-history',
                session: 'user-session',
            },
        });

        this.templates = new nunjucks.Environment(new nunjucks.FileSystemLoader(__dirname + '/pages'));
    };

    showLogin(req, res, options) {
        options = _.defaults(options, {
            enabled: true,
            type: 'login',
            message: null,
            code: null,
            httpCode: 200,
            redirect: null,
            loginUrl: this.options.loginUrl,
            logoutUrl: this.options.logoutUrl,
        });

        res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
        res.header('Expires', '-1');
        res.header('Pragma', 'no-cache');

        res.status(options.httpCode);

        // respond with html page
        if(req.accepts('html')) {
            let html = this.templates.render('login.twig', options);
            res.send(html);
            return;
        }

        // respond with json
        if(req.accepts('json')) {
            res.send({
                message: options.message,
                code: options.code,
            });
            return;
        }

        res.type('txt').send(options.code);
    }

    register(app) {
        this.registerModels();

        let sessionOpts = this.options.session;

        if(!sessionOpts.store) {
            let model = this.getSessionModel();

            sessionOpts.store = new SessionDataStore({
                model,
            });
        }

        app.use(session(sessionOpts));

        app.use(passport.initialize());
        app.use(passport.session());

        if(!this.options.enabled) {
            passport.serializeUser((user, done) => {
                done(null, 1);
            });

            passport.deserializeUser((id, done) => {
                done(null, this.options.anonymous);
            });

            app.use((req, res, next) => {
                req.login(this.options.anonymous, (err) => {
                    return next(err);
                });
            });

            return;
        }

        /**
         * Server password
         */

        passport.serializeUser((user, done) => {
            if(this.options.store == 'event') {
                return this.emit('serialize', [user])
                    .catch(err => {
                        done(err);
                    })
                    .then(id => {
                        done(null, id);
                    });

            } else if(this.options.store == 'nedb' || this.options.store == 'datastore') {
                done(null, user.key);

            } else {
                done(new Error('unknown store option'));
            }
        });

        passport.deserializeUser((id, done) => {
            if(this.options.store == 'event') {
                return this.emit('deserialize', [id])
                    .catch(err => {
                        done(err);
                    })
                    .then(user => {
                        done(null, user);
                    });

            } else if(this.options.store == 'nedb' || this.options.store == 'datastore') {
                return this
                    .getUserModel()
                    .then(User => {
                        return User.findById(id);
                    })
                    .catch(err => {
                        if(err.code == 404) {
                            return null;
                        }

                        return Promise.reject(err);
                    })
                    .then(user => {
                        if(!user) {
                            done(null, false);

                        } else {
                            done(null, user.getValues());
                        }
                    });

            } else {
                done(new Error('unknown store option'));
            }
        });

        passport.use(new LocalAPIKeyStrategy(
            (apikey, done) => {
                if(this.options.store == 'event') {
                    return this.emit('login-apikey', [username, password])
                        .catch(err => {
                            done(err);
                        })
                        .then(user => {
                            done(null, user || false);
                        });

                } else if(this.options.store == 'nedb' || this.options.store == 'datastore') {
                    return this
                        .getTokenModel()
                        .then(Token => {
                            return Token
                                .findOne({
                                    where: {
                                        token: apikey,
                                    }
                                });
                        })
                        .catch(err => {
                            if(err.code == 404) {
                                return null;
                            }

                            return Promise.reject(err);
                        })
                        .then(token => {
                            if(!token) {
                                done(null, false);

                            } else {
                                if(!token.enabled) {
                                    done(new Error('apikey token disabled')); // disabled

                                } else {
                                    return this
                                        .getUserModel()
                                        .then(User => {

                                            return User
                                                .findOne({
                                                    key: token.user_key
                                                })
                                                .catch(err => {
                                                    if(err.code == 404) {
                                                        return null;
                                                    }

                                                    return Promise.reject(err);
                                                })
                                                .then(user => {
                                                    if(!user) {
                                                        done(null, false);

                                                    } else {
                                                        done(null, user.getValues());
                                                    }
                                                });
                                        });

                                }
                            }
                        });

                } else {
                    done(new Error('unknown store option'));
                }
            }
        ));

        passport.use(new LocalStrategy(
            (username, password, done) => {
                if(this.options.store == 'event') {
                    return this.emit('login', [username, password])
                        .catch(err => {
                            done(err);
                        })
                        .then(user => {
                            done(null, user || false);
                        });

                } else if(this.options.store == 'nedb' || this.options.store == 'datastore') {
                    return this
                        .getUserModel()
                        .then(User => {
                            return User
                                .findOne({
                                    where: {
                                        username,
                                        password,
                                    }
                                });
                        })
                        .catch(err => {
                            if(err.code == 404) {
                                return null;
                            }

                            return Promise.reject(err);
                        })
                        .then(user => {
                            if(!user) {
                                done(null, false);

                            } else {
                                done(null, user.getValues());
                            }
                        });

                } else {
                    done(new Error('unknown store option'));
                }
            }
        ));

        app.use((req, res, next) => {
            if(req.isAuthenticated()) {
                return next();
            }

            if(req.query.apikey || req.body.apikey) {
                passport.authenticate('localapikey', {
                    session: false,
                    failureRedirect: '/api/unauthorized' // TODO: esto funciona... pero de potra

                })(req, res, next);

            } else {
                next();
            }
        });

        app.get(this.options.logoutUrl, (req, res, next) => {
            if(!req.isAuthenticated()) {
                next();
                return;
            }

            req.logout();
            res.redirect(this.options.loginUrl);
        });

        app.get(this.options.loginUrl, (req, res, next) => {
            if(req.isAuthenticated()) {
                this.showLogin(req, res, {
                    type: 'logout',
                    message: 'You are already logged as "' + req.user.name + '"',
                    code: 'LOGGED_IN',
                });

            } else {
                this.showLogin(req, res, {
                    code: 'LOGIN_REQUIRED',
                });
            }
        });

        let store = new ExpressBrute.MemoryStore();
        let bruteforce = new ExpressBrute(store, _.defaults(
            {},
            this.options.bruteForce,
            {
                failCallback: (req, res, next, nextValidRequest) => {
                    let nextRetry = moment(nextValidRequest);

                    moment.locale('en');

                    // No darle demasiados detalles al atacante de cuando volver a probar
                    let nextTry = nextRetry.fromNow();
                    let remain = nextRetry.diff(moment());

                    // let nextTry = Math.ceil(nextRetry.diff(moment())/1000);
                    //
                    // if(nextTry > 60) {
                    //     nextTry = nextRetry.fromNow();
                    //
                    // } else {
                    //     nextTry += ' seconds';
                    // }

                    this.showLogin(req, res, {
                        message: 'too many retries, watch the stars and retry again ' + nextTry,
                        code: 'TOO_MANY_RETRIES',
                        httpCode: 429,
                        redirect: req.body.redirect || null,
                        readonly: true,
                        tryAgainMs: remain
                    });
                }
            }
        ));

        app.post(this.options.loginUrl,
            bruteforce.prevent,
            (req, res, next) => {
                passport.authenticate('local', (err, user, info) => {
                    if(err) {
                        return next(err); // will generate a 500 error
                    }

                    if(!user) {
                        this.showLogin(req, res, {
                            message: 'invalid credentials',
                            code: 'INVALID_CREDENTIALS',
                            httpCode: 401,
                            redirect: req.body.redirect || null,
                            username: req.body.username,
                        });

                        return;
                    }

                    req.login(user, loginErr => {
                        if(loginErr) {
                            return next(loginErr);
                        }

                        req.brute.reset(() => {
                            res.status(200);

                            if(req.accepts('html')) {
                                res.redirect(req.body.redirect || this.options.successRedirect);
                                return;
                            }

                            if(req.accepts('json')) {
                                res.json({
                                    message: 'valid credentials',
                                    code: 'VALID_CREDENTIALS',
                                    user,
                                });
                                return;
                            }

                            res.send('logged');
                            return;
                        });
                    });

                })(req, res, next);
            });

        // esperar al final antes de pedir

        // app.use((req, res, next) => {
        //     if(this.options.express_password_regex && !req.url.match(this.options.express_password_regex)) {
        //         next();
        //         return;
        //     }
        //
        //     if(this.options.express_password_regex_exclude && req.url.match(this.options.express_password_regex_exclude)) {
        //         next();
        //         return;
        //     }
        //
        //     let user = basicAuth(req);
        //
        //     if(user === undefined || user['name'] !== this.options.express_user || user['pass'] !== this.options.express_password) {
        //         res.statusCode = 401;
        //         res.setHeader('WWW-Authenticate', 'Basic realm="Access"');
        //         res.end('Unauthorized');
        //
        //     } else {
        //         next();
        //     }
        // });

        if(this.options.initialize) {
            this.initialize()
                .catch(err => {
                    logger.error('error initialicing login users');
                    logger.exception(err);
                });
        }
    }

    initialize() {
        return this
            .getUserModel()
            .then(User => {
                return asyncp
                    .eachLimit(this.options.users, 1, user => {
                        return User
                            .findOrCreate({
                                where: {
                                    key: user.key,
                                },
                                defaults: user,
                            });
                    });
            })
            .then(() => {
                return this.getTokenModel();
            })
            .then(Token => {
                return asyncp
                    .eachLimit(this.options.tokens, 1, token => {
                        return Token
                            .findOrCreate({
                                where: {
                                    key: token.key,
                                },
                                defaults: token,
                            });
                    });
            });
    }

    registerApi(api) {
        return this
            .emit('api', [api])
            .then(result => {
                if(result === false) {
                    return;
                }

                return this
                    .getTokenModel()
                    .then(Token => {
                        api.registerEndpoint('/user', {
                            type: 'callback',
                            group: '/user',
                            priority: 100,
                            method: 'get',
                            description: 'Devuelve la información del usuario logueado.',
                            callback: (request) => {
                                return _.defaults({}, request.user, {
                                    loginUrl: this.options.loginUrl,
                                    logoutUrl: this.options.logoutUrl,
                                });
                            }
                        });

                        api.registerEndpoint('/user/history', {
                            type: 'callback',
                            group: '/user',
                            priority: 100,
                            method: 'get',
                            description: 'Devuelve la información del usuario logueado.',
                            callback: (request) => {
                                return this
                                    .getHistoryModel()
                                    .then(History => {
                                        return History.findAll({
                                            where: {
                                                user_key: request.user.key,
                                            }
                                        });
                                    });
                            }
                        });

                        api.registerEndpoint('/user/token', {
                            type: 'rest',
                            group: '/user',
                            priority: 100,
                            model: Token,
                        });
                    });
            });
    }

    registerEnd(app) {
        if(!this.options.enabled) {
            return;
        }

        if(this.options.loginEverywhere) {
            app.use((req, res, next) => {
                if(req.isAuthenticated()) {
                    return next();
                }

                if(req.method !== 'GET') {
                    return next();
                }

                // sino está autorizado, 404
                if(req.url.match(/\.[a-z0-9]{2,3}$/)) {
                    return next();
                }

                this.showLogin(req, res, {
                    code: 'LOGIN_REQUIRED',
                    httpCode: 401,
                    redirect: 'auto',
                });
            });
        }
    }

    /* -------------------------------------------------------------------------
     * Models
     * ---------------------------------------------------------------------- */

    registerModels() {
        this.cloud(this.options.store).registerPath(__dirname + '/models');
        return this;
    }

    getUserModel() {
        return this.requireModelString(this.options.models.user);
    }

    getTokenModel() {
        return this.requireModelString(this.options.models.token);
    }

    getSessionModel() {
        return this.requireModelString(this.options.models.session);
    }

    getHistoryModel() {
        return this.requireModelString(this.options.models.history);
    }

    requireModelString(model) {
        return this.cloud(this.options.store).model(model)
            .then(Model => {
                if(this.options.partition) {
                    return Model.partition(this.options.partition);
                }

                return Model;
            });
    }
}

module.exports = LoginManager;