import md5 from 'md5';
import passport from 'passport';
import { v4 as uuidv4 } from 'uuid';

import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as FacebookStrategy } from 'passport-facebook';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import Utils from '../Utils.js';

const collection = 'users';
const emailRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;


class UserManager {
    name = 'user';
    /**
     * Class initializer, инициализация на плъгин
     * @param {App} app Class initializer, основна апликация
     */
    init(app) {
        const { db, config, am, global, utils, custom } = app;

        this.passport = passport;

        passport.serializeUser(function (user, done) {
            done(null, user);
        });

        passport.deserializeUser(function (user, done) {
            done(null, user);
        });

        passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' },
            async function (email, password, done) {
                let user = await db.get(collection, { email: { $regex: `^${Utils.escapeRegExp(email)}$`, $options: 'i' } });
                if (user) {
                    if (md5(md5(password) + config.am.salt) == user.password) {
                        return done(null, am.getUserProfile(user));
                    } else {
                        await Utils.wait(3000);
                        return done(null, false, { message: 'invalidPassword' });
                    }
                } else {
                    await Utils.wait(3000);
                    return done(null, false, { message: 'invalidUsername' });
                }
            }
        ));

        passport.use(new FacebookStrategy({
            clientID: config.am.sso.FACEBOOK_APP_ID,
            clientSecret: config.am.sso.FACEBOOK_APP_SECRET,
            callbackURL: config.site.host + "/api/user/auth/facebook/callback",
            profileFields: ['id', 'emails', 'name', 'photos']
        }, am.processSocialLogin));

        passport.use(new GoogleStrategy({
            clientID: config.am.sso.GOOGLE_CLIENT_ID,
            clientSecret: config.am.sso.GOOGLE_CLIENT_SECRET,
            callbackURL: config.site.host + "/api/user/auth/google/callback",
            userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
        }, am.processSocialLogin));

        this.update = async function (ctx, data) {
            if (!am.is(ctx.user, 'admin') && ctx.user._id != data._id) {
                throw new Error('unauthorized');
            }
            let newData = data;
            let user = await db.get(collection, { '_id': db.ObjectId(newData._id) });

            if (newData.password) {
                if (newData.password != newData.passConfirm) {
                    throw new Error('passwordMismatch')
                }
                if (!am.is(ctx.user, 'admin') && md5(md5(newData.passCurrent) + config.am.salt) != user.password) {
                    throw new Error('invalidPassword')
                }
                user.password = md5(md5(newData.password) + config.am.salt);
            }

            let hist = Object.assign({}, user);
            await am.addToHistory(hist, collection, 'update');

            user._meta = user._meta || {};
            am.setMeta(user._meta, 'update', ctx.user);
            let safeData = {};

            this.assignSafeUserData(ctx, safeData, newData, am.is(ctx.user, 'admin'));
            this.assignSafeUserData(ctx, user, newData, am.is(ctx.user, 'admin'));

            await this.updateUserSession(ctx, user._id, safeData);

            am.audit(ctx, 'userUpdate', user._id);

            await db.update(collection, { '_id': db.ObjectId(user._id) }, user);
        }

        this.addGameData = async function(ctx, data){
            if (ctx.user?._id){
                let user = await db.get(collection, { '_id': db.ObjectId(ctx.user._id) });
                let gameData = (user.gameData || []).filter(gd=>gd.id != data.id);
                gameData.push(data);
                ctx.user.gameData = user.gameData = gameData;
                await db.update(collection, { '_id': db.ObjectId(user._id) }, user);
                //ctx.user.gameData = gameData;
            }else{
                let gameData = (ctx.session.gameData || []).filter(gd=>gd.id != data.id);
                gameData.push(data);
                ctx.session.gameData = gameData
            }
        }

        this.assignSafeUserData = function (ctx, userObject, newData, isAdmin) {
            ['displayName', 'firstName', 'lastName', ...(isAdmin ? ['roles', 'groups', 'email', 'status'] : [])].forEach(e => {
                userObject[e] = newData[e];
            });
        }

        this.updateUserSession = async function (ctx, _uid, safeData) {
            if (ctx.user?._id == _uid.toString()) {
                Object.assign(ctx.session.passport.user, safeData);
            }
            if (am.is(ctx.user, 'admin') || ctx.allUserSessions) {
                let userSessions = await db.list('user_sessions', {
                    query: {
                        'session.passport.user._id': _uid
                    }
                });
                for (let us of (userSessions?.data || [])) {
                    Object.assign(us.session.passport.user, safeData);
                    await db.update('user_sessions', { _id: us._id }, us);
                }
            }
        }

        this.delete = async function (ctx, uid) {
            let user = await db.get(collection, { '_id': db.ObjectId(uid) });
            let hist = Object.assign({}, user);
            await am.addToHistory(hist, collection, 'remove');
            am.audit(ctx, 'userDelete', user._id);
            await db.remove(collection, { '_id': db.ObjectId(user._id) });
        }

        this.signUp = async function (ctx, data) {
            return new Promise(async (resolve, reject) => {
                if (!emailRegexp.test(data.email)) {
                    return reject(new Error('invalidEmail'));
                }
                let exists = await db.get(collection, { email: { $regex: `^${Utils.escapeRegExp(data.email)}$`, $options: 'i' } });
                if (exists) {
                    return reject(new Error('emailExists'))
                }
                if (!data.password || data.password != data.passConfirm) {
                    return reject(new Error('passwordMismatch'))
                }
                if (!ctx.session.captcha || ctx.session.captcha?.toLowerCase() != data.captcha?.toLowerCase()) {
                    return reject(new Error('invalidCaptcha'))
                }
                delete ctx.session.captcha;
                let dbUser = {
                    email: data.email,
                    password: data.password && md5(md5(data.password) + config.am.salt),
                    roles: ['user'],
                    status: 0 //0 - neutral
                };
                if (config.am.validateEmail) {
                    dbUser.status = 9; // 9 - needs validation
                    dbUser.mailValidation = {
                        key: md5(md5(uuidv4() + data.email))
                    }
                    this.sendValidationEmail(ctx, dbUser).catch(error => {
                        am.audit(ctx, 'errorSendingMail', null, {
                            email: data.email,
                            error
                        });
                    });
                }
                let r = await db.create(collection, dbUser);
                dbUser._id = r.insertedId;
                am.audit(ctx, 'signup', dbUser._id);

                ctx.login(am.getUserProfile(dbUser), (err) => {
                    if (err) {
                        console.error(err);
                        reject(new Error('unknown'));
                    } else {
                        resolve(ctx.user);
                    }
                })
            })
        }

        this.forgotten = async function (ctx, data) {
            if (!ctx.session.captcha || ctx.session.captcha?.toLowerCase() != data.captcha?.toLowerCase()) {
                throw new Error('invalidCaptcha')
            }
            delete ctx.session.captcha;
            let exists = await db.get(collection, { email: data.email });
            if (!exists) {
                throw new Error('invalidEmail')
            }
            exists.pwdReset = {
                key: md5(md5(uuidv4() + data.email)),
                exp: (Date.now() / 1000) + 60 * 60 * 24
            };
            am.audit(ctx, 'userPwdForgotten', exists._id);
            let l = ctx.lang.t;

            await utils.sendMail({
                from: `${config.apis.mailer.defaultMailFrom}`,
                to: exists.email,
                subject: `${l.siteName} - ${l['reset-password']}`,
                html: l.forgottenPassMailContent(`${config.site.host}/${l._code}/user/change-password?key=${exists.pwdReset.key}`)
            })

            await db.update(collection, { '_id': db.ObjectId(exists._id) }, exists);
        }

        this.reset = async function (ctx, data) {
            let exists = await db.get(collection, { 'pwdReset.key': data.key });
            if (!exists) {
                throw new Error('invalidActivationLink')
            }
            if (exists.pwdReset.exp < Date.now / 1000) {
                throw new Error('activationLinkExpired')
            }
            if (!data.password || data.password != data.passConfirm) {
                throw new Error('passwordMismatch')
            }

            exists.password = data.password && md5(md5(data.password) + config.am.salt);
            delete exists.pwdReset;
            await db.update(collection, { '_id': db.ObjectId(exists._id) }, exists);
            am.audit(ctx, 'userPwdReset', exists._id);
        }

        this.sendValidationEmail = async function (ctx, data) {
            let l = ctx.lang.t;
            await utils.sendMail({
                from: `${config.apis.mailer.defaultMailFrom}`,
                to: data.email,
                subject: `${l.siteName} - ${l['validate-email']}`,
                html: l.validationMailContent(`${config.site.host}/${l._code}/user/validate-email?key=${data.mailValidation.key}`)
            });
        }

        this.validateEmail = async function (ctx, data) {
            let exists = await db.get(collection, { 'mailValidation.key': data.key });
            if (!exists) {
                throw new Error('invalidValidationLink')
            }
            exists.status = 10;
            delete exists.mailValidation;
            custom.emailVerified?.(exists);
            let safeData = {};
            this.assignSafeUserData(ctx, safeData, exists, true);
            let uid = exists._id;
            await this.updateUserSession({ allUserSessions: true }, uid, safeData);
            //db.update should be last, because it deletes exists._id, but I need it above!
            await db.update(collection, { '_id': db.ObjectId(uid) }, exists);
            am.audit(ctx, 'userMailValidated', uid);
        }
    }
}

export { UserManager }