const modulename = 'WebServer:SetupPost';
import path from 'node:path';
import fse from 'fs-extra';
import fsp from 'node:fs/promises';
import slash from 'slash';
import { Deployer } from '@core/deployer/index';
import { validateFixServerConfig, findLikelyCFGPath } from '@lib/fxserver/fxsConfigHelper';
import got from '@lib/got';
import consoleFactory from '@lib/console';
import recipeParser from '@core/deployer/recipeParser';
import { validateTargetPath } from '@core/deployer/utils';
import { TxConfigState } from '@shared/enums';
const console = consoleFactory(modulename);

//Helper functions
const isUndefined = (x) => (x === undefined);

const getDirectories = (source) => {
    return fse.readdirSync(source, { withFileTypes: true })
        .filter((dirent) => dirent.isDirectory())
        .map((dirent) => dirent.name);
};

const getPotentialServerDataFolders = (source) => {
    try {
        return getDirectories(source)
            .filter((dirent) => getDirectories(path.join(source, dirent)).includes('resources'))
            .map((dirent) => slash(path.join(source, dirent)) + '/');
    } catch (error) {
        console.verbose.warn(`Failed to find server data folder with message: ${error.message}`);
        return [];
    }
};

/*
    NOTE: How forgiving are we:
        - Ignore trailing slashes, as well as fix backslashes
        - Check if its the parent folder
        - Check if its inside the parent folder
        - Check if its inside current folder
        - Check if it contains the string `/resources`, then if its the path up to that string
        - Detect config as `server.cfg` or with wrong extensions inside the Server Data Folder

    FIXME: Also note that this entire file is a bit too messy, please clean it up a bit
*/

/**
 * Handle all the server control actions
 * FIXME: separate into validate.ts, saveDeployer.ts, and saveLocal.ts files
 * FIXME: or maybe postDeployer.ts, and postLocal.ts files
 * @param {object} ctx
 */
export default async function SetupPost(ctx) {
    //Sanity check
    if (isUndefined(ctx.params.action)) {
        return ctx.utils.error(400, 'Invalid Request');
    }
    const action = ctx.params.action;

    //Check permissions
    if (!ctx.admin.testPermission('all_permissions', modulename)) {
        return ctx.send({
            success: false,
            message: 'You need to be the admin master or have all permissions to use the setup page.',
        });
    }

    //Ensure the correct state for the setup page
    if (txManager.configState !== TxConfigState.Setup) {
        return ctx.send({
            success: false,
            refresh: true,
        });
    }

    //Delegate to the specific action functions
    if (action == 'validateRecipeURL') {
        return await handleValidateRecipeURL(ctx);
    } else if (action == 'validateLocalDeployPath') {
        return await handleValidateLocalDeployPath(ctx);
    } else if (action == 'validateLocalDataFolder') {
        return await handleValidateLocalDataFolder(ctx);
    } else if (action == 'validateCFGFile') {
        return await handleValidateCFGFile(ctx);
    } else if (action == 'save' && ctx.request.body.type == 'popular') {
        return await handleSaveDeployerImport(ctx);
    } else if (action == 'save' && ctx.request.body.type == 'remote') {
        return await handleSaveDeployerImport(ctx);
    } else if (action == 'save' && ctx.request.body.type == 'custom') {
        return await handleSaveDeployerCustom(ctx);
    } else if (action == 'save' && ctx.request.body.type == 'local') {
        return await handleSaveLocal(ctx);
    } else {
        return ctx.send({
            success: false,
            message: 'Unknown setup action.',
        });
    }
};


/**
 * Handle Validation of a remote recipe/template URL
 * @param {object} ctx
 */
async function handleValidateRecipeURL(ctx) {
    //Sanity check
    if (isUndefined(ctx.request.body.recipeURL)) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }
    const recipeURL = ctx.request.body.recipeURL.trim();

    //Make request & validate recipe
    try {
        const recipeText = await got.get({
            url: recipeURL,
            timeout: { request: 4500 }
        }).text();
        if (typeof recipeText !== 'string') throw new Error('This URL did not return a string.');
        const recipe = recipeParser(recipeText);
        return ctx.send({success: true, name: recipe.name});
    } catch (error) {
        return ctx.send({success: false, message: `Recipe error: ${error.message}`});
    }
}


/**
 * Handle Validation of a remote recipe/template URL
 * @param {object} ctx
 */
async function handleValidateLocalDeployPath(ctx) {
    //Sanity check
    if (isUndefined(ctx.request.body.deployPath)) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }
    const deployPath = slash(path.normalize(ctx.request.body.deployPath.trim()));

    //Perform path checking
    try {
        await validateTargetPath(deployPath); // will throw if invalid
        return ctx.send({success: true, message: 'Path is valid.'});
    } catch (error) {
        return ctx.send({success: false, message: error.message});
    }
}


