/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 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/>.*/

//You could make an argument for making this part of the userModel
//However, this is so rarely used the preformance benefits aren't worth the extra clutter

//Config
const config = require('../../../config.json');

//Node Imports
const crypto = require("node:crypto");

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

//Local Imports
const hashUtil = require('../../utils/hashUtils');
const loggerUtils = require('../../utils/loggerUtils');

/**
 * Password reset token retention time
 * 
 * Lasts about half a year
 */
const daysToExpire = 182;

/**
 * DB Schema for documents containing a single expiring password reset token
 */
const rememberMeToken = new mongoose.Schema({
    id: {
        type: mongoose.SchemaTypes.UUID,
        required: true,
        default: crypto.randomUUID()
    },
    user: {
        type: mongoose.SchemaTypes.ObjectID,
        ref: "user",
        required: true
    },
    token: {
        type: mongoose.SchemaTypes.String,
        required: true
    },
    date: {
        type: mongoose.SchemaTypes.Date,
        required: true,
        default: new Date()
    }
});

/**
 * Pre-Save function for rememberMeSchema
 */
rememberMeToken.pre('save', async function (next){
    //Ensure tokens ALWAYS get a new UUID and creation date
    this.id = crypto.randomUUID();
    this.date = new Date();

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

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

//statics
rememberMeToken.statics.genToken = async function(userDB, pass){
    //Normally I'd use userModel auth, but this saves on DB calls and keeps us from having to refrence the userModel directly
    //Saving us from circular depedency hell
    //Plus this is only really getting called along-side other auth, theres already going to be an error message if this is wrong XP
    if(!await userDB.checkPass(pass)){
        return;
    }

    try{
        //Generate a cryptographically secure string of 32 bytes in hexidecimal
        const token = crypto.randomBytes(32).toString('hex');

        //Create token document off of user and token string
        const tokenDB = await this.create({user: userDB._id, token});

        //Return token document UUID w/ plaintext token for browser consumption
        return {
            id: tokenDB.id,
            token
        };
    //If we failed for a non-login reason 
    }catch(err){
        return loggerUtils.localExceptionHandler(err);
    }
}

/**
 * Authenticates an id and token pair
 * @param {String} id - id of token auth against
 * @param {String} token - token string to auth against
 * @param {String} failLine - Line to paste into custom error upon login failure
 * @returns {Mongoose.Document} - User DB Document upon success
 */
rememberMeToken.statics.authenticate = async function(id, token, failLine = "Bad Username or Password."){
    //check for missing pass
    if(!id || !token){
        throw loggerUtils.exceptionSmith("Missing id/token.", "validation");
    }

    //get the token if it exists
    const tokenDB = await this.findOne({id});

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

    //Populate the user field
    await tokenDB.populate('user');

    //Check our password is correct
    if(await tokenDB.checkToken(token)){
        //Return the user doc
        return tokenDB.user;
    }else{
        loggerUtils.dumpSecurityLog(`Failed attempt at ${tokenDB.user.user}'s Remember-Me token {${tokenDB.id}}... Nuking token!`);
        //Nuke the token for security
        await tokenDB.deleteOne();
        //if not scream and shout
        badLogin();
    }

    //standardize bad login response so it's unknown which is bad for security reasons.
    function badLogin(){
        throw loggerUtils.exceptionSmith(failLine, "unauthorized");
    }
}

/**
 * Schedulable function for processing expired remember me tokens
 */
rememberMeToken.statics.processExpiredTokens = async function(){
    //Pull all tokens from the DB
    //Tested finding request by date, but mongoose kept throwing casting errors.
    //This seems to be an intermittent issue online. Maybe it will work in a future version?
    const tokenDB = await this.find({});

    //Fire em all off at once without waiting for the last one to complete since we don't fuckin' need to
    for(let tokenIndex in tokenDB){
        //pull token from tokenDB by index
        const token = tokenDB[tokenIndex];

        //If the token hasn't been processed and it's been expired
        if(token.getDaysUntilExpiration() <= 0){
            //Delete the token
            await token.deleteOne();
        }
    }
}

//Methods
/**
 * Intakes a plaintext token string and compares it to the hashed remember me token from the database
 * @param {String} token - Plaintext token retrieved from browser cookie
 * @returns {Boolean} Comparison result
 */
rememberMeToken.methods.checkToken = async function(token){
    //Compare ingested token to saved hash
    return await hashUtil.compareRememberMeToken(token, this.token);
}

/**
 * Returns number of days until token expiration
 * @returns {Number} Number of days until token expiration
 */
rememberMeToken.methods.getDaysUntilExpiration = function(){
        //Get request date
        const expirationDate = new Date(this.date);
        //Get expiration days and calculate expiration date
        expirationDate.setDate(expirationDate.getDate() + daysToExpire);
        //Calculate and return days until request expiration
        return ((expirationDate - new Date()) / (1000 * 60 * 60 * 24)).toFixed(1);
}

module.exports = mongoose.model("rememberMe", rememberMeToken);