/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024  Rainbownapkin and the TTN Community

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

//NPM Imports
const {mongoose} = require('mongoose');

//local imports
const server = require('../server');
const statModel = require('./statSchema');
const flairModel = require('./flairSchema');
const permissionModel = require('./permissionSchema');
const hashUtil = require('../utils/hashUtils');


const userSchema = new mongoose.Schema({
    id: {
        type: mongoose.SchemaTypes.Number,
        required: true
    },
    user: {
        type: mongoose.SchemaTypes.String,
        maxLength: 22,
        required: true,
    },
    pass: {
        type: mongoose.SchemaTypes.String,
        required: true
    },
    email: {
        type: mongoose.SchemaTypes.String
    },
    date: {
        type: mongoose.SchemaTypes.Date,
        required: true,
        default: new Date()
    },
    rank: {
        type: mongoose.SchemaTypes.String,
        required: true,
        enum: permissionModel.rankEnum,
        default: "user"
    },
    tokes: {
        type: mongoose.SchemaTypes.Map,
        required: true,
        default: new Map()
    },
    img: {
        type: mongoose.SchemaTypes.String,
        required: true,
        default: "/img/johnny.png"
    },
    bio: {
        type: mongoose.SchemaTypes.String,
        required: true,
        maxLength: 1000,
        default: "Bio not set!"
    },
    pronouns:{
        type: mongoose.SchemaTypes.String,
        optional: true,
        maxLength: 20,
        default: ""
    },
    signature: {
        type: mongoose.SchemaTypes.String,
        required: true,
        maxLength: 150,
        default: "Signature not set!"
    },
    highLevel: {
        type: mongoose.SchemaTypes.Number,
        required: true,
        min: 0,
        max: 10,
        default: 0
    },
    flair: {
        type: mongoose.SchemaTypes.ObjectID,
        default: null,
        ref: "flair"
    }
});

