/*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/>.*/

//Node
const fs = require('node:fs/promises');
const crypto = require('node:crypto');

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

/**
 * Creates and returns a custom exception, tagged as a 'custom' exception, using the 'custom' boolean property.
 * This is used to denote that this error was generated on purpose, with a human readable message, that can be securely sent to the client.
 * Unexpected exceptions should only be logged internally, however, as they may contain sensitive data.
 * 
 * @param {String} msg - Error message to send the client
 * @param {String} type - Error type to send back to the client
 * @returns {Error} The exception to smith
 */
module.exports.exceptionSmith = function(msg, type){
        //Create the new error with the given message
        const exception = new Error(msg);

        //Set the error type
        exception.type = type;
        
        //Mark the error as a custom error
        exception.custom = true;

        //Return the error
        return exception;
}

/**
 * Main error handling function
 * @param {Express.Response} res - Response being sent out to the client who caused the issue
 * @param {String} msg - Error message to send the client
 * @param {String} type - Error type to send back to the client
 * @param {Number} status - HTTP(s) Status Code to send back to the client
 * @returns {Express.Response} If we have a usable Express Response object, return it back after it's been cashed
 */
module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){
        //Some controllers do things after sending headers, for those, we should remain silent
        if(!res.headersSent){
                res.status(status);
                return res.send({errors: [{type, msg, date: new Date()}]});
        }
}

/**
 * Handles local exceptions which where not directly created by user interaction
 * @param {Error} err - Exception to handle
 */
module.exports.localExceptionHandler = function(err){
        //If we're being verbose and this isn't just a basic bitch
        if(!err.custom && config.verbose){
                //Log the error
                module.exports.dumpError(err);
        }
}

/**
 * Handles exceptions which where directly the fault of user action >:(
 * @param {Express.Response} res - Express Response object to bitch at
 * @param {Error} err - Error created by the jerk in question
 */
module.exports.exceptionHandler = function(res, err){
        //If this is a self-made problem
        if(err.custom){
                module.exports.errorHandler(res, err.message, err.type);
        }else{
                //Locally handle the exception
                module.exports.localExceptionHandler(err);

                //if not yell at the browser for fucking up, and tell it what it did wrong.
                module.exports.errorHandler(res, "An unexpected server crash was just prevented. You should probably report this to an admin.", "Caught Exception");
        }
}

/**
 * Basic error-handling for socket.io so we don't just silently swallow errors.
 * @param {Socket} socket - Socket error originated from
 * @param {String} msg - Error message to send the client
 * @param {String} type - Error type to send back to the client
 * @returns {Boolean} - Passthrough from socket.emit
 */
module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){
        return socket.emit("error", {errors: [{type, msg, date: new Date()}]});
}

/**
 * Generates error messages for simple errors generated by socket.io interaction
 * @param {Socket} socket - Socket error originated from
 * @param {Error} err - Error created by the jerk in question
 * @returns {Boolean} - Passthrough from socket.emit
 */
module.exports.socketExceptionHandler = function(socket, err){
        //If this is a self made problem
        if(err.custom){
                //yell at the browser for fucking up, and tell it what it did wrong.
                return module.exports.socketErrorHandler(socket, err.message, err.type);
        }else{
                //Locally handle the exception
                module.exports.localExceptionHandler(err);

                //if not yell at the browser for fucking up
                return module.exports.socketErrorHandler(socket, "An unexpected server crash was just prevented. You should probably report this to an admin.", "Server");
        }
}

/**
 * Generates error messages and drops connection for critical errors caused by socket.io interaction
 * @param {Socket} socket - Socket error originated from
 * @param {Error} err - Error created by the jerk in question
 * @returns {Boolean} - Passthrough from socket.disconnect
 */
