import { parser as pythonParser } from "@lezer/python";
const PYTHON_BUILTINS = new Set([
    "print",
    "len",
    "range",
    "enumerate",
    "zip",
    "map",
    "filter",
    "sorted",
    "reversed",
    "sum",
    "min",
    "max",
    "abs",
    "all",
    "any",
    "chr",
    "ord",
    "bin",
    "hex",
    "oct",
    "int",
    "float",
    "str",
    "bool",
    "list",
    "dict",
    "set",
    "tuple",
    "frozenset",
    "bytes",
    "bytearray",
    "memoryview",
    "object",
    "type",
    "isinstance",
    "issubclass",
    "hasattr",
    "getattr",
    "setattr",
    "delattr",
    "dir",
    "vars",
    "id",
    "hash",
    "callable",
    "format",
    "repr",
    "ascii",
    "open",
    "input",
    "round",
    "pow",
    "divmod",
    "complex",
    "eval",
    "exec",
    "compile",
    "globals",
    "locals",
    "iter",
    "next",
    "slice",
    "staticmethod",
    "classmethod",
    "property",
    "super",
    "help",
    "quit",
    "exit",
    "True",
    "False",
    "None",
    "NotImplemented",
    "Ellipsis",
    "Exception",
    "BaseException",
    "ValueError",
    "TypeError",
    "KeyError",
    "IndexError",
    "AttributeError",
    "RuntimeError",
    "ImportError",
    "StopIteration",
    "__name__",
    "__doc__",
    "__file__",
    "__package__",
    "__loader__",
    "__spec__",
    "__annotations__",
    "__builtins__",
]);
const DEFAULT_TIMEOUT_MS = 5000;
/**
 * Extracts external identifiers from Python code using Lezer parser with scope tracking.
 * This function identifies identifiers that are referenced but not declared locally.
 *
 * Uses a three-pass approach:
 * Pass 1: Collect all locally declared identifiers
 * Pass 2: Collect all imported identifiers
 * Pass 3: Extract member expressions and filter out local/imported identifiers and Python builtins
 *
 * @param code - The Python code to analyze
 * @param timeoutMs - Maximum time to spend parsing (default: 5000ms). Returns empty array on timeout.
 */
export function extractExternalIdentifiersFromPython(code, timeoutMs = DEFAULT_TIMEOUT_MS) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const tree = pythonParser.parse(code);
        // Pass 1: Collect all locally declared identifiers
        const localIdentifiers = collectLocalIdentifiers(code, tree.topNode, controller.signal);
        // Pass 2: Collect all imported identifiers
        const importedIdentifiers = collectImportedIdentifiers(code, tree.topNode, controller.signal);
        // Merge local and imported identifiers
        const excludedIdentifiers = new Set([
            ...localIdentifiers,
            ...importedIdentifiers,
        ]);
        // Pass 3: Extract member expressions and filter out excluded identifiers
        const externalIdentifiers = extractMemberExpressions(code, tree.topNode, excludedIdentifiers, controller.signal);
        // Filter out Python builtins
        return Array.from(externalIdentifiers).filter((id) => !PYTHON_BUILTINS.has(id));
    }
    catch {
        return [];
    }
    finally {
        clearTimeout(timeout);
    }
}
/**
 * Handles:
 * - import os → "os"
 * - import os.path → "os"
 * - from datetime import datetime → "datetime"
 * - import numpy as np → "np" (alias)
 */
