/**
 * JSDoctor
 *
 * Generates TypeScript declaration (`.d.ts`) files for Java classes so that
 * JS language servers (e.g. VSCode's built-in TS/JS IntelliSense) can offer
 * autocomplete, parameter hints, overload resolution, documentation and
 * navigation for objects obtained via `Java.type()` in Nashorn/GraalVM-based
 * JS scripting environments.
 *
 * Generated files are pure type declarations — nothing executes, they exist
 * solely for the language server to read. Runtime behaviour of user scripts
 * is never affected.
 *
 * File layout produced:
 *
 *   scripts/            <-- workspace root the language server sees
 *       java.d.ts
 *       lsp/
 *           generated/
 *               imgui/
 *                   ImGui.d.ts
 *                   ImGuiIO.d.ts
 *               net/
 *                   minecraft/
 *                       client/
 *                           Minecraft.d.ts
 *               ...
 *       (optional legacy .js JSDoc stubs)
 *
 * Declarations are organized into subfolders mirroring the Java package
 * hierarchy of each class, e.g. `net.minecraft.client.Minecraft` is written
 * to `lsp/generated/net/minecraft/client/Minecraft.d.ts`. Cross-file imports
 * and the `java.d.ts` mapping use relative paths that resolve across these
 * subfolders.
 *
 * `java.d.ts` maps every generated class name back to its declaration using
 * literal overloads of `Java.type`, so `Java.type("imgui.ImGui")` infers the
 * full `ImGui` type with zero manual annotations.
 *
 * @namespace JSDoctor
 *
 * @example
 * // Generate declarations for imgui.ImGui (plus referenced types)
 * JSDoctor.generate(Java.type("imgui.ImGui"));
 *
 * @example
 * // Generate declarations from a class name string instead of a live handle
 * JSDoctor.generate("net.minecraft.client.Minecraft");
 *
 * @example
 * // Recurse up to 3 levels deep and skip legacy JS stubs
 * JSDoctor.generate(Java.type("imgui.ImGui"), {
 *   recursive: true,
 *   maxDepth: 3,
 *   generateJsStubs: false
 * });
 */
