import { createWriteStream, existsSync, mkdirSync } from 'fs';
import path from 'path';
import { needleLog } from './logging.js';

const projectDir = process.cwd() + "/";

/**
 * @typedef {"auto-resolve" | ((res: string, packageName: string, index: number, importPath: string) => string | null | void)} PackageResolveValue
 */

/**
 * @type {Record<string, PackageResolveValue>}
 */
const packages_to_resolve = {
    // We are currently overriding "three" resolution to ensure that all dependencies resolve to the same three.js version.
    // This is hacky, but the alternative is potentially having conflicting three.js versions since some packages are
    // stubborn with their peer dependencies or just slow (slower as we) with updating.
    // NOT adding this allows node.js to correctly resolve `exports` specified in three.js package.json;
    // since we're overriding resolution here we need to manually resolve the subset of exports that we use.
    'three/addons': (res, packageName, index, importPath) => {
        return "three/examples/jsm";
    },
    'three/nodes': (res, packageName, index, importPath) => {
        return "three/examples/jsm/nodes/Nodes.js";
    },
    'three': "auto-resolve",

    // Handle all previous imports where users did import using @needle-engine/src
    '@needle-tools/engine/src': (res, packageName, index, importPath) => {
        // resolve old engine/src imports UNLESS it's the asap plugin (the asap plugin currently only exists in the src folder)
        if (!importPath.startsWith("@needle-tools/engine/src/asap")) {
            return res + "/../lib";
        }
    },
    /*
    Removed the previously present @needle-tools/engine entry
    because this is automatically done by vite according to whatever we define in our package.json exports
    This did previously prevent us from declaring proper exports in package.json
    */
    '@needle-tools/engine': (res, packageName, index, importPath) => {
        // Check if the import is something like @needle-tools/engine/engine/engine_utils
        // in which case we want to resolve into the lib directory
        if (importPath.startsWith("@needle-tools/engine/engine")) {
            return res + "/lib";
        }
        const node_modules_path = path.resolve(projectDir, 'node_modules', '@needle-tools/engine');
        if (existsSync(node_modules_path)) {
            return node_modules_path;
        }
    },

    // Auto resolve to 'node_modules/<name>' or '@needle-tools/engine/node_modules/<name>'
    'peerjs': "auto-resolve",
    'websocket-ts': "auto-resolve",
    'md5': "auto-resolve",
    'three-mesh-bvh': "auto-resolve",
    'postprocessing': "auto-resolve",
    '@dimforge/rapier3d-compat': "auto-resolve",
}

/**
 * @param {"build" | "serve"} command
 * @param {import('../types/needleConfig').needleMeta | null | undefined} config
 * @param {import('../types').userSettings} userSettings
 * @returns {import('vite').Plugin | import('vite').Plugin[] | undefined}
 */