export function collectImportedIdentifiers(code, node, signal) {
    const importedIdentifiers = new Set();
    function visit(node) {
        // Check if aborted
        if (signal?.aborted) {
            throw new DOMException("Aborted", "AbortError");
        }
        if (node.name === "ImportStatement") {
            let child = node.firstChild;
            let hasFrom = false;
            let afterImport = false;
            // First pass: check if this is a "from ... import ..." statement
            let checkChild = child;
            while (checkChild) {
                if (signal?.aborted) {
                    throw new DOMException("Aborted", "AbortError");
                }
                if (checkChild.name === "from") {
                    hasFrom = true;
                    break;
                }
                checkChild = checkChild.nextSibling;
            }
            // Second pass: collect identifiers
            while (child) {
                if (signal?.aborted) {
                    throw new DOMException("Aborted", "AbortError");
                }
                if (child.name === "import") {
                    afterImport = true;
                }
                else if (afterImport && child.name === "VariableName") {
                    if (hasFrom) {
                        // from X import Y - collect Y (the imported name)
                        const importedName = code.substring(child.from, child.to);
                        importedIdentifiers.add(importedName);
                        // Check for alias (Y as Z) - collect Z
                        const nextChild = child.nextSibling;
                        if (nextChild && nextChild.name === "as") {
                            const aliasChild = nextChild.nextSibling;
                            if (aliasChild && aliasChild.name === "VariableName") {
                                const aliasName = code.substring(aliasChild.from, aliasChild.to);
                                importedIdentifiers.add(aliasName);
                            }
                        }
                        // Continue to next VariableName for multiple imports
                    }
                    else {
                        // import X or import X.Y - collect only first identifier (X)
                        const moduleName = code.substring(child.from, child.to);
                        importedIdentifiers.add(moduleName);
                        // Check for alias (X as Z)
                        const nextChild = child.nextSibling;
                        if (nextChild && nextChild.name === "as") {
                            const aliasChild = nextChild.nextSibling;
                            if (aliasChild && aliasChild.name === "VariableName") {
                                const aliasName = code.substring(aliasChild.from, aliasChild.to);
                                importedIdentifiers.add(aliasName);
                            }
                        }
                        // For regular import, stop after the first VariableName
                        // (don't collect "path" from "import os.path")
                        break;
                    }
                }
                else if (afterImport && child.name === "*") {
                    // from module import * - can't track what's imported
                    // Don't add anything
                }
                child = child.nextSibling;
            }
            return;
        }
        // Visit all children
        let child = node.firstChild;
        while (child) {
            if (signal?.aborted) {
                throw new DOMException("Aborted", "AbortError");
            }
            visit(child);
            child = child.nextSibling;
        }
    }
    visit(node);
    return importedIdentifiers;
}
/**
 * Pass 1: Collect all locally declared identifiers using tree traversal
 */
