import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { tokenize } from "esprima";
import { traverse } from "../traverse.js";
const extractJsEvaluationPairsWithTokenizer = (jsSnippet, tokenize) => {
    let tokens = [];
    const ret = [];
    try {
        tokens = tokenize(jsSnippet, { range: true, loc: true });
    }
    catch {
        // Swallow the error and skip tokenization, let the backend vm handle javascript parsing
        return ret;
    }
    let canCapture = true; // set to true when we are capturing the first identifier in a list of a.b.c OR a[b][c]
    for (let i = 0; i < tokens.length; i++) {
        switch (tokens[i]?.type) {
            case "Identifier": {
                const prevLine = tokens[i - 1]?.loc?.end.line;
                const currentLine = tokens[i]?.loc?.end.line;
                if (prevLine && currentLine && prevLine < currentLine) {
                    // Handle automatic semicolon insertion such as when previous token ends with ]
                    canCapture = true;
                }
                const shouldExtract = tokens[i]?.value !== "await" && tokens[i]?.value !== "async";
                //   entitiesToExtract.has(tokens[i].value) ||
                //   (currentNamespace &&
                //     namespacedEntitiesToExtract[currentNamespace].has(tokens[i].value));
                if (canCapture && shouldExtract) {
                    // Skip when the identifier is part of a key: value
                    const nextToken = tokens[i + 1]?.value;
                    if (nextToken === ":" || nextToken === "=" || nextToken === "(")
                        break;
                    // We have found the first token to capture the entity path
                    const output = extractEntity(i, tokens);
                    ret.push(output);
                    // Skip ahead
                    if (output.tokenIndex && output.tokenIndex > i) {
                        // with the ++ increment this makes sure we start at the tokenIndex
                        i = output.tokenIndex - 1;
                    }
                    canCapture = true;
                }
                break;
            }
            case "Punctuator": {
                if (tokens[i]?.value === ".") {
                    canCapture = true;
                    // keep prefix if we see a . accessor
                    break;
                }
                const value = tokens[i]?.value;
                if (!value)
                    break;
                canCapture = ![".", "]"].includes(value);
                break;
            }
            default: {
                canCapture = true;
                break;
            }
        }
    }
    return ret;
};
// TODO: Move test for this into shared.
export const extractEntity = (indexToStart, tokens) => {
    const currentPropertyPath = [];
    let previousToken = null;
    let done = false; // cant do break out of switch without state variable
    let invalidPunctuator = false; // if we encounter an invalid punctuator, we should not add it to the property path
    const removeTrailingPunctuation = () => {
        if (currentPropertyPath[currentPropertyPath.length - 1] === "." ||
            currentPropertyPath[currentPropertyPath.length - 1] === "[") {
            // Special case to handle null.length, such as Input1.value.length
            currentPropertyPath.pop();
        }
    };
    for (let j = indexToStart; j < tokens.length; j++) {
        switch (tokens[j]?.type) {
            case "Identifier": {
                const val = tokens[j]?.value;
                const prevLine = previousToken?.loc?.end.line;
                const currentLine = tokens[j]?.loc?.end.line;
                if (j + 1 <= tokens.length - 1 && tokens[j + 1]?.value === "(") {
                    // Ignore function calls
                    done = true;
                    j -= 2;
                    break;
                }
                if (prevLine && currentLine && prevLine < currentLine) {
                    // Detect newlines that would insert a semicolon automatically
                    done = true;
                    // Update token position to previous- used for return values
                    j--;
                    break;
                }
                if (!val)
                    break;
                currentPropertyPath.push(val);
                break;
            }
            case "Punctuator": {
                const value = tokens[j]?.value;
                if (!value)
                    break;
                if (value === "[" &&
                    tokens.length > j + 1 &&
                    tokens[j + 1] &&
                    !["Numeric", "String"].includes(tokens[j + 1].type)) {
                    // if we see [ and the next token is not a numeric (array access) or a string (property access), break out
                    done = true;
                    break;
                }
                if (value === "]" &&
                    !!previousToken &&
                    !["Numeric", "String"].includes(previousToken.type)) {
                    // if we see ] and the previous token is not a numeric (array access) or a string (property access), break out
                    // in this case, we do not want to add an extra ] to resultant property path
                    done = true;
                    break;
                }
                if (![".", "[", "]"].includes(value)) {
                    // invalid Punctuator
                    invalidPunctuator = true;
                    done = true;
                    break;
                }
                currentPropertyPath.push(value);
                break;
            }
            case "Numeric":
            case "String":
            default:
                done = true;
                break;
        }
        if (!invalidPunctuator) {
            previousToken = tokens[j] ?? null;
        }
        if (done) {
            break;
        }
    }
    removeTrailingPunctuation();
    return {
        binding: currentPropertyPath.join(""),
        range: {
            // @ts-expect-error: range is added to token as a result of tokenization config
            start: tokens[indexToStart].range[0],
            // @ts-expect-error: range is added to token as a result of tokenization config
            end: previousToken.range[1],
        },
        tokenIndex: indexToStart + currentPropertyPath.length,
    };
};
export const extractJsEvaluationPairs = (jsSnippet) => {
    return extractJsEvaluationPairsWithTokenizer(jsSnippet, tokenize);
};
export const extractJsEvaluationBindings = async (jsSnippet) => {
    const evaluationPairs = extractJsEvaluationPairs(jsSnippet);
    return evaluationPairs.map((evalPair) => evalPair.binding);
};
export function extractExternalIdentifiersFromJS(code) {
    try {
        const ast = parse(code, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
            errorRecovery: true,
        });
        const externalIdentifiers = new Set();
        const handleMemberLikeExpression = (path) => {
            // For member expressions like `foo.bar` or `foo?.bar`, extract the root identifier
            let current = path.node;
            while (t.isMemberExpression(current.object) ||
                t.isOptionalMemberExpression(current.object)) {
                current = current.object;
            }
            if (t.isIdentifier(current.object)) {
                const rootName = current.object.name;
                const binding = path.scope.getBinding(rootName);
                // Only add if it's not declared locally
                if (!binding) {
                    externalIdentifiers.add(rootName);
                }
            }
        };
        const handleIdentifier = (path) => {
            // skip property key in an object
            if (path.parent.type === "ObjectProperty" &&
                path.parent.key === path.node) {
                return;
            }
            // skip expressions, we get those in other branch
            if ((path.parent.type === "MemberExpression" ||
                path.parent.type === "OptionalMemberExpression") &&
                path.parent.property === path.node) {
                return;
            }
            // functions can't be inputs
            if (path.parent.type === "CallExpression" &&
                path.parent.callee === path.node) {
                return;
            }
            // declarations are local (variable, function, class names)
            if ((path.parent.type === "VariableDeclarator" &&
                path.parent.id === path.node) ||
                (path.parent.type === "FunctionDeclaration" &&
                    path.parent.id === path.node) ||
                (path.parent.type === "ClassDeclaration" &&
                    path.parent.id === path.node) ||
                path.parent.type === "ImportSpecifier" ||
                path.parent.type === "ImportDefaultSpecifier" ||
                path.parent.type === "ImportNamespaceSpecifier") {
                return;
            }
            // only add if it's not declared locally
            const binding = path.scope.getBinding(path.node.name);
            if (!binding) {
                externalIdentifiers.add(path.node.name);
            }
        };
        traverse(ast, {
            MemberExpression(path) {
                handleMemberLikeExpression(path);
            },
            OptionalMemberExpression(path) {
                handleMemberLikeExpression(path);
            },
            Identifier(path) {
                handleIdentifier(path);
            },
        });
        return Array.from(externalIdentifiers);
    }
    catch (_error) {
        // If parsing fails, fall back to empty array
        return [];
    }
}
/**
 * Extract external identifiers with their type annotations from JavaScript/TypeScript code.
 * Looks for destructured parameters with TypeScript type annotations like:
 *   fn: ({ param }: { param: string }) => ...
 *
 * @param code - JavaScript/TypeScript source code
 * @returns Array of identifiers with their type information, or undefined if types can't be extracted
 */
