import { splitJSPath } from "@superblocksteam/library-shared";
import { ActionType, BlockType, convertBlock, isControlBlock, isStepBlock, LanguagePluginID, } from "@superblocksteam/shared";
import { getControlBlockExpressions } from "./extract-control-block-identifiers.js";
import { getDynamicBindings } from "./extract-embedded-identifiers.js";
import { extractExternalIdentifiersFromJS, extractExternalIdentifiersWithTypesFromJS, extractJsEvaluationBindings, } from "./js-identifiers.js";
import { extractExternalIdentifiersFromPython, extractPythonEvaluationBindings, } from "./python-identifiers.js";
export async function extractBindingsFromValue(value, pluginID) {
    if (value?.toString) {
        const stringValue = value.toString();
        if (pluginID === LanguagePluginID.JavaScript) {
            return await extractJsEvaluationBindings(stringValue);
        }
        else if (pluginID === LanguagePluginID.Python) {
            return await extractPythonEvaluationBindings(stringValue);
        }
        // Other non-JS steps with embedded JS
        const { jsSnippets } = getDynamicBindings(stringValue);
        return (await Promise.all(jsSnippets.map((jsSnippet) => extractJsEvaluationBindings(jsSnippet)))).flat();
    }
    return [];
}
const isPropertyDto = (value) => {
    return value && value.key && value.value;
};
export function recursivelyExtractValues(obj) {
    const res = Object.entries(obj).map((entry) => {
        if (typeof entry[1] === "object" || Array.isArray(entry[1])) {
            return recursivelyExtractValues(entry[1]);
        }
        return entry[1];
    });
    return res.flat();
}
export async function extractBindingsFromAction(action) {
    const allBindings = [];
    // Special case: 'parameters' field is pure JS (e.g., "[productName, category]")
    // Check top-level, one level nested (e.g., postgres.parameters), and two levels nested (e.g., databricks.runSql.parameters)
    const findParametersValue = (config) => {
        if (!config || typeof config !== "object")
            return undefined;
        // Direct parameters field
        if (config.parameters)
            return config.parameters;
        // Check one level deep (e.g., postgres.parameters, snowflake.parameters)
        for (const value of Object.values(config)) {
            if (typeof value === "object" && value !== null) {
                const nested = value;
                if (nested.parameters)
                    return nested.parameters;
                // Check two levels deep (e.g., databricks.runSql.parameters, athena.runSql.parameters)
                if (nested.runSql && typeof nested.runSql === "object") {
                    const runSql = nested.runSql;
                    if (runSql.parameters)
                        return runSql.parameters;
                }
            }
        }
        return undefined;
    };
    const rawParametersValue = findParametersValue(action.configuration);
    // Strip surrounding backticks if present (UI sends values wrapped in backticks)
    const parametersValue = typeof rawParametersValue === "string"
        ? rawParametersValue.replace(/^`|`$/g, "")
        : rawParametersValue;
    if (parametersValue && typeof parametersValue === "string") {
        const extracted = extractExternalIdentifiersFromJS(parametersValue);
        allBindings.push(Promise.resolve(extracted));
    }
    // recursively extract from all other configuration values
    const configBindings = Object.values(action.configuration).flatMap((value) => {
        const extracted = typeof value === "object" || Array.isArray(value)
            ? recursivelyExtractValues(value)
            : [value];
        const bindings = [];
        for (const toExtract of extracted) {
            if (isPropertyDto(toExtract)) {
                bindings.push(extractBindingsFromValue(toExtract.key, action.pluginId));
                bindings.push(extractBindingsFromValue(toExtract.value, action.pluginId));
            }
            else {
                bindings.push(extractBindingsFromValue(toExtract, action.pluginId));
            }
        }
        return bindings;
    });
    allBindings.push(...configBindings);
    return (await Promise.all(allBindings)).flat();
}
const getLanguageConfiguration = (block) => {
    switch (block.config.integration) {
        case LanguagePluginID.JavaScript:
            return {
                pluginId: LanguagePluginID.JavaScript,
                configuration: block.config.javascript,
            };
        case LanguagePluginID.Python:
            return {
                pluginId: LanguagePluginID.Python,
                configuration: block.config.python,
            };
        default:
            return {
                pluginId: "other",
                configuration: block.config,
            };
    }
};
const computeStepBlockBindings = async (block) => {
    const { integration } = block.config;
    if (integration === LanguagePluginID.JavaScript) {
        const javascript = block.config.javascript;
        if (javascript?.body) {
            return extractExternalIdentifiersFromJS(javascript.body);
        }
        return [];
    }
    if (integration === LanguagePluginID.Python) {
        const python = block.config.python;
        if (python?.body) {
            return extractExternalIdentifiersFromPython(python.body);
        }
        return [];
    }
    // For other blocks, use the existing extraction logic
    const { name } = block.config;
    const config = {
        type: ActionType.Integration,
        name,
        id: name,
        datasourceId: integration,
        ...getLanguageConfiguration(block),
    };
    return await extractBindingsFromAction(config);
};
async function computeControlBlockBindings(block) {
    const expressions = getControlBlockExpressions(block);
    const bindings = (await Promise.all(expressions.map(async (expression) => {
        const bindings = await extractBindingsFromValue(expression, LanguagePluginID.JavaScript);
        return bindings;
    }))).flat();
    switch (block.type) {
        case BlockType.CONDITION: {
            const config = block.config;
            const ifBindings = await Promise.all(config.if?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            const elseIfBindings = await Promise.all(config.elseIf?.flatMap((elseIf) => elseIf.blocks?.map((block) => computeBlockBindings(block)) ?? []) ?? []);
            const elseBindings = await Promise.all(config.else?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            return [
                ...bindings,
                ...ifBindings.flat(),
                ...elseIfBindings.flat(),
                ...elseBindings.flat(),
            ];
        }
        case BlockType.LOOP: {
            const config = block.config;
            const loopBindings = await Promise.all(config.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            return [...bindings, ...loopBindings.flat()];
        }
        case BlockType.PARALLEL: {
            const config = block.config;
            const parallelBindings = [];
            if (config.dynamic?.blocks) {
                const dynamicBindings = await Promise.all(config.dynamic.blocks.map((block) => computeBlockBindings(block)));
                parallelBindings.push(...dynamicBindings.flat());
            }
            return [...bindings, ...parallelBindings];
        }
        case BlockType.TRY_CATCH: {
            const config = block.config;
            const tryBindings = await Promise.all(config.try?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            const catchBindings = await Promise.all(config.catch?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            const finallyBindings = await Promise.all(config.finally?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
            return [
                ...bindings,
                ...tryBindings.flat(),
                ...catchBindings.flat(),
                ...finallyBindings.flat(),
            ];
        }
        case BlockType.STREAM: {
            const config = block.config;
            const streamBindings = [];
            if (config.trigger) {
                const triggerBindings = await computeBlockBindings(config.trigger);
                streamBindings.push(...triggerBindings.flat());
            }
            if (config.process) {
                const processBindings = await Promise.all(config.process?.blocks?.map((block) => computeBlockBindings(block)) ?? []);
                streamBindings.push(...processBindings.flat());
            }
            return [...bindings, ...streamBindings];
        }
        case BlockType.BREAK:
        case BlockType.RETURN:
        case BlockType.WAIT:
        case BlockType.THROW:
        case BlockType.VARIABLES:
        case BlockType.SEND:
            // These blocks don't have nested blocks, only expressions which are already handled by bindings
            return bindings;
    }
}
const computeBlockBindings = async (baseBlock) => {
    const block = convertBlock(baseBlock);
    if (block == null) {
        return [];
    }
    if (isStepBlock(block)) {
        return await computeStepBlockBindings(block);
    }
    else if (isControlBlock(block)) {
        return await computeControlBlockBindings(block);
    }
    return [];
};
const NODEJS_GLOBALS = new Set([
    "process",
    "global",
    "Buffer",
    "console",
    "require",
    "module",
    "exports",
    "__dirname",
    "__filename",
    "setTimeout",
    "setInterval",
    "clearTimeout",
    "clearInterval",
    "setImmediate",
    "clearImmediate",
    "URL",
    "URLSearchParams",
    "TextEncoder",
    "TextDecoder",
    "AbortController",
    "AbortSignal",
    "EventEmitter",
    "globalThis",
]);
const SB_GLOBAL = new Set(["Global"]);
export async function extractInputsFromApi(api) {
    const blocks = api.apiPb.blocks;
    // Collect all step names from the API
    const stepNames = new Set();
    const collectStepNames = (baseBlock) => {
        // Name is on the base block before conversion
        if (baseBlock.name) {
            stepNames.add(baseBlock.name);
        }
        // Recursively collect step names from nested blocks
        const block = convertBlock(baseBlock);
        if (block && isControlBlock(block)) {
            switch (block.type) {
                case BlockType.CONDITION: {
                    const config = block.config;
                    config.if?.blocks?.forEach(collectStepNames);
                    config.elseIf?.forEach((elseIf) => elseIf.blocks?.forEach(collectStepNames));
                    config.else?.blocks?.forEach(collectStepNames);
                    break;
                }
                case BlockType.LOOP: {
                    const config = block.config;
                    config.blocks?.forEach(collectStepNames);
                    break;
                }
                case BlockType.PARALLEL: {
                    const config = block.config;
                    config.dynamic?.blocks?.forEach(collectStepNames);
                    break;
                }
                case BlockType.TRY_CATCH: {
                    const config = block.config;
                    config.try?.blocks?.forEach(collectStepNames);
                    config.catch?.blocks?.forEach(collectStepNames);
                    config.finally?.blocks?.forEach(collectStepNames);
                    break;
                }
                case BlockType.STREAM: {
                    const config = block.config;
                    if (config.trigger) {
                        collectStepNames(config.trigger);
                    }
                    config.process?.blocks?.forEach(collectStepNames);
                    break;
                }
            }
        }
    };
    blocks?.forEach(collectStepNames);
    // Extract all bindings
    const results = await Promise.all(blocks?.map(async (baseBlock) => {
        return await computeBlockBindings(baseBlock);
    }) ?? []);
    const allBindings = results.flat();
    // Extract root identifiers and filter out step names and other globals
    const rootIdentifiers = new Set();
    allBindings.forEach((binding) => {
        const splitPath = splitJSPath(binding)?.filter((path) => !!path) ?? [];
        if (splitPath.length > 0) {
            const rootIdentifier = splitPath[0];
            if (rootIdentifier &&
                !stepNames.has(rootIdentifier) &&
                !NODEJS_GLOBALS.has(rootIdentifier) &&
                !SB_GLOBAL.has(rootIdentifier) &&
                !(rootIdentifier in globalThis)) {
                rootIdentifiers.add(rootIdentifier);
            }
        }
    });
    return Array.from(rootIdentifiers).sort();
}
/**
 * Extract API inputs with their TypeScript type annotations.
 *
 * This function attempts to extract type information from TypeScript type annotations
 * in the API source code. If type annotations are found, it returns IdentifierWithType[],
 * otherwise it falls back to string[] with "any" types.
 *
 * @param api - The API to extract inputs from
 * @param apiSource - Optional API source code (TypeScript). If not provided, types will be "any"
 * @returns Array of inputs with type information
 */
export async function extractInputsWithTypesFromApi(api, apiSource) {
    // First, get the list of input names using the existing function
    const inputNames = await extractInputsFromApi(api);
    // If no API source is provided, return with "any" types
    if (!apiSource) {
        return inputNames.map((name) => ({
            name,
            type: "any",
            optional: false,
        }));
    }
    // Try to extract types from the TypeScript source
    const typedIdentifiers = extractExternalIdentifiersWithTypesFromJS(apiSource);
    // If we successfully extracted types, use them
    if (typedIdentifiers) {
        // Create a map of typed identifiers for quick lookup
        const typeMap = new Map(typedIdentifiers.map((id) => [id.name, id]));
        // For each input name, use the type if available, otherwise default to "any"
        return inputNames.map((name) => {
            const typedInfo = typeMap.get(name);
            return (typedInfo || {
                name,
                type: "any",
                optional: false,
            });
        });
    }
    // Fallback: no types found, use "any" for everything
    return inputNames.map((name) => ({
        name,
        type: "any",
        optional: false,
    }));
}
//# sourceMappingURL=extract-identifiers.js.map