export function collectLocalIdentifiers(code, node, signal) {
    const localIdentifiers = new Set();
    function visit(node) {
        // Check if aborted
        if (signal?.aborted) {
            throw new DOMException("Aborted", "AbortError");
        }
        switch (node.name) {
            case "AssignStatement": {
                // Variable assignment: x = ..., x, y = ...
                // The left side of assignment contains variable names
                let child = node.firstChild;
                while (child && child.name !== "AssignOp") {
                    if (signal?.aborted) {
                        throw new DOMException("Aborted", "AbortError");
                    }
                    if (child.name === "VariableName") {
                        const varName = code.substring(child.from, child.to);
                        localIdentifiers.add(varName);
                    }
                    child = child.nextSibling;
                }
                // Continue visiting all children (including right side) to find nested local vars
                visitChildren(node);
                return;
            }
            case "FunctionDefinition": {
                // Function definition: def func_name(...)
                let child = node.firstChild;
                while (child) {
                    if (signal?.aborted) {
                        throw new DOMException("Aborted", "AbortError");
                    }
                    if (child.name === "VariableName") {
                        const funcName = code.substring(child.from, child.to);
                        localIdentifiers.add(funcName);
                        break; // Only the first VariableName is the function name
                    }
                    child = child.nextSibling;
                }
                // Still visit children for nested definitions
                visitChildren(node);
                return;
            }
            case "ParamList": {
                // Function parameters
                visitChildren(node);
                return;
            }
            case "VariableName": {
                // Check if this is a parameter or loop variable by looking at parent context
                const parent = node.parent;
                const varName = code.substring(node.from, node.to);
                if (parent) {
                    if (parent.name === "ParamList" || parent.name === "Param") {
                        localIdentifiers.add(varName);
                    }
                    // For ForStatement, only the loop variable (before 'in') should be local
                    if (parent.name === "ForStatement") {
                        // Check if this VariableName comes before the 'in' keyword
                        let nextSibling = node.nextSibling;
                        while (nextSibling) {
                            if (signal?.aborted) {
                                throw new DOMException("Aborted", "AbortError");
                            }
                            if (nextSibling.name === "in") {
                                // This VariableName is before 'in', so it's the loop variable
                                localIdentifiers.add(varName);
                                break;
                            }
                            nextSibling = nextSibling.nextSibling;
                        }
                    }
                    // For ComprehensionFor, similar logic applies
                    if (parent.name === "ComprehensionFor") {
                        // Check if this VariableName comes before the 'in' keyword
                        let nextSibling = node.nextSibling;
                        while (nextSibling) {
                            if (signal?.aborted) {
                                throw new DOMException("Aborted", "AbortError");
                            }
                            if (nextSibling.name === "in") {
                                // This VariableName is before 'in', so it's the loop variable
                                localIdentifiers.add(varName);
                                break;
                            }
                            nextSibling = nextSibling.nextSibling;
                        }
                    }
                    // Handle comprehension loop variables
                    // In comprehensions: [item for item in list], {key: val for key in dict}
                    // The loop variable appears after 'for' and before 'in'
                    if (parent.name === "ArrayComprehensionExpression" ||
                        parent.name === "DictionaryComprehensionExpression" ||
                        parent.name === "SetComprehensionExpression") {
                        // Check if this VariableName comes after a 'for' keyword
                        let prevSibling = node.prevSibling;
                        let foundFor = false;
                        while (prevSibling) {
                            if (signal?.aborted) {
                                throw new DOMException("Aborted", "AbortError");
                            }
                            if (prevSibling.name === "for") {
                                foundFor = true;
                                break;
                            }
                            // Stop if we hit 'in' keyword (we've gone too far)
                            if (prevSibling.name === "in") {
                                break;
                            }
                            prevSibling = prevSibling.prevSibling;
                        }
                        if (foundFor) {
                            // This is a loop variable, add it
                            localIdentifiers.add(varName);
                        }
                    }
                }
                return;
            }
            case "ImportStatement":
            case "ImportFrom": {
                // Import statements
                visitChildren(node);
                return;
            }
        }
        // Visit all children by default
        visitChildren(node);
    }
    function visitChildren(node) {
        let child = node.firstChild;
        while (child) {
            if (signal?.aborted) {
                throw new DOMException("Aborted", "AbortError");
            }
            visit(child);
            child = child.nextSibling;
        }
    }
    visit(node);
    return localIdentifiers;
}
/**
 * Pass 2: Extract root identifiers from member expressions (e.g., foo.bar -> foo)
 */
