"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const fs = require("fs");
const shell = require("shelljs");
const child_process_1 = require("child_process");
const util_1 = require("util");
const console_1 = require("../../utils/console");
const watcher_1 = require("../../utils/watcher");
const errors_1 = require("../../utils/errors");
const sidebarGenerator_1 = require("../../utils/sidebarGenerator");
const defaults_1 = require("./defaults");
const exercise_1 = require("./exercise");
const file_1 = require("../file");
/* exercise folder name standard */
const execAsync = (0, util_1.promisify)(child_process_1.exec);
// eslint-disable-next-line
const fetch = require("node-fetch");
// eslint-disable-next-line
const chalk = require("chalk");
/* exercise folder name standard */
/**
 * Retrieves the configuration path for the learnpack package.
 *
 * @returns An object containing the configuration file path and the base directory.
 * @throws NotFoundError if the learn.json file is not found in the current folder.
 */
const getConfigPath = () => {
    const possibleFileNames = ["learn.json", ".learn/learn.json"];
    const config = possibleFileNames.find(file => fs.existsSync(file)) || null;
    if (config && fs.existsSync(".breathecode"))
        return { config, base: ".breathecode" };
    if (config === null)
        throw (0, errors_1.NotFoundError)("learn.json file not found on current folder, is this a learnpack package?");
    return { config, base: ".learn" };
};
const getExercisesPath = (base) => {
    const possibleFileNames = ["./exercises", base + "/exercises", "./"];
    return possibleFileNames.find(file => fs.existsSync(file)) || null;
};
const getGitpodAddress = () => {
    if (shell.exec("gp -h", { silent: true }).code === 0) {
        return shell
            .exec("gp url", { silent: true })
            .stdout.replace(/(\r\n|\n|\r)/gm, "");
    }
    console_1.default.debug("Gitpod command line tool not found");
    return "http://localhost";
};
const getCodespacesNamespace = () => {
    // Example: https://orange-rotary-phone-wxpg49q5gcg4rp-3000.app.github.dev
    const codespace_name = shell
        .exec("echo $CODESPACE_NAME", { silent: true })
        .stdout.replace(/(\r\n|\n|\r)/gm, "");
    if (!codespace_name ||
        codespace_name === "" ||
        codespace_name === undefined ||
        codespace_name === "$CODESPACE_NAME") {
        return null;
    }
    return codespace_name;
};
const checkDuration = (configObj) => {
    if (!configObj.exercises || !configObj.config || !configObj.config.dirPath)
        return;
    const learnJSONPath = path.join(configObj.config.dirPath, "../learn.json");
    const learnJSON = fs.readFileSync(learnJSONPath, "utf8");
    const learnJSONObj = JSON.parse(learnJSON);
    const duration = learnJSONObj.duration;
    if (!duration) {
        learnJSONObj.duration = configObj.exercises.reduce((acc) => acc + 2, 0);
        fs.writeFileSync(learnJSONPath, JSON.stringify(learnJSONObj, null, 2));
    }
};
exports.default = async ({ grading, mode, disableGrading, version, }) => {
    var _a, _b, _c;
    const confPath = getConfigPath();
    const suggestions = { agent: null };
    console_1.default.debug("This is the config path: ", confPath);
    let configObj = {};
    if (confPath) {
        const learnJsonContent = fs.readFileSync(confPath.config);
        let hiddenBcContent = {};
        if (fs.existsSync(confPath.base + "/config.json")) {
            hiddenBcContent = fs.readFileSync(confPath.base + "/config.json");
            hiddenBcContent = JSON.parse(hiddenBcContent);
            if (!hiddenBcContent)
                throw new Error(`Invalid ${confPath.base}/config.json syntax: Unable to parse.`);
        }
        const jsonConfig = JSON.parse(`${learnJsonContent}`);
        if ((_a = jsonConfig === null || jsonConfig === void 0 ? void 0 : jsonConfig.editor) === null || _a === void 0 ? void 0 : _a.agent) {
            suggestions.agent = jsonConfig.editor.agent;
        }
        if (!jsonConfig)
            throw new Error(`Invalid ${confPath.config} syntax: Unable to parse.`);
        let session;
        // add using id to the installation
        if (!jsonConfig.session) {
            session = Math.floor(Math.random() * 10000000000000000000);
        }
        else {
            session = jsonConfig.session;
            delete jsonConfig.session;
        }
        configObj = deepMerge(hiddenBcContent, {
            config: jsonConfig,
            session: session,
        });
    }
    else {
        throw (0, errors_1.ValidationError)("No learn.json file has been found, make sure you are in the correct directory. To start a new LearnPack package run: $ learnpack init my-course-name");
    }
    configObj = deepMerge(defaults_1.default || {}, configObj, {
        config: {
            grading: grading || ((_b = configObj.config) === null || _b === void 0 ? void 0 : _b.grading),
            configPath: confPath.config,
        },
    });
    if (!configObj.config)
        throw (0, errors_1.InternalError)("Config object not found");
    configObj.config.outputPath = confPath.base + "/dist";
    console_1.default.debug("This is your configuration object: ", Object.assign(Object.assign({}, configObj), { exercises: configObj.exercises ?
            configObj.exercises.map(e => e.slug) :
            [] }));
    // auto detect agent (if possible)
    const codespaces_workspace = getCodespacesNamespace();
    console_1.default.debug("This is the codespace namespace: ", codespaces_workspace);
    if (!configObj.config.editor) {
        configObj.config.editor = {};
    }
    configObj.config.suggestions = suggestions;
    if (shell.which("gp") && configObj && configObj.config) {
        console_1.default.debug("Gitpod detected");
        configObj.address = getGitpodAddress();
        configObj.config.publicUrl = `https://${configObj.config.port}-${(_c = configObj.address) === null || _c === void 0 ? void 0 : _c.slice(8)}`;
        configObj.config.editor.agent = "vscode";
    }
    else if (configObj.config && Boolean(codespaces_workspace)) {
        console_1.default.debug("Codespaces detected: ", codespaces_workspace);
        configObj.address = `https://${codespaces_workspace}.github.dev`;
        configObj.config.publicUrl = `https://${codespaces_workspace}-${configObj.config.port}.app.github.dev`;
        configObj.config.editor.agent = "vscode";
    }
    else {
        console_1.default.debug("Neither Gitpod nor Codespaces detected, using localhost.");
        configObj.address = `http://localhost:${configObj.config.port}`;
        configObj.config.publicUrl = `http://localhost:${configObj.config.port}`;
        configObj.config.editor.agent =
            process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os";
    }
    if (configObj.config.editor.agent === "vscode" && isCodeCommandAvailable()) {
        const extensionName = "learn-pack.learnpack-vscode";
        if (!isExtensionInstalled(extensionName)) {
            console_1.default.info("The LearnPack extension is not installed, trying to install it automatically.");
            if (!installExtension(extensionName)) {
                configObj.config.warnings = {
                    extension: EXTENSION_INSTALLATION_STEPS,
                };
                console_1.default.warning("The LearnPack extension is not installed and this tutorial is meant to be run inside VSCode. Please install the LearnPack extension.");
            }
        }
    }
    if (configObj.config.suggestions &&
        configObj.config.editor &&
        configObj.config.suggestions.agent !== null &&
        configObj.config.suggestions.agent !== configObj.config.editor.agent) {
        console_1.default.warning(`The suggested agent "${configObj.config.suggestions.agent}" is different from the detected agent "${configObj.config.editor.agent}"`);
        try {
            configObj.config.warnings = Object.assign(Object.assign({}, configObj.config.warnings), { agent: buildAgentWarning(configObj.config.editor.agent, configObj.config.suggestions.agent) });
        }
        catch (_d) {
            console_1.default.error("IMPOSSIBLE TO SET WARNING");
        }
    }
    else {
        configObj.config.warnings.agent = undefined;
    }
    if (configObj.config && !configObj.config.publicUrl)
        configObj.config.publicUrl = `${configObj.address}:${configObj.config.port}`;
    if (configObj.config && !configObj.config.editor.mode && mode) {
        configObj.config.editor.mode = mode;
    }
    configObj.config.editor.mode =
        process.env.TERM_PROGRAM === "vscode" ? "extension" : "preview";
    if (version)
        configObj.config.editor.version = version;
    else if (configObj.config.editor.version === null) {
        console_1.default.debug("Config version not found, downloading default.");
        const resp = await fetch("https://raw.githubusercontent.com/learnpack/ide/master/package.json");
        const packageJSON = await resp.json();
        configObj.config.editor.version = packageJSON.version || "5.0.0";
    }
    configObj.config.dirPath = "./" + confPath.base;
    configObj.config.exercisesPath = getExercisesPath(confPath.base) || "./";
    if (configObj.config.variables) {
        const allowedCommands = new Set(["ls", "pwd", "echo", "node", "python"]);
        const variableKeys = Object.keys(configObj.config.variables);
        const promises = [];
        for (const v of variableKeys) {
            if (Object.prototype.hasOwnProperty.call(configObj.config.variables, v)) {
                const variable = configObj.config.variables[v];
                if (typeof variable === "string") {
                    continue;
                }
                const type = variable.type;
                if (type === "command") {
                    const promise = (async () => {
                        try {
                            // Validate the command
                            if (!allowedCommands.has(variable.source.split(" ")[0])) {
                                throw new Error("Command not allowed");
                            }
                            // Execute the code and replace the value
                            const { stdout } = await execAsync(variable.source);
                            if (!configObj.config || !configObj.config.variables)
                                return;
                            configObj.config.variables[v] = stdout.trim();
                        }
                        catch (error) {
                            console.error(`Error executing code: ${error}`);
                        }
                    })();
                    promises.push(promise);
                }
                else if (type === "string") {
                    configObj.config.variables[v] = variable.source;
                }
            }
        }
        await Promise.all(promises);
    }
    return {
        validLanguages: {},
        get: () => {
            return configObj;
        },
        validateEngine: function (language, server, socket) {
            // eslint-disable-next-line
            const alias = (_l) => {
                const map = {
                    python3: "python",
                };
                if (map[_l])
                    return map[_l];
                return _l;
            };
            // decode aliases
            language = alias(language);
            if (this.validLanguages[language])
                return true;
            console_1.default.debug(`Validating engine for ${language} compilation`);
            let result = shell.exec("learnpack plugins", { silent: true });
            if (result.code === 0 &&
                result.stdout.includes(`@learnpack/${language}`)) {
                this.validLanguages[language] = true;
                return true;
            }
            console_1.default.info(`Language engine for ${language} not found, installing...`);
            // Install the compiler in their new versions
            result = shell.exec(`learnpack plugins:install @learnpack/${language}`, {
                silent: true,
            });
            if (result.code === 0) {
                socket.log("compiling", "Installing the python compiler, you will have to reset the exercises after installation by writing on your terminal: $ learnpack run");
                console_1.default.info(`Successfully installed the ${language} exercise engine, \n please start learnpack again by running the following command: \n ${chalk.white("$ learnpack start")}\n\n `);
                server.terminate();
                return false;
            }
            this.validLanguages[language] = false;
            socket.error(`Error installing ${language} exercise engine`);
            console_1.default.error(`Error installing ${language} exercise engine`);
            console_1.default.log(result.stdout);
            throw (0, errors_1.InternalError)(`Error installing ${language} exercise engine`);
        },
        logout: () => {
            if (configObj.config) {
                (0, file_1.rmSync)(configObj.config.dirPath + "/.session");
            }
        },
        clean: async () => {
            if (configObj.config) {
                if (configObj.config.outputPath) {
                    (0, file_1.rmSync)(configObj.config.outputPath);
                }
                (0, file_1.rmSync)(configObj.config.dirPath + "/_app");
                (0, file_1.rmSync)(configObj.config.dirPath + "/reports");
                (0, file_1.rmSync)(configObj.config.dirPath + "/.session");
                (0, file_1.rmSync)(configObj.config.dirPath + "/resets");
                // clean __pycache__ folder
                (0, file_1.rmSync)(configObj.config.dirPath + "/../__pycache__");
                // clean the __pycache__ for each exercise it exists
                if (configObj.exercises) {
                    for (const e of configObj.exercises) {
                        if (fs.existsSync(e.path + "/__pycache__")) {
                            (0, file_1.rmSync)(e.path + "/__pycache__");
                        }
                    }
                }
                // clean tag gz
                if (fs.existsSync(configObj.config.dirPath + "/app.tar.gz"))
                    fs.unlinkSync(configObj.config.dirPath + "/app.tar.gz");
                if (fs.existsSync(configObj.config.dirPath + "/config.json"))
                    fs.unlinkSync(configObj.config.dirPath + "/config.json");
                if (fs.existsSync(configObj.config.dirPath + "/telemetry.json"))
                    fs.unlinkSync(configObj.config.dirPath + "/telemetry.json");
                if (fs.existsSync(configObj.config.dirPath + "/vscode_queue.json"))
                    fs.unlinkSync(configObj.config.dirPath + "/vscode_queue.json");
                await (0, sidebarGenerator_1.checkAndFixSidebar)(configObj, true);
                checkDuration(configObj);
            }
        },
        getExercise: slug => {
            console_1.default.debug("ExercisePath Slug", slug);
            const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
            if (!exercise)
                throw (0, errors_1.ValidationError)(`Exercise ${slug} not found`);
            return exercise;
        },
        getAllExercises: () => {
            return configObj.exercises;
        },
        startExercise: function (slug) {
            const exercise = this.getExercise(slug);
            // set config.json with current exercise
            configObj.currentExercise = exercise.slug;
            this.save();
            // eslint-disable-next-line
            exercise.files.forEach((f) => {
                if (configObj.config) {
                    const _path = configObj.config.outputPath + "/" + f.name;
                    if (f.hidden === false && fs.existsSync(_path))
                        fs.unlinkSync(_path);
                }
            });
            return exercise;
        },
        noCurrentExercise: function () {
            configObj.currentExercise = null;
            this.save();
        },
        reset: slug => {
            var _a;
            if (configObj.config &&
                !fs.existsSync(`${configObj.config.dirPath}/resets/` + slug))
                throw (0, errors_1.ValidationError)("Could not find the original files for " + slug);
            const exercise = (configObj.exercises || []).find(ex => ex.slug === slug);
            if (!exercise)
                throw (0, errors_1.ValidationError)(`Exercise ${slug} not found on the configuration`);
            if (configObj.config) {
                for (const fileName of fs.readdirSync(`${configObj.config.dirPath}/resets/${slug}/`)) {
                    const content = fs.readFileSync(`${(_a = configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath}/resets/${slug}/${fileName}`);
                    fs.writeFileSync(`${exercise.path}/${fileName}`, content);
                }
            }
        },
        buildIndex: function () {
            var _a, _b;
            console_1.default.info("Building the exercise index...");
            const isDirectory = (source) => {
                var _a;
                const name = path.basename(source);
                if (name === path.basename(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath) || ""))
                    return false;
                // ignore folders that start with a dot
                if (name.charAt(0) === "." || name.charAt(0) === "_")
                    return false;
                return fs.lstatSync(source).isDirectory();
            };
            const getDirectories = (source) => fs
                .readdirSync(source)
                .map(name => path.join(source, name))
                .filter(isDirectory);
            // add the .learn folder
            if (!fs.existsSync(confPath.base))
                fs.mkdirSync(confPath.base);
            // add the outout folder where webpack will publish the the html/css/js files
            if (configObj.config &&
                configObj.config.outputPath &&
                !fs.existsSync(configObj.config.outputPath))
                fs.mkdirSync(configObj.config.outputPath);
            // TODO: we could use npm library front-mater to read the title of the exercises from the README.md
            const grupedByDirectory = getDirectories(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "");
            configObj.exercises =
                grupedByDirectory.length > 0 ?
                    grupedByDirectory.map((path, position) => (0, exercise_1.exercise)(path, position, configObj)) :
                    [(0, exercise_1.exercise)(((_b = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _b === void 0 ? void 0 : _b.exercisesPath) || "", 0, configObj)];
            if (configObj.config) {
                configObj.config.editor.agent =
                    process.env.TERM_PROGRAM === "vscode" ? "vscode" : "os";
            }
            this.save();
        },
        createExercise: (slug, content, language) => {
            var _a;
            try {
                const dirPath = `${(_a = configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath}/${slug}`;
                if (!fs.existsSync(dirPath))
                    fs.mkdirSync(dirPath, { recursive: true });
                const isEnglish = language === "us" || language === "en";
                const fileName = isEnglish ? "README.md" : `README.${language}.md`;
                fs.writeFileSync(`${dirPath}/${fileName}`, content);
                return true;
            }
            catch (error) {
                console_1.default.error("Error creating exercise: ", error);
                return false;
            }
        },
        deleteExercise: (slug) => {
            var _a;
            const dirPath = `${(_a = configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath}/${slug}`;
            if (fs.existsSync(dirPath)) {
                fs.rmSync(dirPath, { recursive: true, force: true });
                return true;
            }
            return false;
        },
        getSidebar: () => {
            var _a;
            const sidebarPath = path.join(((_a = configObj.config) === null || _a === void 0 ? void 0 : _a.dirPath) || "", "sidebar.json");
            if (fs.existsSync(sidebarPath)) {
                return JSON.parse(fs.readFileSync(sidebarPath, "utf8"));
            }
            return {};
        },
        watchIndex: function (onChange) {
            var _a;
            if (configObj.config && !configObj.config.exercisesPath)
                throw (0, errors_1.ValidationError)("No exercises directory to watch: " + configObj.config.exercisesPath);
            this.buildIndex();
            (0, watcher_1.default)(((_a = configObj === null || configObj === void 0 ? void 0 : configObj.config) === null || _a === void 0 ? void 0 : _a.exercisesPath) || "", onChange)
                .then(() => {
                console_1.default.debug("Changes detected on your exercises");
                this.buildIndex();
                // if (onChange) onChange(filename);
            })
                .catch(error => {
                throw error;
            });
        },
        save: () => {
            console_1.default.debug("Saving configuration with: ", configObj);
            // remove the duplicates form the actions array
            // configObj.config.actions = [...new Set(configObj.config.actions)];
            if (configObj.config) {
                configObj.config.translations = [
                    ...new Set(configObj.config.translations),
                ];
                fs.writeFileSync(configObj.config.dirPath + "/config.json", JSON.stringify(configObj, null, 4));
            }
        },
    };
};
function deepMerge(...sources) {
    let acc = {};
    for (const source of sources) {
        if (Array.isArray(source)) {
            if (!Array.isArray(acc)) {
                acc = [];
            }
            acc = [...source];
        }
        else if (source instanceof Object) {
            // eslint-disable-next-line
            for (let [key, value] of Object.entries(source)) {
                if (value instanceof Object && key in acc) {
                    value = deepMerge(acc[key], value);
                }
                if (value !== undefined)
                    acc = Object.assign(Object.assign({}, acc), { [key]: value });
            }
        }
    }
    return acc;
}
const buildAgentWarning = (current, suggested) => {
    const message = `# Agent mismatch!\n
        
In LearnPack, the agent is in charge of running the LearnPack interface.

You're currently using LearnPack through \`${current}\` but the suggested agent is \`${suggested}\`. 

We recommend strongly recommend changing your agent.

  ${stepsToChangeAgent(suggested)}
       `;
    return message;
};
const stepsToChangeAgent = (agent) => {
    if (agent === "vscode") {
        return `
# Steps to Change Agent to VSCode

1. **Install VSCode**:
   - Visit the [VSCode website](https://code.visualstudio.com/Download) and download the latest version.
   - Follow the installation instructions for your operating system.

2. **Install LearnPack VSCode Extension**:
   - Open VSCode.
   - Go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window or by pressing \`Ctrl+Shift+X\`.
   - Search for "LearnPack" and click "Install".
   - If the extension is already installed but disabled, enable it.

3. **Run LearnPack in VSCode**:
   - Open your terminal in VSCode.
   - Navigate to your LearnPack project directory.
   - Run the following command:
     \`\`\`sh
     learnpack start
     \`\`\`

We strongly recommend using VSCode for a better learning experience.
    `;
    }
    if (agent === "os") {
        return `
# Steps to Change Agent to OS

This learning package was designed to run outside of VSCode. We strongly recommend closing VSCode and running the package independently.

1. **Close VSCode**:
   - Save your work and close the VSCode application.

2. **Open a New Terminal**:
   - Open a terminal or command prompt on your operating system.

3. **Navigate to Your LearnPack Project Directory**:
   - Use the \`cd\` command to navigate to the directory where your LearnPack project is located.

4. **Run LearnPack**:
   - Run the following command to start LearnPack:
     \`\`\`sh
     learnpack start
     \`\`\`

We strongly recommend running the package independently for a better learning experience.
    `;
    }
    return "";
};
/**
 * Checks if the 'code' command is available in the terminal.
 *
 * @returns {boolean} True if the 'code' command is available, false otherwise.
 */