//This is one of those places where you really DON'T want to use an arrow function over an anonymous one!
userSchema.pre('save', async function (next){

    //If the password was changed
    if(this.isModified("pass")){
        //Hash that sunnovabitch, no questions asked.
        this.pass = hashUtil.hashPassword(this.pass);
    }

    //If the flair was changed
    if(this.isModified("flair")){
        //Get flair properties 
        await this.populate('flair');

        if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(this.flair.rank)){
            throw new Error(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`);
        }
    }

    //Ensure we don't have empty flair
    if(this.flair == null){
        const flairDB = await flairModel.findOne({});
        this.flair = flairDB._id;
    }

    if(this.isModified("rank")){
        await this.killAllSessions("Your site-wide rank has changed. Sign-in required.");
    }

    //All is good, continue on saving.
    next();
});

//statics
userSchema.statics.register = async function(userObj){
    const {user, pass, passConfirm, email} = userObj;

    if(pass == passConfirm){
        const userDB = await this.findOne({$or: email ? [{user}, {email}] : [{user}]});

        //If the user is found or someones trying to impersonate tokeboi
        if(userDB || user.toLowerCase() == "tokebot"){
            throw new Error("User name/email already taken!");
        }else{
            const id = await statModel.incrementUserCount();
            const newUser = await this.create({id, user, pass, email});
        }
    }else{
        throw new Error("Confirmation password doesn't match!");
    }
}

userSchema.statics.authenticate = async function(user, pass){
    //check for missing pass
    if(!user || !pass){
        throw new Error("Missing user/pass.");
    }

    //get the user if it exists
    const userDB = await this.findOne({ user });

    //if not scream and shout
    if(!userDB){
        badLogin();
    }

    //Check our password is correct
    if(userDB.checkPass(pass)){
        return userDB;
    }else{
        //if not scream and shout
        badLogin();
    }

    //standardize bad login response so it's unknowin which is bad for security reasons.
    function badLogin(){
        throw new Error("Bad Username or Password.");
    }
}

userSchema.statics.findProfile = async function(user){
    //If someone's looking for tokebot
    if(user.user.toLowerCase() == "tokebot"){
        //fake a profile hashtable for tokebot
        profile = {
            id: -420,
            user: "Tokebot",
            date: (await statModel.getStats()).firstLaunch,
            tokes: await statModel.getTokeCommandCounts(),
            tokeCount: await statModel.getTokeCount(),
            img: "/img/johnny.png",
            signature: "!TOKE",
            bio: "!TOKE OR DIE!"
        };

        //return the faked profile
        return profile;
    }else{
        //find user
        const userDB = await this.findOne({user: user.user});
        
        //If we don't find a user just return a null profile
        if(userDB == null){
            return null
        }

        //return the profile
        return userDB.getProfile();
    }

}

userSchema.statics.tattooToke = function(tokemap){
    //For each toke, asynchronously:
    tokemap.forEach(async (toke, user) => {
        //get user
        const userDB = await this.findOne({user});

        //Check that the user exists (might come in handy for future treez.one integration?)
        if(userDB != null){
            var tokeCount = userDB.tokes.get(toke);

            //if this is the first time using this toke command
            if(tokeCount == null){
                //set toke count to one
                tokeCount = 1;
            //otherwise
            }else{
                //increment tokecount
                tokeCount++;
            }

            //Set the toke count for the specific command
            userDB.tokes.set(toke, tokeCount);

            //Save the user doc to the database
            await userDB.save();
        }
    });
}

//methods
userSchema.methods.checkPass = function(pass){
    return hashUtil.comparePassword(pass, this.pass);
}

userSchema.methods.getAuthenticatedSessions = async function(){
    var returnArr = [];

    //retrieve active sessions (they really need to implement this shit async already)
    return new Promise((resolve) => {
        server.store.all((err, sessions) => {
            //You guys ever hear of a 'not my' problem? Fucking y33tskies lmao, better use a try/catch
            if(err){
                throw err;

            }

            //crawl through active sessions
            sessions.forEach((session) => {
                //if a session matches the current user
                if(session.user.id == this.id){
                    //we return it
                    returnArr.push(session);
                }
            });


            resolve(returnArr);

        });
    });

}

userSchema.statics.getUserList = async function(fullList = false){
    var userList = [];
    //Get all of our users
    const users = await this.find({});

    //Return empty if we don't find nuthin'
    if(users == null){
        return [];
    }

    //For each user
    users.forEach((user)=>{
        //create a user object with limited properties (safe for public consumption)
        var userObj = {
            id: user.id,
            user: user.user,
            img: user.img,
            date: user.date
        }

        //Put together a spicier version for admins when told so (permission checks should happen before this is called)
        if(fullList){
            userObj.rank = user.rank,
            userObj.email = user.email
        }

        //Add user object to list
        userList.push(userObj);
    });
    
    //return the userlist
    return userList;
}

//methods
userSchema.methods.getProfile = function(){
    //Create profile hashtable
    const profile = {
        id: this.id,
        user: this.user,
        date: this.date,
        tokes: this.tokes,
        tokeCount: this.getTokeCount(),
        img: this.img,
        signature: this.signature,
        bio: this.bio
    };
    
    //return profile hashtable
    return profile;
}

userSchema.methods.setFlair = async function(flair){
    //Find flair by name
    const flairDB = await flairModel.findOne({name: flair});
    //Set the users flair ref to the found flairs _id
    this.flair = flairDB._id;
    //Save the user
    await this.save();
    //return the found flair
    return flairDB;
}

userSchema.methods.getTokeCount = function(){
    //Set tokeCount to 0
    var tokeCount = 0;

    //For each toke command the user has used
    this.tokes.forEach((commandCount) => {
        //Add the count for that specific command to the total
        tokeCount += commandCount;
    });

    //Return the amount of tokes recorded
    return tokeCount;
}

//note: if you gotta call this from a request authenticated by it's user, make sure to kill that session first!
userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){
    //get authenticated sessions
    var sessions = await this.getAuthenticatedSessions();

    //crawl through and kill all sessions
    sessions.forEach((session) => {
        server.store.destroy(session.seshid);
    });

    //Tell the application side of the house to kick the user out as well
    server.channelManager.kickConnections(this.user, reason);
}

userSchema.methods.passwordReset = async function(passChange){
    if(this.checkPass(passChange.oldPass)){
        if(passChange.newPass == passChange.confirmPass){
            //Note: We don't have to worry about hashing here because the schema is written to do it auto-magically
            this.pass = passChange.newPass;
            
            //Save our password
            await this.save();

            //Kill all authed sessions for security purposes
            await this.killAllSessions("Your password has been reset.");
        }else{
            //confirmation pass doesn't match
            throw new Error("Mismatched confirmation password!");
        }
    }else{
        //Old password wrong
        throw new Error("Incorrect Password!");
    }

}

userSchema.methods.nuke = async function(pass){
    if(pass == "" || pass == null){
        throw new Error("No confirmation password!");
    }

    if(this.checkPass(pass)){
        //Annoyingly there isnt a good way to do this from 'this'
        var oldUser = await this.deleteOne();
        
        if(oldUser){
            await this.killAllSessions("If you're seeing this, your account has been deleted. So long, and thanks for all the fish! <3");
        }else{
            throw new Error("Server Error: Unable to delete account! Please report this error to your server administrator, and with timestamp.");
        }
    }else{
        throw new Error("Bad pass.");
    }
}

module.exports.userModel = mongoose.model("user", userSchema);