function extractMemberExpressions(code, node, localIdentifiers, signal) {
    const externalIdentifiers = new Set();
    function visit(node, isInMemberExpression = false) {
        // Check if aborted
        if (signal?.aborted) {
            throw new DOMException("Aborted", "AbortError");
        }
        // Skip import statements - they're handled separately
        if (node.name === "ImportStatement") {
            return;
        }
        // Look for member expressions / attribute access
        if (node.name === "MemberExpression" || node.name === "Attribute") {
            // Find the root identifier (leftmost VariableName)
            const rootIdentifier = findRootIdentifier(node, code, signal);
            if (rootIdentifier && !localIdentifiers.has(rootIdentifier)) {
                externalIdentifiers.add(rootIdentifier);
            }
            // Don't visit children as we've handled this subtree
            return;
        }
        // Look for simple VariableName that is not part of a member expression
        if (node.name === "VariableName" && !isInMemberExpression) {
            const parent = node.parent;
            if (parent) {
                // Skip if this is the left side of an assignment
                if (parent.name === "AssignStatement") {
                    const assignOp = Array.from({ length: parent.childCount })
                        .map((_, i) => parent.child(i))
                        .find((child) => child?.name === "AssignOp");
                    if (assignOp && node.from < assignOp.from) {
                        // This VariableName is before the assignment operator, skip it
                        return;
                    }
                }
                // Skip if this is a function name being called
                // In CallExpression, the function name comes before the ArgList
                if (parent.name === "CallExpression") {
                    // Check if this VariableName is before the ArgList (i.e., it's the function name)
                    let nextSibling = node.nextSibling;
                    while (nextSibling) {
                        if (signal?.aborted) {
                            throw new DOMException("Aborted", "AbortError");
                        }
                        if (nextSibling.name === "ArgList") {
                            // This VariableName is before ArgList, so it's the function being called
                            return;
                        }
                        nextSibling = nextSibling.nextSibling;
                    }
                }
                // Skip if this is a keyword argument name (e.g., func(key=value))
                // The structure is: ArgList > AssignOp with VariableName before =
                if (parent.name === "ArgList") {
                    // Check if the next sibling is AssignOp (=)
                    const nextSibling = node.nextSibling;
                    if (nextSibling && nextSibling.name === "AssignOp") {
                        // This is a keyword argument name, skip it
                        return;
                    }
                }
            }
            const varName = code.substring(node.from, node.to);
            if (varName && !localIdentifiers.has(varName)) {
                externalIdentifiers.add(varName);
            }
            return;
        }
        // Visit all children, marking if we're inside a member expression
        const inMemberExpr = isInMemberExpression ||
            node.name === "MemberExpression" ||
            node.name === "Attribute";
        let child = node.firstChild;
        while (child) {
            if (signal?.aborted) {
                throw new DOMException("Aborted", "AbortError");
            }
            visit(child, inMemberExpr);
            child = child.nextSibling;
        }
    }
    visit(node);
    return externalIdentifiers;
}
/**
 * Find the root identifier in a member expression
 * For example: foo.bar.baz -> foo
 */