module.exports.socketCriticalExceptionHandler = function(socket, err){
        //If this is a self made problem
        if(err.custom){
                //yell at the browser for fucking up, and tell it what it did wrong.
                socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`});
        }else{
                //Locally handle the exception
                module.exports.localExceptionHandler(err);

                //yell at the browser for fucking up
                socket.emit("kick", {type: "Disconnected", reason: "An unexpected server crash was just prevented. You should probably report this to an admin."});
        }
        return socket.disconnect();
}

/**
 * Prints warning text to server console
 * @param {String} string - String to print to console
 */
module.exports.consoleWarn = function(string){
        console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string);
}

/**
 * Basic error-handling middleware to ensure we're not dumping stack traces to the client, as that would be insecure
 * @param {Error} err - Error to handle
 * @param {Express.Request} req - Express Request
 * @param {Express.Response} res - Express Response
 * @param {Function} next - Next function in the Express middleware chain (Not that it's getting called XP)
 */
module.exports.errorMiddleware = function(err, req, res, next){
        //Set generic error
        var reason = "Server Error";

        //If it's un-authorized
        if(err.status == 403){
                reason = "Unauthorized"
        }

        module.exports.errorHandler(res, err.message, reason, err.status);
}

/**
 * Dumps unexpected server crashes to dedicated log files
 * @param {Error} err - error to dump to file
 * @param {Date} date - Date of error, defaults to now
 * @param {String} subDir - subdirectory inside the log folder we want to dump to
 * @param {Boolean} muzzle - Tells the function to STFU
 */
module.exports.dumpError = async function(err, date = new Date(), subDir = 'crash/', muzzle = false){
        //Generate content from error
        const content = `Error Date: ${date.toLocaleString()} (UTC-${date.getTimezoneOffset()/60})\nError Type: ${err.name}\nError Msg:${err.message}\nStack Trace:\n\n${err.stack}`;

        //Dump text to file
        module.exports.dumpLog(content, date.getTime(), subDir, muzzle);
}


module.exports.dumpSecurityLog = async function(content, date = new Date()){
        module.exports.dumpLog(content, `Incident-{${crypto.randomUUID()}}-${date.getTime()}`, 'security/', true);
}

/**
 * Dumps log file to log folder
 * @param {String} content - Text to dump to file
 * @param {String} name - file name to save to
 * @param {String} subDir - subdirectory inside the log folder we want to dump to
 * @param {Boolean} muzzle - Tells the function to STFU
 */
module.exports.dumpLog = async function(content, name, subDir = '/', muzzle = false){
        try{
                //Crash directory
                const dir = `./log/${subDir}`

                //Double check crash folder exists
                try{
                        await fs.stat(dir);
                //If we caught an error (most likely it's missing)
                }catch(err){
                        if(!muzzle){
                                //Shout about it 
                                module.exports.consoleWarn("Log folder missing, mking dir!")
                        }
                        
                        //Make it if doesn't
                        await fs.mkdir(dir, {recursive: true});
                }

                //Assemble log file path
                const path = `${dir}${name}.log`;

                //Write content to file
                fs.writeFile(path, content);

                if(!muzzle){
                        //Whine about the error
                        module.exports.consoleWarn(`Warning: Unexpected Server Crash gracefully dumped to '${path}'... SOMETHING MAY BE VERY BROKEN!!!!`);
                }
        //If somethine went really really wrong
        }catch(doubleErr){
                if(!muzzle){
                        //Use humor to cope with the pain
                        module.exports.consoleWarn("Yo Dawg, I herd you like errors, so I put an error in your error dump, so you can dump while you dump:");
                        //Dump the original error to console
                        module.exports.consoleWarn(err);
                        //Dump the error we had saving that error to file to console
                        module.exports.consoleWarn(doubleErr);
                }
        }
}

module.exports.welcomeWagon = function(count, date, tokes){
        //Inject values into ascii art
        const art = `
\x1b[32m        !        \x1b[0m
\x1b[32m       420       \x1b[0m   \x1b[32m\x1b[40m${config.instanceName}\x1b[0m\x1b[2m, Powered By:\x1b[0m
\x1b[32m       420       \x1b[0m 
\x1b[32m      WEEED      \x1b[0m    CCCC   AAA   NN  N   OOO   PPPP   Y   Y  
\x1b[32m!     WEEED     !\x1b[0m   C      A   A  NN  N  O   O  P   P  Y   Y 
\x1b[32mWEE   EEEEE   EED\x1b[0m   C      A   A  N N N  O   O  P   P  Y   Y 
\x1b[32m WEE  EEEEE  EED\x1b[0m    C      AAAAA  N N N  O   O  PPPP     Y
\x1b[32m  WEE  EEE  EED\x1b[0m     C      A   A  N N N  O   O  P        Y
\x1b[32m   WEE EEE EED\x1b[0m      C      A   A  N  NN  O   O  P        Y
\x1b[32m     WEEEEED\x1b[0m         CCCC  A   A  N  NN   OOO   P        Y
\x1b[32m  WEEE  !  EEED\x1b[0m    
\x1b[32m        !\x1b[0m          \x1b[34mInitialization Complete!\x1b[0m This server has booted \x1b[4m${count}\x1b[0m time${count == 1 ? '' : 's'} and taken \x1b[4m${tokes}\x1b[0m toke${tokes == 1 ? '' : 's'}.
\x1b[32m        !\x1b[0m          This server was first booted on \x1b[4m${date}\x1b[0m.`

        //Dump art to console
        console.log(art);
        //Add some extra padding for the port printout from server.js
        process.stdout.write('                   ');
}