export function extractExternalIdentifiersWithTypesFromJS(code) {
    try {
        const ast = parse(code, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
            errorRecovery: true,
        });
        const identifiersWithTypes = new Map();
        traverse(ast, {
            // Look for arrow functions and function expressions with destructured params
            "ArrowFunctionExpression|FunctionExpression"(path) {
                // Type guard to ensure we have a function with params
                if (!t.isArrowFunctionExpression(path.node) &&
                    !t.isFunctionExpression(path.node)) {
                    return;
                }
                const params = path.node.params;
                if (params.length === 0)
                    return;
                // We're looking for the first parameter which should be an ObjectPattern
                const firstParam = params[0];
                if (!t.isObjectPattern(firstParam))
                    return;
                // Check if it has a type annotation
                const typeAnnotation = firstParam.typeAnnotation;
                if (!typeAnnotation || !t.isTSTypeAnnotation(typeAnnotation))
                    return;
                // The type annotation should be a TSTypeLiteral (object type)
                const tsType = typeAnnotation.typeAnnotation;
                if (!t.isTSTypeLiteral(tsType))
                    return;
                // Extract type information from each property
                for (const member of tsType.members) {
                    if (!t.isTSPropertySignature(member))
                        continue;
                    const key = member.key;
                    if (!t.isIdentifier(key))
                        continue;
                    const paramName = key.name;
                    const isOptional = member.optional ?? false;
                    // Extract the type as a string
                    let typeString = "any";
                    if (member.typeAnnotation &&
                        t.isTSTypeAnnotation(member.typeAnnotation)) {
                        typeString = extractTypeString(member.typeAnnotation.typeAnnotation);
                    }
                    identifiersWithTypes.set(paramName, {
                        name: paramName,
                        type: typeString,
                        optional: isOptional,
                    });
                }
            },
        });
        return identifiersWithTypes.size > 0
            ? Array.from(identifiersWithTypes.values())
            : undefined;
    }
    catch (_error) {
        // If parsing fails or types can't be extracted, return undefined
        return undefined;
    }
}
/**
 * Convert a TypeScript AST type node to a string representation.
 * Handles common types like primitives, arrays, unions, objects, etc.
 */