/**
 * Handle Validation of Local (existing) Server Data Folder
 * @param {object} ctx
 */
async function handleValidateLocalDataFolder(ctx) {
    //Sanity check
    if (isUndefined(ctx.request.body.dataFolder)) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }
    const dataFolderPath = slash(path.normalize(ctx.request.body.dataFolder.trim() + '/'));

    //FIXME: replace with stuff in core/routes/settings/saveConfigs.ts > handleFxserverCard
    try {
        if (!fse.existsSync(path.join(dataFolderPath, 'resources'))) {
            const recoveryTemplate = `The path provided is invalid. <br>
                But it looks like <code>{{attempt}}</code> is correct. <br>
                Do you want to use it instead?`;

            //Recovery if parent folder
            const attemptIsParent = path.join(dataFolderPath, '..');
            if (fse.existsSync(path.join(attemptIsParent, 'resources'))) {
                const message = recoveryTemplate.replace('{{attempt}}', attemptIsParent);
                return ctx.send({success: false, message, suggestion: attemptIsParent});
            }

            //Recovery parent inside folder
            const attemptOutside = getPotentialServerDataFolders(path.join(dataFolderPath, '..'));
            if (attemptOutside.length >= 1) {
                const message = recoveryTemplate.replace('{{attempt}}', attemptOutside[0]);
                return ctx.send({success: false, message, suggestion: attemptOutside[0]});
            }

            //Recovery if resources
            if (dataFolderPath.includes('/resources')) {
                const attemptRes = dataFolderPath.split('/resources')[0];
                if (fse.existsSync(path.join(attemptRes, 'resources'))) {
                    const message = recoveryTemplate.replace('{{attempt}}', attemptRes);
                    return ctx.send({success: false, message, suggestion: attemptRes});
                }
            }

            //Recovery subfolder
            const attemptInside = getPotentialServerDataFolders(dataFolderPath);
            if (attemptInside.length >= 1) {
                const message = recoveryTemplate.replace('{{attempt}}', attemptInside[0]);
                return ctx.send({success: false, message, suggestion: attemptInside[0]});
            }

            //really invalid :(
            throw new Error("Couldn't locate or read a resources folder inside of the path provided.");
        } else {
            return ctx.send({
                success: true,
                detectedConfig: findLikelyCFGPath(dataFolderPath),
            });
        }
    } catch (error) {
        return ctx.send({success: false, message: error.message});
    }
}


/**
 * Handle Validation of CFG File
 * @param {object} ctx
 */
async function handleValidateCFGFile(ctx) {
    //Sanity check
    if (
        isUndefined(ctx.request.body.dataFolder)
        || isUndefined(ctx.request.body.cfgFile)
    ) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }

    const dataFolderPath = slash(path.normalize(ctx.request.body.dataFolder.trim()));
    const cfgFilePathNormalized = slash(path.normalize(ctx.request.body.cfgFile.trim()));

    //Validate file
    try {
        const result = await validateFixServerConfig(cfgFilePathNormalized, dataFolderPath);
        if (result.errors) {
            const message = `**The file path is correct, but there are error(s) in your config file(s):**\n${result.errors}`;
            return ctx.send({success: false, markdown: true, message});
        } else {
            return ctx.send({success: true});
        }
    } catch (error) {
        const message = `Error:\n ${error.message}.`;
        return ctx.send({success: false, message});
    }
}


/**
 * Handle Save settings for local server data imports
 * Actions: sets serverDataPath/cfgPath, starts the server, redirect to live console
 * @param {object} ctx
 */