const JSDoctor = (function () {

    const DEFAULT_OUTPUT_DIR = "config/neoscripts_js/scripts/lsp";

    /**
     * Content written to `jsconfig.json` at the workspace root on demand, so
     * JS language servers (VSCode/Zed) resolve `require(...)` of local libs to
     * typed modules. `checkJs` stays off so user scripts don't show a wall of
     * errors; per-file `// @ts-check` on libs is what feeds types into the
     * `require()` of consumers.
     */
    const JS_CONFIG = `{
  "compilerOptions": {
    "target": "es2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["**/*.js", "**/*.d.ts"],
  "exclude": ["lsp"]
}
`;

    const Modifier = Java.type('java.lang.reflect.Modifier');

    /**
     * Safely calls a `java.lang.reflect.Modifier` predicate, returning
     * `false` if the predicate is unavailable or throws. Some embedded
     * runtimes don't expose every Modifier method (e.g. `isSynthetic`), so
     * each check is guarded.
     * @private
     * @param {java.lang.reflect.Modifier} mods
     * @param {string} name
     * @returns {boolean}
     */
    function modifierFlag(mods, name) {
        try {
            const fn = Modifier[name];
            if (typeof fn === 'undefined') return false;
            return !!fn(mods);
        } catch (e) {
            return false;
        }
    }
    const isStaticMod = m => modifierFlag(m, 'isStatic');
    const isSyntheticMod = m => modifierFlag(m, 'isSynthetic');
    const isBridgeMod = m => modifierFlag(m, 'isBridge');
    const isTransientMod = m => modifierFlag(m, 'isTransient');
    const isPrivateMod = m => modifierFlag(m, 'isPrivate');
    const isPublicMod = m => modifierFlag(m, 'isPublic');

    /** Maps Java type names to their TypeScript equivalents. */
    const TYPE_MAP = {
        'boolean': 'boolean', 'Boolean': 'boolean',
        'byte': 'number', 'Byte': 'number',
        'short': 'number', 'Short': 'number',
        'int': 'number', 'Integer': 'number',
        'long': 'number', 'Long': 'number',
        'float': 'number', 'Float': 'number',
        'double': 'number', 'Double': 'number',
        'char': 'string', 'Character': 'string',
        'String': 'string',
        'void': 'void',
        'Object': 'any',
        'java.lang.String': 'string',
        'java.lang.Object': 'any',
        'java.lang.Boolean': 'boolean',
        'java.lang.Byte': 'number',
        'java.lang.Short': 'number',
        'java.lang.Integer': 'number',
        'java.lang.Long': 'number',
        'java.lang.Float': 'number',
        'java.lang.Double': 'number',
        'java.lang.Character': 'string',
        'java.lang.Number': 'number'
    };

    /** Types that must never be used as an `extends` target. */
    const NON_EXTENDABLE = {
        'java.lang.Object': true,
        'java.lang.Boolean': true,
        'java.lang.Byte': true,
        'java.lang.Short': true,
        'java.lang.Integer': true,
        'java.lang.Long': true,
        'java.lang.Float': true,
        'java.lang.Double': true,
        'java.lang.Character': true,
        'java.lang.String': true,
        'java.lang.Enum': true,
        'java.lang.Record': true
    };

    /**
     * Resolves a `java.lang.Class` from a Java object, a Class instance, or
     * a fully-qualified class name string.
     * @private
     * @param {Object|string} target
     * @returns {java.lang.Class}
     */
    function resolveClass(target) {
        if (typeof target === 'string') return Java.type(target).class;
        if (target && target.class) return target.class;
        if (target && typeof target.getClass === 'function') return target.getClass();
        return target;
    }

    /**
     * Safely retrieves the simple name of a class, falling back to manual
     * string parsing if JVM reflection throws due to bytecode mismatch
     * (e.g. IncompatibleClassChangeError in obfuscated/modded environments).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {string}
     */
    function safeGetSimpleName(clazz) {
        try {
            return clazz.getSimpleName();
        } catch (e) {
            const name = clazz.getName();
            const lastDot = name.lastIndexOf('.');
            const base = lastDot >= 0 ? name.substring(lastDot + 1) : name;
            const lastDollar = base.lastIndexOf('$');
            return lastDollar >= 0 ? base.substring(lastDollar + 1) : base;
        }
    }

    /**
     * Safe wrapper to retrieve declared fields. Returns an empty array if
     * class resolution fails (e.g. NoClassDefFoundError).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.reflect.Field[]}
     */
    function safeGetDeclaredFields(clazz) {
        try {
            return clazz.getDeclaredFields();
        } catch (e) {
            return [];
        }
    }

    /**
     * Safe wrapper to retrieve declared constructors. Returns an empty array if
     * class resolution fails (e.g. NoClassDefFoundError).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.reflect.Constructor[]}
     */
    function safeGetDeclaredConstructors(clazz) {
        try {
            return clazz.getDeclaredConstructors();
        } catch (e) {
            return [];
        }
    }

    /**
     * Safe wrapper to retrieve declared methods. Returns an empty array if
     * class resolution fails (e.g. NoClassDefFoundError).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.reflect.Method[]}
     */
    function safeGetDeclaredMethods(clazz) {
        try {
            return clazz.getDeclaredMethods();
        } catch (e) {
            return [];
        }
    }

    /**
     * Collects the full public method surface of a class: own declared
     * methods plus every method inherited from the superclass chain and all
     * implemented/exposed interfaces (Java default methods too). Private
     * methods are only kept when the declaring class is the class itself,
     * matching what the JS runtime can actually reach through interop.
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.reflect.Method[]}
     */
    function safeGetAllMethods(clazz) {
        const all = [];
        const seen = new Set();
        const queue = [];
        let head = 0;
        queue.push(clazz);
        while (head < queue.length) {
            const node = queue[head++];
            if (!node) continue;
            const nodeName = node.getName();
            if (nodeName === 'java.lang.Object') continue;
            if (seen.has(nodeName)) continue;
            seen.add(nodeName);
            try {
                const declared = node.getDeclaredMethods();
                for (let i = 0; i < declared.length; i++) {
                    const m = declared[i];
                    const mods = m.getModifiers();
                    if (m.getDeclaringClass() !== clazz && isPrivateMod(mods)) continue;
                    all.push(m);
                }
            } catch (e) {
                // skip classes that fail introspection
            }
            try {
                const sup = node.getSuperclass();
                if (sup) queue.push(sup);
            } catch (e) {}
            try {
                const ifaces = node.getInterfaces();
                for (let i = 0; i < ifaces.length; i++) queue.push(ifaces[i]);
            } catch (e) {}
        }
        return all;
    }

    /**
     * Safe wrapper to retrieve implemented interfaces. Returns an empty array if
     * class resolution fails (e.g. NoClassDefFoundError).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.Class[]}
     */
    function safeGetInterfaces(clazz) {
        try {
            return clazz.getInterfaces();
        } catch (e) {
            return [];
        }
    }

    /**
     * Safe wrapper to retrieve the superclass. Returns null if
     * class resolution fails (e.g. NoClassDefFoundError).
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.Class|null}
     */
    function safeGetSuperclass(clazz) {
        try {
            return clazz.getSuperclass();
        } catch (e) {
            return null;
        }
    }

    /**
     * Converts an arbitrary class name into a deterministic, filesystem-safe
     * identifier. Nested classes (`Outer$Inner`) map to `Outer_Inner`.
     * @private
     * @param {string} name
     * @returns {string}
     */
    function sanitizeIdent(name) {
        let out = '';
        for (let i = 0; i < name.length; i++) {
            const c = name.charAt(i);
            const isAlnum = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
            out += (isAlnum || c === '_') ? c : '_';
        }
        if (out === '') out = '_';
        if (out.charAt(0) >= '0' && out.charAt(0) <= '9') out = '_' + out;
        return out;
    }

    /**
     * Derives the exported identifier + file basename for a class.
     * @private
     * @param {java.lang.Class} clazz
     * @returns {string}
     */
    function outputName(clazz) {
        const fqcn = clazz.getName();
        const dot = fqcn.lastIndexOf('.');
        const tail = dot >= 0 ? fqcn.substring(dot + 1) : fqcn;
        return sanitizeIdent(tail);
    }

    /**
     * Returns the package segments of a fully-qualified class name as an
     * array (empty for the default package). Nested class markers (`$`) do
     * not form part of the package path.
     * @private
     * @param {string} fqcn
     * @returns {string[]}
     */
    function pkgDirs(fqcn) {
        const dot = fqcn.lastIndexOf('.');
        const pkg = dot >= 0 ? fqcn.substring(0, dot) : '';
        return pkg === '' ? [] : pkg.split('.');
    }

    /**
     * Builds the subfolder path (relative to the generated base dir) a
     * class's declaration lives in, mirroring its Java package hierarchy,
     * e.g. `net/minecraft/client/Minecraft` -> `net/minecraft/client`.
     * @private
     * @param {string} fqcn
     * @returns {string}
     */
    function packagePath(fqcn) {
        return pkgDirs(fqcn).join('/');
    }

    /**
     * Computes a relative import specifier from the folder containing
     * `fromFqcn` to the declaration file of `toFqcn`, used for cross-file
     * references between generated declarations that live in different
     * package subfolders.
     * @private
     * @param {string} fromFqcn
     * @param {string} toFqcn
     * @param {string} toName - Export/simple name of the target declaration.
     * @returns {string}
     */
    function relImportPath(fromFqcn, toFqcn, toName) {
        const from = pkgDirs(fromFqcn);
        const to = pkgDirs(toFqcn);
        let common = 0;
        while (common < from.length && common < to.length && from[common] === to[common]) common++;
        const parts = [];
        for (let i = common; i < from.length; i++) parts.push('..');
        for (let i = common; i < to.length; i++) parts.push(to[i]);
        parts.push(toName);
        return parts.join('/');
    }

    /**
     * Checks whether a name matches the javac default synthetic parameter
     * pattern `argN` (e.g. `arg0`, `arg12`). Written without RegExp since
     * some embedded GraalJS contexts don't expose the internal "regex"
     * polyglot language and throw on any regex literal/method.
     * @private
     * @param {string} name
     * @returns {boolean}
     */
    function looksLikeSyntheticArgName(name) {
        if (!name || name.length < 4 || name.substring(0, 3) !== 'arg') return false;
        for (let i = 3; i < name.length; i++) {
            const c = name.charAt(i);
            if (c < '0' || c > '9') return false;
        }
        return true;
    }

    /**
     * Returns a safe, human-readable parameter name, falling back to
     * `argN` when the class wasn't compiled with `-parameters`.
     * @private
     * @param {java.lang.reflect.Parameter} param
     * @param {number} index
     * @returns {string}
     */
    function paramName(param, index) {
        const name = param.getName();
        return (name && !looksLikeSyntheticArgName(name)) ? name : `arg${index}`;
    }

    /**
     * Converts a `java.lang.Class` to a TypeScript type string, registering
     * imports for types that have generated declarations.
     *
     * Handles primitive/boxed scalars, arrays, and falls back gracefully on
     * erased generic/unknown types (never throws).
     *
     * @private
     * @param {java.lang.Class} type
     * @param {Object} ctx - `{ fqcn, registry, imports }`
     * @returns {string}
     */
    function tsType(type, ctx) {
        if (!type) return 'any';
        try {
            if (type.isArray()) {
                return tsType(type.getComponentType(), ctx) + '[]';
            }
            if (type.isPrimitive()) {
                return TYPE_MAP[type.getName()] || 'number';
            }
            const name = type.getName();
            const simple = safeGetSimpleName(type);
            if (TYPE_MAP[simple]) return TYPE_MAP[simple];
            if (TYPE_MAP[name]) return TYPE_MAP[name];
            const meta = ctx.registry.get(name);
            if (meta) {
                if (name !== ctx.fqcn) ctx.imports.set(meta.outputName, relImportPath(ctx.fqcn, name, meta.outputName));
                return meta.outputName;
            }
            return 'any';
        } catch (e) {
            return 'any';
        }
    }

    /**
     * Collects all non-JDK, non-primitive types a class references via
     * fields, method/constructor parameters, return types, superclass and
     * implemented interfaces. Used for recursive generation.
     * @private
     * @param {java.lang.Class} clazz
     * @returns {java.lang.Class[]}
     */
    function collectRelatedTypes(clazz) {
        const seen = new Map();
        const results = [];

        function consider(type) {
            if (!type) return;
            if (type.isPrimitive()) return;
            if (type.isArray()) { consider(type.getComponentType()); return; }
            const name = type.getName();
            if (seen.has(name)) return;
            if (name.indexOf('java.') === 0 || name.indexOf('javax.') === 0 || name.indexOf('jdk.') === 0) return;
            if (TYPE_MAP[name]) return;
            if (name === clazz.getName()) return;
            seen.set(name, true);
            results.push(type);
        }

        try {
            const superClass = safeGetSuperclass(clazz);
            if (superClass && !NON_EXTENDABLE[superClass.getName()]) consider(superClass);
            
            const interfaces = safeGetInterfaces(clazz);
            for (let i = 0; i < interfaces.length; i++) consider(interfaces[i]);

            const fields = safeGetDeclaredFields(clazz);
            for (let i = 0; i < fields.length; i++) consider(fields[i].getType());

            const methods = safeGetDeclaredMethods(clazz);
            for (let i = 0; i < methods.length; i++) {
                consider(methods[i].getReturnType());
                const params = methods[i].getParameters();
                for (let j = 0; j < params.length; j++) consider(params[j].getType());
            }

            const ctors = safeGetDeclaredConstructors(clazz);
            for (let i = 0; i < ctors.length; i++) {
                const params = ctors[i].getParameters();
                for (let j = 0; j < params.length; j++) consider(params[j].getType());
            }
        } catch (e) {
            // ignore classes that fail introspection
        }

        return results;
    }

    /**
     * Renders the field declarations for a class.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx
     * @returns {string[]}
     */
    function renderFields(clazz, ctx) {
        const out = [];
        const fields = safeGetDeclaredFields(clazz);
        const names = [];
        const byName = new Map();
        for (let i = 0; i < fields.length; i++) {
            const f = fields[i];
            const mods = f.getModifiers();
            if (isSyntheticMod(mods) || isTransientMod(mods)) continue;
            if (!isPublicMod(mods)) continue;
            const name = f.getName();
            if (name.indexOf('$') >= 0) continue;
            if (isStaticMod(mods) && name === 'class' || name === 'serialVersionUID') continue;
            if (byName.has(name)) continue;
            byName.set(name, f);
            names.push(name);
        }
        names.sort();
        for (let i = 0; i < names.length; i++) {
            const f = byName.get(names[i]);
            try {
                const mods = f.getModifiers();
                const isStatic = isStaticMod(mods);
                const type = tsType(f.getType(), ctx);
                out.push(`    ${isStatic ? 'static ' : ''}${f.getName()}: ${type};`);
            } catch (e) {
                out.push(`    ${f.getName()}: any;`);
            }
        }
        return out;
    }

    /**
     * Renders constructor overload declarations for a class.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx
     * @returns {string[]}
     */
    function renderConstructors(clazz, ctx) {
        const out = [];
        const ctors = safeGetDeclaredConstructors(clazz);
        const sigs = [];
        for (let i = 0; i < ctors.length; i++) {
            const c = ctors[i];
            if (isPrivateMod(c.getModifiers())) continue;
            const parts = [];
            const params = c.getParameters();
            for (let j = 0; j < params.length; j++) {
                const name = paramName(params[j], j);
                parts.push(`${name}: ${tsType(params[j].getType(), ctx)}`);
            }
            sigs.push(`    constructor(${parts.join(', ')});`);
        }
        sigs.sort();
        for (let i = 0; i < sigs.length; i++) out.push(sigs[i]);
        return out;
    }

    /**
     * Renders overloaded method declarations for a class. Java reflection
     * returns every overload independently; identical names are merged and
     * every distinct signature is preserved.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx
     * @returns {string[]}
     */
    function renderMethods(clazz, ctx, asInterface) {
        const out = [];
        const methods = safeGetAllMethods(clazz);
        const grouped = new Map();

        for (let i = 0; i < methods.length; i++) {
            const m = methods[i];
            const mods = m.getModifiers();
            if (isSyntheticMod(mods) || isBridgeMod(mods)) continue;
            const name = m.getName();
            if (name === 'wait' || name === 'notify' || name === 'notifyAll' || name === 'getClass') continue;
            if (name.indexOf('$') >= 0) continue;
            if (!grouped.has(name)) grouped.set(name, []);
            grouped.get(name).push(m);
        }

        const methodNames = Array.from(grouped.keys());
        methodNames.sort();

        for (let n = 0; n < methodNames.length; n++) {
            const name = methodNames[n];
            const overloads = grouped.get(name);
            const sigs = [];
            const seen = {};

            for (let i = 0; i < overloads.length; i++) {
                const m = overloads[i];
                const isStatic = isStaticMod(m.getModifiers());
                let paramsStr = '';
                try {
                    const parts = [];
                    const params = m.getParameters();
                    for (let j = 0; j < params.length; j++) {
                        let ts = tsType(params[j].getType(), ctx);
                        let pName = paramName(params[j], j);
                        if (j === params.length - 1 && m.isVarArgs()) {
                            const comp = params[j].getType().getComponentType();
                            ts = tsType(comp ? comp : params[j].getType(), ctx) + '[]';
                            pName = '...' + pName;
                        }
                        parts.push(`${pName}: ${ts}`);
                    }
                    paramsStr = parts.join(', ');
                } catch (e) {
                    paramsStr = '';
                }
                const ret = (() => {
                    try {
                        return tsType(m.getReturnType(), ctx);
                    } catch (e) {
                        return 'any';
                    }
                })();
                const sig = `    ${(isStatic && !asInterface) ? 'static ' : ''}${name}(${paramsStr}): ${ret};`;
                if (!seen[sig]) {
                    seen[sig] = true;
                    sigs.push(sig);
                }
            }

            sigs.sort();
            for (let i = 0; i < sigs.length; i++) out.push(sigs[i]);
        }

        return out;
    }

    /**
     * Renders an enum declaration, falling back to a class with `static
     * readonly` constants when the enum constants cannot be enumerated.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx
     * @returns {string}
     */
    function renderEnum(clazz, ctx) {
        const simple = outputName(clazz);
        let constants = [];
        try {
            const vals = clazz.getEnumConstants();
            if (vals && vals.length > 0) {
                for (let i = 0; i < vals.length; i++) {
                    const c = vals[i];
                    const n = c.name();
                    if (n && n.indexOf('$') < 0) constants.push(n);
                }
            }
        } catch (e) {
            constants = [];
        }

        if (constants.length > 0) {
            const lines = [`export declare enum ${simple} {`];
            const sorted = constants.slice().sort();
            for (let i = 0; i < sorted.length; i++) lines.push(`    ${sorted[i]},`);
            lines.push('}');
            return lines.join('\n');
        }

        const lines = [`export declare class ${simple} {`];
        for (let i = 0; i < constants.length; i++) {
            lines.push(`    static readonly ${constants[i]}: ${simple};`);
        }
        lines.push('}');
        return lines.join('\n');
    }

    /**
     * Renders a complete `.d.ts` declaration for a `java.lang.Class`.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx - `{ registry, fqcn }`
     * @returns {string}
     */
    function renderDeclaration(clazz, ctx) {
        ctx = ctx || {};
        ctx.registry = ctx.registry || new Map();
        ctx.fqcn = clazz.getName();
        ctx.imports = new Map();

        const simple = outputName(clazz);
        const fqcn = clazz.getName();

        if (clazz.isEnum()) {
            return [
                `/**`,
                ` * Auto-generated declaration for ${fqcn}.`,
                ` * Generated by JSDoctor — do not edit by hand.`,
                ` */`,
                renderEnum(clazz, ctx)
            ].join('\n');
        }

        const header = [
            '/**',
            ` * Auto-generated declaration for ${fqcn}.`,
            ' * Generated by JSDoctor — do not edit by hand.',
            ' */'
        ];

        let declLine = `export declare class ${simple}`;

        let superName = null;
        try {
            const superClass = safeGetSuperclass(clazz);
            if (superClass && !NON_EXTENDABLE[superClass.getName()]) {
                const meta = ctx.registry.get(superClass.getName());
                if (meta && meta.outputName !== simple) {
                    ctx.imports.set(meta.outputName, relImportPath(clazz.getName(), superClass.getName(), meta.outputName));
                    superName = meta.outputName;
                }
            }
        } catch (e) {
            superName = null;
        }
        if (superName) declLine += ` extends ${superName}`;

        const interfaceNames = [];
        if (!clazz.isInterface()) {
            try {
                const interfaces = safeGetInterfaces(clazz);
                for (let i = 0; i < interfaces.length; i++) {
                    const iface = interfaces[i];
                    const meta = ctx.registry.get(iface.getName());
                    if (meta && meta.outputName !== simple) {
                        ctx.imports.set(meta.outputName, relImportPath(clazz.getName(), iface.getName(), meta.outputName));
                        interfaceNames.push(meta.outputName);
                    }
                }
            } catch (e) {
                // ignore
            }
        }
        if (interfaceNames.length > 0) declLine += ` implements ${interfaceNames.join(', ')}`;

        const body = [];

        if (clazz.isInterface()) {
            declLine = `export declare interface ${simple}` + (superName ? ` extends ${superName}` : '');
            body.push(...renderMethods(clazz, ctx, true));
            const lines = header.slice();
            const imports = Array.from(ctx.imports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
            for (let i = 0; i < imports.length; i++) {
                lines.push(`import { ${imports[i][0]} } from "./${imports[i][1]}";`);
            }
            lines.push('');
            lines.push(declLine + ' {');
            for (let i = 0; i < body.length; i++) lines.push(body[i]);
            lines.push('}');
            return lines.join('\n');
        }

        const fields = renderFields(clazz, ctx);
        const ctors = renderConstructors(clazz, ctx);
        const methods = renderMethods(clazz, ctx);

        for (let i = 0; i < fields.length; i++) body.push(fields[i]);
        if (fields.length > 0 && (ctors.length > 0 || methods.length > 0)) body.push('');
        for (let i = 0; i < ctors.length; i++) body.push(ctors[i]);
        if (ctors.length > 0 && methods.length > 0) body.push('');
        for (let i = 0; i < methods.length; i++) body.push(methods[i]);

        const lines = header.slice();
        const imports = Array.from(ctx.imports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
        for (let i = 0; i < imports.length; i++) {
            lines.push(`import { ${imports[i][0]} } from "./${imports[i][1]}";`);
        }
        lines.push('');
        lines.push(declLine + ' {');
        for (let i = 0; i < body.length; i++) lines.push(body[i]);
        lines.push('}');

        return lines.join('\n');
    }

    /**
     * Renders the legacy JSDoc-annotated `.js` stub for a class, kept for
     * compatibility with tooling that only understands JSDoc stubs.
     * @private
     * @param {java.lang.Class} clazz
     * @param {Object} ctx
     * @returns {string}
     */
    function renderJsStub(clazz, ctx) {
        ctx = ctx || {};
        ctx.registry = ctx.registry || new Map();
        ctx.fqcn = clazz.getName();
        ctx.imports = new Map();

        const className = outputName(clazz);
        const fullName = clazz.getName();

        const jsDocType = (type) => {
            if (!type) return '*';
            if (type.isArray()) return jsDocType(type.getComponentType()) + '[]';
            const t = tsType(type, ctx);
            return t === 'any' ? '*' : t;
        };

        const out = [
            '/**',
            ` * Auto-generated JSDoc stub for ${fullName}.`,
            ' * Generated by JSDoctor — do not edit by hand.',
            ' * @class',
            ' */',
            `class ${className} {`
        ];

		try {
			const fields = safeGetDeclaredFields(clazz);
			for (let i = 0; i < fields.length; i++) {
				const f = fields[i];
				const mods = f.getModifiers();
				if (isSyntheticMod(mods) || isStaticMod(mods) || !isPublicMod(mods)) continue;
				const name = f.getName();
				if (name.indexOf('$') >= 0) continue;
				try {
					out.push('  /**', `   * @type {${jsDocType(f.getType())}}`, '   */', `  ${name};`, '');
				} catch (e) {
					out.push('  /**', '   * @type {*}', '   */', `  ${name};`, '');
				}
			}
		} catch (e) {}

		try {
			const ctors = safeGetDeclaredConstructors(clazz);
			for (let i = 0; i < ctors.length; i++) {
				const c = ctors[i];
				if (isPrivateMod(c.getModifiers())) continue;
				const parts = [];
				const params = c.getParameters();
				for (let j = 0; j < params.length; j++) {
					const pName = paramName(params[j], j);
					parts.push(pName);
					out.push('  /**', `   * @param {${jsDocType(params[j].getType())}} ${pName}`, '   */');
				}
				out.push(`  constructor(${parts.join(', ')}) {}`, '');
			}
		} catch (e) {}

		try {
			const methods = safeGetDeclaredMethods(clazz);
			for (let i = 0; i < methods.length; i++) {
				const m = methods[i];
				const mods = m.getModifiers();
				if (isSyntheticMod(mods) || isBridgeMod(mods)) continue;
				const name = m.getName();
				if (name.indexOf('$') >= 0) continue;
				const isStatic = isStaticMod(mods);
				const jsdocLines = ['  /**'];
				const argNames = [];
				try {
					const params = m.getParameters();
					for (let j = 0; j < params.length; j++) {
						const pName = paramName(params[j], j);
						argNames.push(pName);
						jsdocLines.push(`   * @param {${jsDocType(params[j].getType())}} ${pName}`);
					}
				} catch (e) {
					// fall through with no params
				}
				let ret = 'void';
				try { ret = jsDocType(m.getReturnType()); } catch (e) { ret = '*'; }
				jsdocLines.push(`   * @returns {${ret}}`);
				if (isStatic) jsdocLines.push('   * @static');
				jsdocLines.push('   */');
				jsdocLines.push(`  ${isStatic ? 'static ' : ''}${name}(${argNames.join(', ')}) {}`);
				out.push(jsdocLines.join('\n'), '');
			}
		} catch (e) {}
        out.push('}', '', `module.exports = ${className};`, '');
        return out.join('\n');
    }

    /**
     * Writes UTF-8 text content to disk, creating parent directories as
     * needed.
     *
     * Uses `Files.writeString` rather than converting to a byte array
     * manually — GraalJS marshals JS strings straight into Java `String`/
     * `CharSequence` parameters for a method call, but `java.lang.String`
     * is a "boxed" host type: constructing one directly (`new JString(...)`)
     * gets collapsed back into a plain JS string with no Java methods, so
     * `.getBytes()` isn't available on it.
     *
     * @private
     * @param {string} dir
     * @param {string} fileName
     * @param {string} content
     * @returns {string} Absolute path written to.
     */
    function writeFile(dir, fileName, content) {
        const FilesClass = Java.type('java.nio.file.Files');
        const PathsClass = Java.type('java.nio.file.Paths');
        const dirPath = PathsClass.get(dir);
        FilesClass.createDirectories(dirPath);
        const filePath = dirPath.resolve(fileName);
        FilesClass.writeString(filePath, content);
        return filePath.toString();
    }

    /**
     * Reads existing `java.d.ts` entries so a later `generate()` call can
     * merge new classes in without dropping previously generated ones.
     * Returns `{ fqcn: { importPath, outputName, isInterface } }` where
     * `importPath` is the specifier inside `import("...")` (relative to the
     * scripts root, e.g. `./lsp/generated/com/google/common/collect/ImmutableList`).
     * @private
     * @param {string} filePath
     * @returns {Object}
     */
    function readJavaMap(filePath) {
        const result = {};
        try {
            const FilesClass = Java.type('java.nio.file.Files');
            const PathsClass = Java.type('java.nio.file.Paths');
            const path = PathsClass.get(filePath);
            if (!FilesClass.exists(path)) return result;
            const lines = FilesClass.readAllLines(path);
            for (let i = 0; i < lines.size(); i++) {
                const line = lines.get(i);
                const needle = 'function type(name: "';
                const idx = line.indexOf(needle);
                if (idx < 0) continue;
                const start = idx + needle.length;
                const endQ = line.indexOf('"', start);
                if (endQ < 0) continue;
                const fqcn = line.substring(start, endQ);
                const importIdx = line.indexOf('import("', endQ);
                if (importIdx < 0) continue;
                const pathStart = importIdx + 'import("'.length;
                const pathEnd = line.indexOf('")', pathStart);
                if (pathEnd < 0) continue;
                const importPath = line.substring(pathStart, pathEnd);
                const lastSlash = importPath.lastIndexOf('/');
                const outputName = lastSlash >= 0 ? importPath.substring(lastSlash + 1) : importPath;
                const prefix = line.substring(endQ, importIdx);
                const isInterface = prefix.indexOf('typeof') < 0;
                result[fqcn] = { importPath: importPath, outputName: outputName, isInterface: isInterface };
            }
        } catch (e) {
            // unable to read prior file; start from scratch
        }
        return result;
    }

    /**
     * Self-healing LSP infrastructure: creates `jsconfig.json` if missing and
     * injects `// @ts-check` into every `libs/*.js` without it. Called both at
     * module load and after every `generate()`, so a fresh install reprovides
     * the support files automatically. `Java.extend` is emitted directly when
     * `java.d.ts` is written, so it cannot be lost on regeneration.
     * @private
     */
    const ensureInfrastructure = (function () {
        let ran = false;
        return function ensureInfrastructure() {
            if (ran) return;
            ran = true;
            try {
                const FilesClass = Java.type('java.nio.file.Files');
                const PathsClass = Java.type('java.nio.file.Paths');

                const rootDir = DEFAULT_OUTPUT_DIR.replace(/\/lsp\/?$/, '');

                const cfgPath = PathsClass.get(rootDir + '/jsconfig.json');
                if (!FilesClass.exists(cfgPath)) {
                    FilesClass.writeString(cfgPath, JS_CONFIG);
                    console.log('[JSDoctor] created jsconfig.json -> ' + cfgPath.toString());
                }

                const libDir = rootDir + '/libs';
                const FileClass = Java.type('java.io.File');
                const libDirFile = new FileClass(libDir);
                if (libDirFile.isDirectory()) {
                    const files = libDirFile.listFiles();
                    for (let i = 0; i < files.length; i++) {
                        const f = files[i];
                        const name = f.getName();
                        if (!name.endsWith('.js')) continue;
                        const text = FilesClass.readString(f.toPath());
                        if (text.indexOf('@ts-check') >= 0) continue;
                        FilesClass.writeString(f.toPath(), '// @ts-check\n' + text);
                        console.log('[JSDoctor] Injected // @ts-check into libs/' + name);
                    }
                }
            } catch (e) {
                console.log('[JSDoctor] infrastructure bootstrap failed: ' + e);
            }
        };
    })();

    return {
        /**
         * Generates TypeScript declaration (`.d.ts`) files for the given Java
         * class or object, plus a `java.d.ts` mapping every generated class
         * name back to its declaration via `Java.type(...)` literal overloads.
         *
         * `lsp/generated/` (organized into package subfolders, e.g.
         * `lsp/generated/net/minecraft/client/`); `java.d.ts` is written to
         * the scripts root (the parent of `outputDir`) so the language server
         * sees it as part of the workspace. Optional legacy `.js` JSDoc stubs
         * are written alongside the declarations unless `generateJsStubs` is
         * disabled.
         *
         * @param {Object|string} target - A Java object, `java.lang.Class`,
         *   or fully-qualified class name (e.g. `'imgui.ImGui'`).
         * @param {Object} [options]
         * @param {string} [options.outputDir='config/neoscripts_js/scripts/lsp'] -
         *   Base directory declarations are written to (see layout above).
         * @param {boolean} [options.recursive=false] - Also generate
         *   declarations for custom (non-JDK) types referenced by fields,
         *   parameters, return types, superclass and interfaces.
         * @param {number} [options.maxDepth=1] - Max recursion depth when
         *   `recursive` is enabled.
         * @param {boolean} [options.generateJsStubs=true] - Also emit legacy
         *   `.js` JSDoc stubs alongside the `.d.ts` declarations.
         * @returns {string} Absolute path of the generated `java.d.ts`.
         *
         * @example
         * JSDoctor.generate(Java.type("imgui.ImGui"));
         *
         * @example
         * JSDoctor.generate("net.minecraft.client.Minecraft", {
         *   recursive: true,
         *   maxDepth: 2
         * });
         */
        generate(target, options) {
            options = options || {};
            const outputDir = options.outputDir || DEFAULT_OUTPUT_DIR;
            const recursive = !!options.recursive;
            const maxDepth = options.maxDepth || 1;
            const generateJsStubs = options.generateJsStubs !== false;

            const generatedDir = outputDir + '/generated';

            const strippedDir = outputDir.replace(/\/+$/, '');
            const lastDirSep = strippedDir.lastIndexOf('/');
            const javaDir = lastDirSep >= 0 ? strippedDir.substring(0, lastDirSep) : '';
            const javaImportPrefix = './' + strippedDir.substring(lastDirSep + 1) + '/generated';

            const registry = new Map();
            const entry = resolveClass(target);
            const visited = new Set();
            const queue = [{ clazz: entry, depth: 0 }];

            while (queue.length > 0) {
                const { clazz, depth } = queue.shift();
                const fullName = clazz.getName();
                if (visited.has(fullName)) continue;
                visited.add(fullName);
                registry.set(fullName, {
                    fqcn: fullName,
                    clazz: clazz,
                    simpleName: safeGetSimpleName(clazz),
                    outputName: outputName(clazz),
                    pkgPath: packagePath(fullName)
                });

                if (recursive && depth < maxDepth) {
                    const related = collectRelatedTypes(clazz);
                    for (let i = 0; i < related.length; i++) {
                        queue.push({ clazz: related[i], depth: depth + 1 });
                    }
                }
            }

            for (const meta of registry.values()) {
                const dir = meta.pkgPath ? `${generatedDir}/${meta.pkgPath}` : generatedDir;
                const ctx = { registry: registry, fqcn: meta.fqcn };
                const declaration = renderDeclaration(meta.clazz, ctx);
                const dtsPath = writeFile(dir, `${meta.outputName}.d.ts`, declaration);
                console.log(`[JSDoctor] Generated ${meta.outputName}.d.ts -> ${dtsPath}`);

                if (generateJsStubs) {
                    const jsStub = renderJsStub(meta.clazz, ctx);
                    const jsPath = writeFile(dir, `${meta.outputName}.js`, jsStub);
                    console.log(`[JSDoctor] Generated ${meta.outputName}.js -> ${jsPath}`);
                }
            }

            const prior = readJavaMap(`${javaDir}/java.d.ts`);
            const classKeys = Array.from(registry.keys());
            for (let i = 0; i < classKeys.length; i++) {
                const meta = registry.get(classKeys[i]);
                const importPath = meta.pkgPath
                    ? `${javaImportPrefix}/${meta.pkgPath}/${meta.outputName}`
                    : `${javaImportPrefix}/${meta.outputName}`;
                prior[meta.fqcn] = {
                    importPath: importPath,
                    outputName: meta.outputName,
                    isInterface: meta.clazz.isInterface()
                };
            }

            const javaLines = [
                '/**',
                ' * Auto-generated mapping from Java.type(...) class names to',
                ' * their generated declarations.',
                ' * Generated by JSDoctor — do not edit by hand.',
                ' */',
                'declare namespace Java {'
            ];
            const keys = Object.keys(prior).sort();
            for (let i = 0; i < keys.length; i++) {
                const entry = prior[keys[i]];
                const ref = entry.isInterface
                    ? `import("${entry.importPath}").${entry.outputName}`
                    : `typeof import("${entry.importPath}").${entry.outputName}`;
                javaLines.push(
                    `    function type(name: "${keys[i]}"): ${ref};`
                );
            }
            javaLines.push('    function type(name: string): any;');
            javaLines.push('    function extend(host: Function, overrides: any): any;');
            javaLines.push('}');

            const javaPath = writeFile(javaDir, 'java.d.ts', javaLines.join('\n'));
            console.log(`[JSDoctor] Generated java.d.ts -> ${javaPath}`);

            ensureInfrastructure();

            return javaPath;
        },
        renderDeclaration(target, options) {
            const clazz = resolveClass(target);
            const registry = new Map();
            registry.set(clazz.getName(), {
                fqcn: clazz.getName(),
                clazz: clazz,
                simpleName: safeGetSimpleName(clazz),
                outputName: outputName(clazz)
            });
            return renderDeclaration(clazz, { registry: registry, fqcn: clazz.getName() });
        },
        resolveClass
    };
})();

module.exports = JSDoctor;