function findRootIdentifier(node, code, signal) {
    let current = node;
    // Traverse down to find the leftmost identifier
    while (current) {
        if (signal?.aborted) {
            throw new DOMException("Aborted", "AbortError");
        }
        if (current.name === "VariableName") {
            return code.substring(current.from, current.to);
        }
        // Look for the object part of the member expression
        let child = current.firstChild;
        let foundRelevantChild = false;
        while (child) {
            if (signal?.aborted) {
                throw new DOMException("Aborted", "AbortError");
            }
            if (child.name === "VariableName" ||
                child.name === "MemberExpression" ||
                child.name === "Attribute") {
                current = child;
                foundRelevantChild = true;
                break;
            }
            child = child.nextSibling;
        }
        // If we didn't find a relevant child, break
        if (!foundRelevantChild) {
            break;
        }
    }
    return null;
}
const extractPyEvaluationPairsWithTokenizer = (pythonSnippet, parser) => {
    // Parsing always succeeds, but does not imply that the grammar is valid.
    // Invalid tokens use the type ⚠, but does not affect the entity extraction
    const tree = parser.parse(pythonSnippet);
    const cursor = tree.cursor();
    let isCapturing = false;
    const bindingPairs = [];
    let currentEntityPath = [];
    let bindingStart = 0;
    let bindingEnd = 0;
    // let currentNamespace;
    const previousCursorNamesAndValues = [];
    const removeTrailingPunctuation = () => {
        if (currentEntityPath[currentEntityPath.length - 1] === "." ||
            currentEntityPath[currentEntityPath.length - 1] === "[") {
            // Don't leave a trailing period if the entity is already done
            currentEntityPath.pop();
        }
    };
    do {
        switch (cursor.name) {
            case "VariableName": {
                const text = pythonSnippet.substring(cursor.from, cursor.to);
                // Ignore function names
                if (previousCursorNamesAndValues.length > 0 &&
                    previousCursorNamesAndValues[previousCursorNamesAndValues.length - 1]
                        ?.name === "def") {
                    break;
                }
                if (!isCapturing) {
                    currentEntityPath.push(text);
                    bindingStart = cursor.from;
                    isCapturing = true;
                }
                break;
            }
            case "PropertyName": {
                const text = pythonSnippet.substring(cursor.from, cursor.to);
                if (isCapturing) {
                    currentEntityPath.push(text);
                    break;
                }
                else {
                    isCapturing = true;
                    bindingStart = cursor.from;
                    currentEntityPath.push(text);
                }
                break;
            }
            case ".": {
                if (isCapturing) {
                    currentEntityPath.push(cursor.name);
                }
                break;
            }
            case "]": {
                // If we hit a ] and are capturing, check if we are looking at
                // a literal array index (i.e. [0]) or a property value (i.e ["property"]).
                // If so, try to extract the value
                if (isCapturing &&
                    previousCursorNamesAndValues.length === 2 &&
                    previousCursorNamesAndValues[0]?.name === "[") {
                    if (previousCursorNamesAndValues[1]?.name === "Number") {
                        const indexValue = Number(previousCursorNamesAndValues[1].value);
                        currentEntityPath.push(`[${indexValue}]`);
                    }
                    // Ignore text properties, (i.e `foo['bar']` should return `foo`)
                    if (previousCursorNamesAndValues[1]?.name === "String") {
                        bindingPairs.push({
                            binding: currentEntityPath.join(""),
                            range: {
                                start: bindingStart,
                                end: previousCursorNamesAndValues[0].cursorTo - 1,
                            },
                        });
                        currentEntityPath = [];
                        isCapturing = false;
                        break;
                    }
                }
                break;
            }
            case "[":
            case "String":
            case "Number": {
                // want to keep capturing if we are in the middle of capturing, in order to handle literal array indexing
                break;
            }
            case "AssignOp": {
                // Ignore variable assignments
                currentEntityPath = [];
                isCapturing = false;
                break;
            }
            case "ArgList": {
                const bindingEnd = previousCursorNamesAndValues[previousCursorNamesAndValues.length - 1]
                    ?.cursorTo;
                if (!bindingEnd)
                    break;
                // Save previous binding values and stop capturing.
                bindingPairs.push({
                    binding: currentEntityPath.join(""),
                    range: {
                        start: bindingStart,
                        end: bindingEnd,
                    },
                });
                currentEntityPath = [];
                isCapturing = false;
                break;
            }
            default: {
                if (isCapturing) {
                    removeTrailingPunctuation();
                    bindingEnd = cursor.to;
                    bindingPairs.push({
                        binding: currentEntityPath.join(""),
                        range: {
                            start: bindingStart,
                            end: bindingEnd,
                        },
                    });
                    currentEntityPath = [];
                    isCapturing = false;
                }
                break;
            }
        }
        // add the previous cursor name and value to the list. ensure that previous cursor list is only 2 items long
        previousCursorNamesAndValues.push({
            name: cursor.name,
            value: pythonSnippet.substring(cursor.from, cursor.to),
            cursorTo: cursor.to,
        });
        if (previousCursorNamesAndValues.length > 2) {
            previousCursorNamesAndValues.shift();
        }
    } while (cursor.next());
    if (isCapturing) {
        removeTrailingPunctuation();
        bindingEnd = cursor.to;
        bindingPairs.push({
            binding: currentEntityPath.join(""),
            range: {
                start: bindingStart,
                end: bindingEnd,
            },
        });
        isCapturing = false;
    }
    return bindingPairs;
};
export const extractPythonEvaluationPairs = async (pythonSnippet) => {
    return extractPyEvaluationPairsWithTokenizer(pythonSnippet, pythonParser);
};
export const extractPythonEvaluationBindings = async (pythonSnippet) => {
    const evaluationPairs = await extractPythonEvaluationPairs(pythonSnippet);
    return evaluationPairs.map((evalPair) => evalPair.binding);
};
//# sourceMappingURL=python-identifiers.js.map