function extractTypeString(typeNode) {
    try {
        // Primitive types
        if (t.isTSStringKeyword(typeNode))
            return "string";
        if (t.isTSNumberKeyword(typeNode))
            return "number";
        if (t.isTSBooleanKeyword(typeNode))
            return "boolean";
        if (t.isTSAnyKeyword(typeNode))
            return "any";
        if (t.isTSUnknownKeyword(typeNode))
            return "unknown";
        if (t.isTSNullKeyword(typeNode))
            return "null";
        if (t.isTSUndefinedKeyword(typeNode))
            return "undefined";
        if (t.isTSVoidKeyword(typeNode))
            return "void";
        if (t.isTSNeverKeyword(typeNode))
            return "never";
        // Literal types
        if (t.isTSLiteralType(typeNode)) {
            const literal = typeNode.literal;
            if (t.isStringLiteral(literal))
                return `"${literal.value}"`;
            if (t.isNumericLiteral(literal))
                return literal.value.toString();
            if (t.isBooleanLiteral(literal))
                return literal.value.toString();
        }
        // Array types
        if (t.isTSArrayType(typeNode)) {
            const elementType = extractTypeString(typeNode.elementType);
            return `${elementType}[]`;
        }
        // Union types (e.g., string | number)
        if (t.isTSUnionType(typeNode)) {
            const types = typeNode.types.map(extractTypeString);
            return types.join(" | ");
        }
        // Intersection types (e.g., A & B)
        if (t.isTSIntersectionType(typeNode)) {
            const types = typeNode.types.map(extractTypeString);
            return types.join(" & ");
        }
        // Type references (e.g., Array<T>, Record<K, V>)
        if (t.isTSTypeReference(typeNode)) {
            const typeName = t.isIdentifier(typeNode.typeName)
                ? typeNode.typeName.name
                : "unknown";
            if (typeNode.typeParameters) {
                const params = typeNode.typeParameters.params.map(extractTypeString);
                return `${typeName}<${params.join(", ")}>`;
            }
            return typeName;
        }
        // Object types (e.g., { id: number; name: string })
        if (t.isTSTypeLiteral(typeNode)) {
            const members = typeNode.members
                .map((member) => {
                if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
                    const optional = member.optional ? "?" : "";
                    const typeStr = member.typeAnnotation &&
                        t.isTSTypeAnnotation(member.typeAnnotation)
                        ? extractTypeString(member.typeAnnotation.typeAnnotation)
                        : "any";
                    return `${member.key.name}${optional}: ${typeStr}`;
                }
                return null;
            })
                .filter(Boolean);
            if (members.length === 0)
                return "{}";
            return `{ ${members.join("; ")} }`;
        }
        // Tuple types
        if (t.isTSTupleType(typeNode)) {
            const elements = typeNode.elementTypes.map((el) => {
                if (t.isTSType(el)) {
                    return extractTypeString(el);
                }
                return "any";
            });
            return `[${elements.join(", ")}]`;
        }
        // Fallback to "any" for complex or unsupported types
        return "any";
    }
    catch (_error) {
        return "any";
    }
}
//# sourceMappingURL=js-identifiers.js.map