async function handleSaveLocal(ctx) {
    //Sanity check
    if (
        isUndefined(ctx.request.body.name)
        || isUndefined(ctx.request.body.dataFolder)
        || isUndefined(ctx.request.body.cfgFile)
    ) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }

    //Prepare body input
    const cfg = {
        name: ctx.request.body.name.trim(),
        dataFolder: slash(path.normalize(ctx.request.body.dataFolder + '/')),
        cfgFile: slash(path.normalize(ctx.request.body.cfgFile)),
    };

    //Validating Server Data Path
    try {
        const stat = await fsp.stat(path.join(cfg.dataFolder, 'resources'))
        if (!stat.isDirectory()) {
            throw new Error('not a directory');
        }
    } catch (error) {
        let msg = error?.message ?? 'unknown error';
        if (error?.code === 'ENOENT') {
            msg = 'The server data folder does not exist.';
        }
        return ctx.send({success: false, message: `<strong>Server Data Folder error:</strong> ${msg}`});
    }

    //Preparing & saving config
    try {
        txCore.configStore.saveConfigs({
            general: {
                serverName: cfg.name,
            },
            server: {
                dataPath: cfg.dataFolder,
                cfgPath: cfg.cfgFile,
            }
        }, ctx.admin.name);
    } catch (error) {
        console.warn(`[${ctx.admin.name}] Error changing global/fxserver settings via setup stepper.`);
        console.verbose.dir(error);
        return ctx.send({
            type: 'danger',
            markdown: true,
            message: `**Error saving the configuration file:**\n${error.message}`
        });
    }

    //Refreshing config
    txCore.cacheStore.set('deployer:recipe', 'none');

    //Logging
    ctx.admin.logAction('Changing global/fxserver settings via setup stepper.');

    //If running (for some reason), kill it first 
    if (!txCore.fxRunner.isIdle) {
        ctx.admin.logCommand('STOP SERVER');
        await txCore.fxRunner.killServer('new server set up', ctx.admin.name, true);
    }

    //Starting server
    const spawnError = await txCore.fxRunner.spawnServer(false);
    if (spawnError !== null) {
        return ctx.send({success: false, markdown: true, message: spawnError});
    } else {
        return ctx.send({success: true});
    }
}


/**
 * Handle Save settings for remote recipe importing
 * Actions: download recipe, starts deployer
 * @param {object} ctx
 */
async function handleSaveDeployerImport(ctx) {
    //Sanity check
    if (
        isUndefined(ctx.request.body.name)
        || isUndefined(ctx.request.body.isTrustedSource)
        || isUndefined(ctx.request.body.recipeURL)
        || isUndefined(ctx.request.body.targetPath)
        || isUndefined(ctx.request.body.deploymentID)
    ) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }
    const isTrustedSource = (ctx.request.body.isTrustedSource === 'true');
    const serverName = ctx.request.body.name.trim();
    const recipeURL = ctx.request.body.recipeURL.trim();
    const targetPath = slash(path.normalize(ctx.request.body.targetPath + '/'));
    const deploymentID = ctx.request.body.deploymentID;

    //Get recipe
    let recipeText;
    try {
        recipeText = await got.get({
            url: recipeURL,
            timeout: { request: 4500 }
        }).text();
        if (typeof recipeText !== 'string') throw new Error('This URL did not return a string.');
    } catch (error) {
        return ctx.send({success: false, message: `Recipe download error: ${error.message}`});
    }

    //Preparing & saving config
    try {
        txCore.configStore.saveConfigs({
            general: { serverName },
        }, ctx.admin.name);
    } catch (error) {
        console.warn(`[${ctx.admin.name}] Error changing global settings via setup stepper.`);
        console.verbose.dir(error);
        return ctx.send({
            type: 'danger',
            markdown: true,
            message: `**Error saving the configuration file:** ${error.message}`
        });
    }
    ctx.admin.logAction('Changing global settings via setup stepper and started Deployer.');

    //Start deployer (constructor will validate the recipe)
    try {
        txManager.startDeployer(recipeText, deploymentID, targetPath, isTrustedSource, {serverName});
        txCore.webServer.webSocket.pushRefresh('status');
    } catch (error) {
        return ctx.send({success: false, message: error.message});
    }
    return ctx.send({success: true});
}


/**
 * Handle Save settings for custom recipe
 * Actions: download recipe, starts deployer
 * @param {object} ctx
 */
async function handleSaveDeployerCustom(ctx) {
    //Sanity check
    if (
        isUndefined(ctx.request.body.name)
        || isUndefined(ctx.request.body.targetPath)
        || isUndefined(ctx.request.body.deploymentID)
    ) {
        return ctx.utils.error(400, 'Invalid Request - missing parameters');
    }
    const serverName = ctx.request.body.name.trim();
    const targetPath = slash(path.normalize(ctx.request.body.targetPath + '/'));
    const deploymentID = ctx.request.body.deploymentID;

    //Preparing & saving config
    try {
        txCore.configStore.saveConfigs({
            general: { serverName },
        }, ctx.admin.name);
    } catch (error) {
        console.warn(`[${ctx.admin.name}] Error changing global settings via setup stepper.`);
        console.verbose.dir(error);
        return ctx.send({
            type: 'danger',
            markdown: true,
            message: `**Error saving the configuration file:** ${error.message}`
        });
    }
    ctx.admin.logAction('Changing global settings via setup stepper and started Deployer.');

    //Start deployer (constructor will create the recipe template)
    const customMetaData = {
        author: ctx.admin.name,
        serverName,
    };
    try {
        txManager.startDeployer(false, deploymentID, targetPath, false, customMetaData);
        txCore.webServer.webSocket.pushRefresh('status');
    } catch (error) {
        return ctx.send({success: false, message: error.message});
    }
    return ctx.send({success: true});
}
