// @ts-check
import path from 'path';
import fs from 'fs';

/** @typedef {{ id: string, imports: GraphNode[], importedBy: GraphNode[] }} GraphNode */

/**
 * @param {"build" | "serve"} _command
 * @param {unknown} _config
 * @param {import('../types').userSettings} userSettings
 * @returns {import('vite').Plugin | null}
 */
export function needleImportsLogger(_command, _config, userSettings) {

    if (!userSettings.debugImportChains) return null;

    /** @type {{allNodes: Map<string, GraphNode>, graph: Record<string, unknown>}} */
    const graph = {
        allNodes: new Map(),
        graph: {},
    };

    const showOneLevelOfImportsThatWereAlreadyLogged = false;
    const showOneLevelOfImportedBy = true;
    const logToConsole = false;
    const logToImportsLogFile = true;

    return {
        name: 'needle:imports-logger',
        enforce: 'pre',
        /** @param {string} id @param {string | undefined} importer */
        resolveId(id, importer) {

            // we want to make a graph of all the imports
            // so when we get this call,
            // we can store the importer and id in a graph
            
            // to make this work properly we need to resolve the paths all to absolute paths
            // assuming that id is relative to importer
            if (importer)
                id = path.resolve(path.dirname(importer), id);

            if (!graph.allNodes.has(id)) {
                graph.allNodes.set(id, {
                    id,
                    imports: [],
                    importedBy: [],
                });
            }

            if (importer && !graph.allNodes.has(importer)) {
                graph.allNodes.set(importer, {
                    id: importer,
                    imports: [],
                    importedBy: [],
                });
            }

            const node = graph.allNodes.get(id);
            const importerNode = importer ? graph.allNodes.get(importer) : undefined;
            if (node && importerNode) {
                if (!node.importedBy.includes(importerNode))
                    node.importedBy.push(importerNode);
                if (!importerNode.imports.includes(node))
                    importerNode.imports.push(node);
            }

            return;
        },
        /** @param {Error | undefined} [_error] */
        buildEnd(_error) {
            try {
                // create log file and append lines to it
                const logFile = path.resolve(process.cwd(), "imports.log");
                // append a single line efficiently; replace if necessary
                const fd = fs.openSync(logFile, "w");

                /** @param {string} str */
                const write = (str) => {
                    if (logToConsole)
                        console.log(str);

                    if (logToImportsLogFile) {
                        // replace coloring characters
                        str = str.replace(/\x1b\[\d+m/g, "");
                        fs.writeSync(fd, str + "\n");
                    }
                }
                
                // log graph
                const loggedNodes = /** @type {Set<string>} */ (new Set());
                /** @param {GraphNode} node @param {number} depth @param {0|1|2} type */
                const logNode = (node, depth, type) => {
                    if (!showOneLevelOfImportsThatWereAlreadyLogged && loggedNodes.has(node.id))
                        return;
                    const char = ["├", "├", "└"][type];
                    const depthStringWithExactlyThreePlaces = depth.toString().padStart(3, " ");
                    let spacing = ("│   ").repeat(depth);
                    // format console.log with dim grey
                    let id = node.id;
                    const alreadyLogged = loggedNodes.has(node.id);
                    if (alreadyLogged)
                        id = "\x1b[2m" + id + "\x1b[0m";

                    const suffix = node.imports.length > 0 ? " \x1b[2m[+" + node.imports.length + "]\x1b[0m" : "";
                    const lineString = "[" + depthStringWithExactlyThreePlaces + "]" + spacing;
                    write(lineString + char + "─ " + id + (!showOneLevelOfImportsThatWereAlreadyLogged ? suffix : ""));

                    if (showOneLevelOfImportedBy && node.importedBy.length > 0) {
                        for (let i = 0; i < node.importedBy.length; i++) {
                            const _importer = node.importedBy[i];
                            write(lineString + "│  " + "\x1b[2m^ " + _importer.id + "\x1b[0m");
                        }
                    }

                    if (alreadyLogged)
                        return;

                    loggedNodes.add(node.id);

                    // all utf8 characters for showing a tree in a console:
                    // ─ │ ├ └ ┌ ┐ ┘ ┴ ┬ ─ │ ├ └ ┌ ┐ ┘ ┴ ┬

                    // get list of nodes that haven't been displayed before
                    // const childNodes = node.imports.filter(n => !loggedNodes.has(n.id));
                    const childNodes = node.imports;
                    const omittedChildNodes = node.imports.length - childNodes.length;
                    for( let i = 0; i < childNodes.length; i++) {
                        const _import = childNodes[i];
                        const isFirst = i === 0;
                        const isLast = i === childNodes.length - 1;
                        logNode(_import, depth + 1, isLast ? 2 : isFirst ? 0 : 1);
                    };
                    if (omittedChildNodes > 0) {
                        const spacing = ("│   ").repeat(depth + 1);
                        write("[" + depthStringWithExactlyThreePlaces + "]" + spacing + "└" + "─" + " " + omittedChildNodes + " more");
                    }
                }
                graph.allNodes.forEach(node => {
                    try {
                        logNode(node, 1, 0);
                    }
                    catch (e) {
                        console.error(e);
                        return;
                    }
                });

                fs.closeSync(fd);
                console.log(logFile + "(1,0) : [needle-imports-logger] Wrote Module Imports log file.");
            }
            catch (e) {
                console.error("Error in plugin", e);
            }
        }
    }
}