const isCodeCommandAvailable = () => {
    return shell.which("code") !== null;
};
/**
 * Checks if a specific VSCode extension is installed.
 *
 * @param {string} extensionName - The name of the extension to check.
 * @returns {boolean} True if the extension is installed, false otherwise.
 */
const isExtensionInstalled = (extensionName) => {
    if (!isCodeCommandAvailable()) {
        throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
    }
    const result = shell.exec("code --list-extensions", { silent: true });
    return result.stdout.split("\n").includes(extensionName);
};
const EXTENSION_INSTALLATION_STEPS = `
# Steps to Install LearnPack VSCode Extension

1. **Open VSCode**:
   - Launch the Visual Studio Code application on your computer.

2. **Go to Extensions View**:
   - Click on the Extensions icon in the Activity Bar on the side of the window.
   - Alternatively, you can open the Extensions view by pressing \`Ctrl+Shift+X\`.

3. **Search for LearnPack**:
   - In the Extensions view, type "LearnPack" into the search bar.

4. **Install the Extension**:
   - Find the "LearnPack" extension in the search results and click the "Install" button.
   - If the extension is already installed but disabled, click the "Enable" button.

5. **Verify Installation**:
   - Once installed, you can verify the installation by running the following command in the terminal:
     \`\`\`sh
     code --list-extensions | grep learn-pack.learnpack-vscode
     \`\`\`
   - If the extension is listed, it means the installation was successful.

We strongly recommend using the LearnPack extension in VSCode for a better learning experience.
`;
/**
 * Installs the LearnPack VSCode extension if the 'code' command is available.
 *
 * @param {string} extensionName - The name of the extension to install.
 * @returns {boolean} True if the installation was successful, false otherwise.
 */
const installExtension = (extensionName) => {
    if (!isCodeCommandAvailable()) {
        throw new Error("The 'code' command is not available in the terminal. Please ensure that VSCode is installed and the 'code' command is added to your PATH.");
    }
    const result = shell.exec(`code --install-extension ${extensionName}`, {
        silent: true,
    });
    return result.code === 0;
};
