/**
 * Runs as git pre-commit hook
 * Filters the list of changed files on 'git commit'
 * If *.ts files in specified projects are detected, runs the 'lint:ts:fix' package.json script for that project
 * E.g. if a *.ts file is changed in /sdk, then this script will run 'pnpm run lint:ts:fix' in the /sdk project
 */

const fs = require('fs');
const { execSync } = require('child_process');

/**
 * ENUMS
 */

// File extensions to filter for
const FILE_EXTENSION = {
    TYPESCRIPT: "TYPESCRIPT",
    SOLIDITY: "SOLIDITY",
}

// Projects to filter for
const FOLDER = {
    BRIDGEUI: "BRIDGEUI",
    CONTRACTS: "CONTRACTS",
    E2E: "E2E",
    OPERATIONS: "OPERATIONS",
    POSTMAN: "POSTMAN",
    SDK: "SDK",
}

// Project runtimes
const RUNTIME = {
    NODEJS: "NODEJS"
}

/**
 * MAPPINGS
 */

// File extension => regex
const FILE_EXTENSION_FILTERS = {
    [FILE_EXTENSION.TYPESCRIPT]: "\.ts$",
    [FILE_EXTENSION.SOLIDITY]: "\.sol$",
};

// File extension => script in package.json to run for linting
const FILE_EXTENSION_LINTING_COMMAND = {
    [FILE_EXTENSION.TYPESCRIPT]: "pnpm run lint:ts:fix",
    [FILE_EXTENSION.SOLIDITY]: "pnpm run lint:sol:fix",
};

// File extension => script in package.json to run for documentation generation
const FILE_EXTENSION_DOCUMENTATION_UPDATING_COMMAND = {
    [FILE_EXTENSION.SOLIDITY]: "pnpm run solidity:docgen",
};

// Project => Path in monorepo
const FOLDER_PATH = {
    [FOLDER.BRIDGEUI]: "bridge-ui/",
    [FOLDER.CONTRACTS]: "contracts/",
    [FOLDER.E2E]: "e2e/",
    [FOLDER.OPERATIONS]: "operations/",
    [FOLDER.POSTMAN]: "postman/",
    [FOLDER.SDK]: "sdk/",
};

// Project => List of changed files
const FOLDER_CHANGED_FILES = {
    [FOLDER.BRIDGEUI]: new Array(),
    [FOLDER.CONTRACTS]: new Array(),
    [FOLDER.E2E]: new Array(),
    [FOLDER.OPERATIONS]: new Array(),
    [FOLDER.POSTMAN]: new Array(),
    [FOLDER.SDK]: new Array(),
};

// Project => Runtime
const FOLDER_RUNTIME = {
    [FOLDER.BRIDGEUI]: RUNTIME.NODEJS,
    [FOLDER.CONTRACTS]: RUNTIME.NODEJS,
    [FOLDER.E2E]: RUNTIME.NODEJS,
    [FOLDER.OPERATIONS]: RUNTIME.NODEJS,
    [FOLDER.POSTMAN]: RUNTIME.NODEJS,
    [FOLDER.SDK]: RUNTIME.NODEJS,
};

/**
 * MAIN FUNCTION
 */

main();

function main() {
    const changedFileList = getChangedFileList();
    partitionChangedFileList(changedFileList);

    for (const folder in FOLDER) {
        if (!isDependenciesInstalled(folder)) {
            console.error(`Dependencies not installed in ${FOLDER_PATH[folder]}, exiting...`)
            process.exit(1);
        }
        const changedFileExtensions = getChangedFileExtensions(folder);
        executeCommand(folder, changedFileExtensions, FILE_EXTENSION_LINTING_COMMAND);
        executeCommand(folder, changedFileExtensions, FILE_EXTENSION_DOCUMENTATION_UPDATING_COMMAND);
    }

    updateGitIndex();
}

/**
 * HELPER FUNCTIONS
 */

/**
 * Gets a list of changed files in the git commit
 * @returns {string[]}
 */
function getChangedFileList() {
    try {
        const cmd = 'git diff --name-only HEAD'
        const stdout = execSync(cmd, { encoding: 'utf8' });
        return stdout.split('\n').filter(file => file.trim() !== '');
    } catch (error) {
        console.error($`Error running ${cmd}:`, error.message);
        process.exit(1)
    }
}

/**
 * Partitions list of changed files from getChangedFileList() by project
 * Stores results in FOLDER_CHANGED_FILES
 * @param {string[]}
 */
function partitionChangedFileList(_changedFileList) {
    for (const file of _changedFileList) {
        for (const path in FOLDER) {
            if (file.match(new RegExp(`^${FOLDER_PATH[path]}`))) {
                FOLDER_CHANGED_FILES[path].push(file);
            }
        }
    }
}

/**
 * Checks if runtime dependencies are installed for a project
 * @param {FOLDER}
 * @returns {boolean}
 */
function isDependenciesInstalled(_folder) {
    const runtime = FOLDER_RUNTIME[_folder];
    const path = FOLDER_PATH[_folder];

    switch(runtime) {
        case RUNTIME.NODEJS:
            const dependencyFolder = `${path}node_modules`
            return fs.existsSync(dependencyFolder)
        default:
            console.error(`${runtime} runtime not supported.`);
            return false
    }
}

/**
 * Resolve list of changed file extensions for a project
 * @param {FOLDER}
 * @returns {FILE_EXTENSION[]}
 */
function getChangedFileExtensions(_folder) {
    // Use sets to implement early exit from loop, once we have matched all configured file extensions
    const remainingFileExtensionsSet = new Set(Object.values(FILE_EXTENSION));
    const foundFileExtensionsSet = new Set();

    for (const file of FOLDER_CHANGED_FILES[_folder]) {
        for (const fileExtension of remainingFileExtensionsSet) {
            if (file.match(new RegExp(FILE_EXTENSION_FILTERS[fileExtension]))) {
                foundFileExtensionsSet.add(fileExtension);
                remainingFileExtensionsSet.delete(fileExtension);
            }
        }

        // No more remaining file extensions to look for
        if (remainingFileExtensionsSet.size == 0) break; 
    }

    return Array.from(foundFileExtensionsSet);
}

/**
 * Execute command based on file extension
 * @param {FOLDER, FILE_EXTENSION[], FILE_EXTENSION_LINTING_COMMAND | FILE_EXTENSION_DOCUMENTATION_UPDATING_COMMAND}
 */
function executeCommand(_folder, _changedFileExtensions, _command) {
    for (const fileExtension of _changedFileExtensions) {
        const path = FOLDER_PATH[_folder];
        const cmd = _command[fileExtension];
        if (!cmd) return;
        console.log(`${fileExtension} change found in ${path}, executing command ${cmd}`);
        try {
            // Execute command synchronously and stream output directly to the current stdout
            execSync(`
                cd ${path};
                ${cmd};
            `, { stdio: 'inherit' });
        } catch (error) {
            console.error(`Error:`, error.message);
            console.error(`Exiting...`);
            process.exit(1);
        }
    }
}

/**
 * Redo `git add` for files updated during executeLinting(), so that they are not left out of the commit
 * The difference between 'git add .' and 'git update-index --again', is that the latter will not include untracked files
 */
function updateGitIndex() {
    try {
        const cmd = 'git update-index --again'
        execSync(cmd, { stdio: 'inherit' });
    } catch (error) {
        console.error($`Error running ${cmd}:`, error.message);
        process.exit(1);
    }
}