export function needleViteAlias(command, config, userSettings) {

    if (config?.noAlias === true || userSettings?.noAlias === true)
        return;

    const debug = userSettings.debugAlias;

    let outputDebugFile = null;
    function log(...msg) {
        needleLog("needle-alias", msg.join(" "));
        if (debug) logToFile(...msg);
    }
    function logToFile(...msg) {
        outputDebugFile?.write(msg.join(" ") + "\n");
    }
    if (debug) {
        const outputFilePath = path.resolve(projectDir, 'node_modules/.needle/needle.alias.log');
        const outputDirectory = path.dirname(outputFilePath);
        if (!existsSync(outputDirectory)) mkdirSync(outputDirectory, { recursive: true });
        outputDebugFile = createWriteStream(outputFilePath, { flags: "a" });
        const timestamp = new Date().toISOString();
        outputDebugFile.write("\n\n\n--------------------------\n");
        outputDebugFile.write(`[needle-alias] Logging to: ${outputFilePath} (${timestamp})\n`);
        log("Logging to:", outputFilePath);
    }

    // Pre-compute resolved paths for each package
    /** @type {Array<{name: string, fullpath: string, pathExists: boolean, isNestedInEngine: boolean, callback: ((res: string, packageName: string, index: number, importPath: string) => string | null | void) | null}>} */
    const resolvers = [];
    /** @type {Array<{find: string, replacement: string}>} Aliases for nested engine auto-resolve packages */
    const nestedAliases = [];
    for (const name in packages_to_resolve) {
        const value = packages_to_resolve[name];
        const callback = typeof value === "function" ? value : null;
        const isAutoResolve = value === "auto-resolve";

        let fullpath = path.resolve(projectDir, 'node_modules', name);
        let isNestedInEngine = false;
        {
            const pathInEngine = path.resolve(projectDir, 'node_modules', "@needle-tools/engine", "node_modules", name);
            if (existsSync(pathInEngine)) {
                fullpath = pathInEngine;
                isNestedInEngine = true;
            }
        }
        const pathExists = existsSync(fullpath);
        resolvers.push({ name, fullpath, pathExists, isNestedInEngine, callback });

        // For auto-resolve packages nested in the engine, use resolve.alias so both
        // the optimizer and resolveId see the same path. This ensures Vite pre-bundles
        // these deps (handling CJS interop) while keeping a single copy.
        if (isAutoResolve && isNestedInEngine && pathExists) {
            nestedAliases.push({ find: name, replacement: fullpath });
        }
    }

    const workingDirectory = `${process.cwd()}/`;

    /** @type {import("vite").Plugin} */
    const aliasPlugin = {
        name: "needle-alias",
        // Register resolve.alias for nested engine packages so Vite's optimizer
        // can find and pre-bundle them (with proper CJS interop).
        config(config) {
            if (nestedAliases.length > 0) {
                if (!config.resolve) config.resolve = {};
                // resolve.alias can be an array of {find,replacement} or a Record<string,string>.
                // Convert to array format if needed (works in Vite 2-8).
                if (!config.resolve.alias) {
                    config.resolve.alias = [];
                }
                else if (!Array.isArray(config.resolve.alias)) {
                    // Convert Record<string,string> to array format
                    const entries = Object.entries(config.resolve.alias);
                    config.resolve.alias = entries.map(([find, replacement]) => ({ find, replacement }));
                }
                for (const alias of nestedAliases) {
                    config.resolve.alias.push(alias);
                    needleLog("needle-alias", `Adding resolve.alias: ${alias.find} → ${alias.replacement.split('node_modules/').pop()}`);
                }
            }
        },
        // Use resolveId instead of resolve.alias to support both Rollup and Rolldown (Vite 8+).
        // Rolldown does not support function-based alias replacements.
        enforce: 'pre',
        async resolveId(id, _importer, _options) {
            for (const { name, fullpath, pathExists, isNestedInEngine, callback } of resolvers) {
                // Check if import matches this resolver (exact match or starts with name/)
                if (id !== name && !id.startsWith(name + '/')) continue;

                if (callback !== null) {
                    const overrideResult = callback(fullpath, name, 0, id);
                    if (typeof overrideResult === "string") {
                        if (debug) log(`Redirecting "${id}"   →   "${overrideResult}"`);
                        // Let Vite resolve the result further (handles directories, package.json exports, etc.)
                        return this.resolve(overrideResult, _importer, { skipSelf: true });
                    }
                }

                // For auto-resolve packages in the project's own node_modules,
                // let Vite handle resolution naturally (preserves CJS interop / optimizeDeps).
                // Only redirect when the package is nested inside @needle-tools/engine/node_modules.
                if (pathExists && (callback !== null || isNestedInEngine)) {
                    if (debug) log(`Resolved "${id}"   →   "${fullpath.substring(workingDirectory.length).replaceAll("\\", "/")}"`);
                    // Rewrite the import to resolve from the target directory, letting Vite handle package.json/exports
                    const subpath = id.substring(name.length); // e.g. "" or "/something"
                    const rewritten = fullpath + subpath;
                    return this.resolve(rewritten, _importer, { skipSelf: true });
                }

                break;
            }
        },
    }

    /** @type {string|undefined} */
    let lastImporter = "";
    /** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */

    /** @type {import("vite").Plugin} */
    const debuggingPlugin = {
        name: "needle:alias-debug",
        // needs to run before regular resolver
        enforce: 'pre',
        resolveId(id, importer, options) {
            if (importer) {
                // simplify paths for better readability
                if (importer.includes("js/package~")) importer = "package~" + importer.split("js/package~")[1];
                if (importer.includes("node_modules/@needle-tools")) importer = "node_modules/@needle-tools" + importer.split("node_modules/@needle-tools")[1];
                if (importer.includes("node_modules/.vite")) importer = ".vite" + importer.split("node_modules/.vite")[1];
            }

            // verbose logging for all imports
            if (lastImporter !== importer) {
                lastImporter = importer;
                logToFile(`[needle-alias] Resolving: ${importer} (file${options?.ssr ? ", SSR" : ""})`);
            }
            logToFile(`[needle-alias]     → ${id}`);
            return;
        },
    }
    if (debug) return [debuggingPlugin, aliasPlugin];
    return [aliasPlugin];
}
