import { exec, spawn } from "node:child_process";
import { promisify } from "node:util";
import { getLogger } from "../../../../util/logger.js";
const execAsync = promisify(exec);
/**
 * Search a gron file using ripgrep.
 * Returns raw lines from the gron file that match the pattern.
 */
export async function searchGronWithRipgrep(gronPath, pattern) {
    const logger = getLogger();
    try {
        // Use ripgrep with:
        // -i: case-insensitive search (critical for Snowflake UPPERCASE identifiers)
        // --no-heading: don't group by file
        // --no-line-number: don't show line numbers
        // NOTE: We don't use -m (match limit) here! We want ALL matches so we can
        // intelligently extract unique parent objects and then trim to the limit.
        // This prevents broad patterns from hitting the limit and missing results.
        const { stdout } = await execAsync(`rg -i --no-heading --no-line-number '${pattern.replace(/'/g, "'\\''")}' '${gronPath}'`, { maxBuffer: 50 * 1024 * 1024 });
        const lines = stdout
            .trim()
            .split("\n")
            .filter((line) => line.length > 0);
        logger.debug(`[ripgrep] Found ${lines.length} matches in ${gronPath} for pattern: ${pattern}`);
        // Warn if we're getting a huge number of matches (might indicate pattern is too broad)
        if (lines.length > 10000) {
            logger.warn(`[ripgrep] Pattern "${pattern}" matched ${lines.length} lines - consider using a more specific pattern`);
        }
        return lines;
    }
    catch (err) {
        if (err.code === 1) {
            // ripgrep exit code 1 = no matches (not an error)
            logger.debug(`[ripgrep] No matches in ${gronPath} for pattern: ${pattern}`);
            return [];
        }
        if (err.code === 127) {
            throw new Error("ripgrep (rg) not found. Please install ripgrep: https://github.com/BurntSushi/ripgrep");
        }
        throw new Error(`ripgrep search failed: ${err.message}`);
    }
}
/**
 * Strip quotes from a gron key.
 * Example: '"/users"' -> '/users'
 */
function stripQuotes(key) {
    if (key.startsWith('"') && key.endsWith('"')) {
        // Unescape the content between quotes
        return key.slice(1, -1).replace(/\\(.)/g, "$1");
    }
    return key;
}
/**
 * Extract unique parent prefixes AND track which indices/keys matched.
 * Example: 'json.dbSchema.tables[3].columns[5].name = "email";'
 * Returns: { prefixes: ['json.dbSchema.tables[3].'], matched: { tables: [3], columns: {3: [5]} } }
 *
 * Example: 'json.openApiSpec.paths["/users"].get.summary = "..."'
 * Returns: { prefixes: ['json.openApiSpec.paths["/users"].'], matched: { paths: ["/users"] } }
 *
 * The prefix is the path to the parent container (table, schema, type, etc.)
 * so we can fetch ALL fields of that parent in one go.
 */
function extractParentPrefixes(matches) {
    const logger = getLogger();
    const prefixes = new Set();
    const matched = {
        tables: new Set(),
        columns: new Map(),
        schemas: new Set(),
        paths: new Set(),
        operations: new Set(),
        types: new Set(),
        fields: new Map(),
        rawValues: [],
    };
    // Debug: log first few matches
    if (matches.length > 0) {
        logger.debug(`[prefix-extract] Sample matches (first 3): ${matches
            .slice(0, 3)
            .map((m) => m.substring(0, 100))
            .join(" | ")}`);
    }
    for (const match of matches) {
        // Extract table matches: json.{namespace}.tables[N]
        const tableMatch = match.match(/json\.(?:schema|dbSchema|databaseSchemaMetadata)\.tables\[(\d+)\]/);
        if (tableMatch?.[1]) {
            const tableIdx = parseInt(tableMatch[1], 10);
            matched.tables.add(tableIdx);
            // Check if it's a column match: json.{namespace}.tables[N].columns[M]
            const columnMatch = match.match(/json\.(?:schema|dbSchema|databaseSchemaMetadata)\.tables\[(\d+)\]\.columns\[(\d+)\]/);
            if (columnMatch?.[2]) {
                const colIdx = parseInt(columnMatch[2], 10);
                if (!matched.columns.has(tableIdx)) {
                    matched.columns.set(tableIdx, new Set());
                }
                matched.columns.get(tableIdx).add(colIdx);
            }
        }
        // Extract schema matches: json.dbSchema.schemas[N]
        const schemaMatch = match.match(/json\.dbSchema\.schemas\[(\d+)\]/);
        if (schemaMatch?.[1]) {
            matched.schemas.add(parseInt(schemaMatch[1], 10));
        }
        // Extract path matches: json.openApiSpec.paths["..."] or json.openApiSpec.paths[N]
        // OpenAPI uses string keys like ["/users"], not numeric indices
        const pathMatch = match.match(/json\.openApiSpec\.paths\[((?:"(?:[^"\\]|\\.)+"|[0-9]+))\]/);
        if (pathMatch?.[1]) {
            const pathKey = stripQuotes(pathMatch[1]);
            matched.paths.add(pathKey);
        }
        // Extract operation matches: json.openApiSpec.operations["..."] or json.openApiSpec.operations[N]
        const opMatch = match.match(/json\.openApiSpec\.operations\[((?:"(?:[^"\\]|\\.)+"|[0-9]+))\]/);
        if (opMatch?.[1]) {
            const opKey = stripQuotes(opMatch[1]);
            matched.operations.add(opKey);
        }
        // Extract type matches: json.graphql.data.__schema.types[N]
        const typeMatch = match.match(/json\.graphql\.data\.__schema\.types\[(\d+)\]/);
        if (typeMatch?.[1]) {
            matched.types.add(parseInt(typeMatch[1], 10));
        }
        // Extract field matches: json.graphql.data.__schema.types[N].fields[M]
        // Also handle inputFields for INPUT_OBJECT types
        const fieldMatch = match.match(/json\.graphql\.data\.__schema\.types\[(\d+)\]\.(fields|inputFields)\[(\d+)\]/);
        if (fieldMatch?.[1] && fieldMatch?.[3]) {
            const typeIdx = parseInt(fieldMatch[1], 10);
            const fieldIdx = parseInt(fieldMatch[3], 10);
            if (!matched.fields.has(typeIdx)) {
                matched.fields.set(typeIdx, new Set());
            }
            matched.fields.get(typeIdx).add(fieldIdx);
        }
        // Note: Queries and mutations are not separate arrays in the standard GraphQL introspection format.
        // They are found by:
        // 1. Getting the type name from json.graphql.data.__schema.queryType.name (or mutationType.name)
        // 2. Finding that type in the types[] array
        // 3. Accessing that type's fields[] array
        // This is handled in extractResultsFromJSON by looking at the Query/Mutation type's fields.
        // Extract the parent container prefix
        // Pattern: json.{namespace}.{container}[{index_or_key}].
        // Examples:
        //   json.dbSchema.tables[3].columns[5].name → json.dbSchema.tables[3].
        //   json.openApiSpec.paths["/users"].get → json.openApiSpec.paths["/users"].
        //   json.graphqlSchema.types[4].fields[1].name → json.graphqlSchema.types[4].
        // Match patterns like: json.anything[number_or_"string"].
        // This matches both [123] and ["/users"]
        const match1 = match.match(/^(json\.[^[]+\[(?:\d+|"(?:[^"\\]|\\.)+")]\.)/);
        if (match1?.[1]) {
            prefixes.add(match1[1]);
            continue;
        }
        // For nested structures (columns inside tables, fields inside types)
        // We want the grandparent (the table, not the column array)
        // json.dbSchema.tables[3].columns[5].name → json.dbSchema.tables[3].
        const match2 = match.match(/^(json\.[^[]+\[(?:\d+|"(?:[^"\\]|\\.)+")]\.[^[]+)\[(?:\d+|"(?:[^"\\]|\\.)+")]\./);
        if (match2?.[1]) {
            // Extract up to the first array: json.dbSchema.tables[3].
            const grandparent = match2[1].match(/^(json\.[^[]+\[(?:\d+|"(?:[^"\\]|\\.)+")]\.)/)?.[1];
            if (grandparent) {
                prefixes.add(grandparent);
                continue;
            }
        }
        // Fallback: Parse unstructured gron lines as raw values
        // This handles non-array paths like json.openApiSpec.info.description = "..."
        // Format: json.path.to.field = value;
        const rawValueMatch = match.match(/^(json\.[^=]+)\s*=\s*(.+);?$/);
        if (rawValueMatch?.[1] && rawValueMatch?.[2]) {
            const jsonPath = rawValueMatch[1].replace(/^json\./, "").trim();
            const rawValue = rawValueMatch[2].trim().replace(/;$/, "");
            // Try to parse the value (could be string, number, boolean, null)
            let parsedValue;
            try {
                parsedValue = JSON.parse(rawValue);
            }
            catch {
                // If JSON.parse fails, treat it as a raw string (shouldn't happen with valid gron)
                parsedValue = rawValue;
            }
            // Only add if it's a simple value (not an object/array marker like {} or [])
            if (typeof parsedValue === "string" ||
                typeof parsedValue === "number" ||
                typeof parsedValue === "boolean" ||
                parsedValue === null) {
                matched.rawValues.push({ path: jsonPath, value: parsedValue });
            }
        }
    }
    // Log info about raw values found (useful for debugging)
    if (matched.rawValues.length > 0) {
        logger.debug(`[prefix-extract] Found ${matched.rawValues.length} raw value matches`);
    }
    // Warn if we found ripgrep matches but extracted no prefixes AND no raw values
    if (matches.length > 0 &&
        prefixes.size === 0 &&
        matched.rawValues.length === 0) {
        logger.warn(`[prefix-extract] Found ${matches.length} ripgrep matches but extracted 0 prefixes and 0 raw values. ` +
            `This likely means the pattern doesn't match expected gron format.`);
    }
    return { prefixes, matched };
}
/**
 * Fetch and ungron parent objects in batched calls.
 * Given prefixes like ['json.dbSchema.tables[3].', 'json.dbSchema.tables[12].'],
 * fetches gron lines in batches and ungrons them.
 *
 * Returns: { json: reconstructed JSON, processedCount: number of prefixes processed }
 */
async function ungronMultiplePrefixes(gronPath, prefixes, startIndex = 0, batchSize = 250) {
    const logger = getLogger();
    if (prefixes.size === 0 || startIndex >= prefixes.size) {
        return { json: {}, processedCount: 0 };
    }
    const prefixArray = Array.from(prefixes);
    const endIndex = Math.min(startIndex + batchSize, prefixArray.length);
    const batch = prefixArray.slice(startIndex, endIndex);
    logger.debug(`[ungron-batch] Processing batch ${Math.floor(startIndex / batchSize) + 1}: ` +
        `prefixes ${startIndex}-${endIndex - 1} of ${prefixArray.length} ` +
        `(${batch.length} prefixes)`);
    try {
        // Build a regex that matches any of the prefixes in this batch
        // ^json\.dbSchema\.tables\[3\]\.|^json\.dbSchema\.tables\[12\]\.
        // Need to escape ALL regex metacharacters in the prefix
        const escapeRegex = (str) => str.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
        const escapedPrefixes = batch.map((p) => `^${escapeRegex(p)}`);
        const prefixRegex = escapedPrefixes.join("|");
        // ONE ripgrep call to get all lines for this batch of prefixes
        // Use spawn to avoid maxBuffer limits
        const rg = spawn("rg", [prefixRegex, gronPath], {
            stdio: ["ignore", "pipe", "inherit"],
        });
        const rgExitPromise = new Promise((resolve, reject) => {
            rg.on("exit", (code) => {
                if (code === null)
                    code = 0;
                resolve(code);
            });
            rg.on("error", (err) => {
                reject(new Error(`Failed to spawn ripgrep: ${err.message}`));
            });
        });
        // Collect ripgrep output
        const rgChunks = [];
        for await (const chunk of rg.stdout) {
            rgChunks.push(chunk);
        }
        const grepOutput = Buffer.concat(rgChunks).toString();
        // Wait for ripgrep to exit
        const rgExitCode = await rgExitPromise;
        if (rgExitCode === 1) {
            // ripgrep exit code 1 means no matches found
            logger.debug(`[ungron-batch] No data found for batch`);
            return { json: {}, processedCount: batch.length };
        }
        if (rgExitCode !== 0) {
            throw new Error(`ripgrep exited with code ${rgExitCode}`);
        }
        if (!grepOutput.trim()) {
            logger.debug(`[ungron-batch] No data found for batch`);
            return { json: {}, processedCount: batch.length };
        }
        // Pipe gron output to 'gron --ungron' to reconstruct JSON
        // Use spawn to avoid shell escaping issues with special characters
        const ungron = spawn("gron", ["--ungron"], {
            stdio: ["pipe", "pipe", "inherit"],
        });
        const exitPromise = new Promise((resolve, reject) => {
            ungron.on("exit", (code) => {
                if (code === null)
                    code = 0;
                resolve(code);
            });
            ungron.on("error", (err) => {
                reject(new Error(`Failed to spawn gron --ungron: ${err.message}`));
            });
        });
        // Write gron lines to ungron's stdin
        ungron.stdin.write(grepOutput);
        ungron.stdin.end();
        // Collect JSON output
        const chunks = [];
        for await (const chunk of ungron.stdout) {
            chunks.push(chunk);
        }
        const jsonOutput = Buffer.concat(chunks).toString();
        // Wait for ungron to exit
        const exitCode = await exitPromise;
        if (exitCode !== 0) {
            throw new Error(`gron --ungron exited with code ${exitCode}`);
        }
        const reconstructed = JSON.parse(jsonOutput);
        logger.debug(`[ungron-batch] Reconstructed ${batch.length} parent objects in batch`);
        return { json: reconstructed, processedCount: batch.length };
    }
    catch (err) {
        if (err.code === 1) {
            logger.debug(`[ungron-batch] No matches for prefixes`);
            return { json: {}, processedCount: batch.length };
        }
        logger.warn(`[ungron-batch] Error: ${err.message}`);
        return { json: {}, processedCount: batch.length };
    }
}
/**
 * Extract results from the reconstructed JSON using matched indices.
 * This walks the JSON structure and builds GrepMetadataResult items.
 * Uses the pre-computed matched indices to avoid re-checking with regex.
 *
 * @param startGlobalIndex - Skip results before this global index (for pagination)
 * @param globalIndexOffset - Start numbering results from this offset (for windowed ungronning)
 */
function extractResultsFromJSON(json, matched, limit, includeDetails, startGlobalIndex = 0, globalIndexOffset = 0) {
    const results = [];
    const logger = getLogger();
    // Track global result index for pagination (increments for every result)
    // Start from the offset to account for prefixes we skipped in windowed ungronning
    let globalIndex = globalIndexOffset;
    // Debug: log the JSON structure we received
    logger.debug(`[extract] JSON keys: ${Object.keys(json).join(", ")}`);
    if (json.schema) {
        logger.debug(`[extract] json.schema keys: ${Object.keys(json.schema).join(", ")}`);
    }
    if (json.databaseSchemaMetadata) {
        logger.debug(`[extract] json.databaseSchemaMetadata keys: ${Object.keys(json.databaseSchemaMetadata).join(", ")}`);
    }
    if (json.dbSchema) {
        logger.debug(`[extract] json.dbSchema keys: ${Object.keys(json.dbSchema).join(", ")}`);
    }
    // Walk database tables (support multiple field names for compatibility)
    const tables = json.schema?.tables ||
        json.dbSchema?.tables ||
        json.databaseSchemaMetadata?.tables;
    if (tables) {
        const nonNullTables = tables.filter((t) => t != null);
        logger.debug(`[extract] Walking ${tables.length} tables (${nonNullTables.length} non-null) from reconstructed JSON`);
        for (let i = 0; i < tables.length; i++) {
            // Skip sparse array holes efficiently
            if (!(i in tables))
                continue;
            const table = tables[i];
            if (!table)
                continue;
            // Only process tables that matched
            if (!matched.tables.has(i))
                continue;
            // Check if this is a table match (not just a column match)
            const hasColumnMatches = matched.columns.has(i);
            if (!hasColumnMatches) {
                // Skip if before our pagination start point
                if (globalIndex >= startGlobalIndex) {
                    // Table itself was matched (not via columns)
                    results.push({
                        type: "table",
                        name: table.name,
                        schema: table.schema,
                        columns: table.columns?.filter((c) => c != null) || [],
                        _globalIndex: globalIndex,
                        details: includeDetails
                            ? {
                                columnCount: table.columns?.filter((c) => c != null).length || 0,
                            }
                            : undefined,
                    });
                }
                globalIndex++;
            }
            // Check columns if they were matched
            if (hasColumnMatches && table.columns) {
                const matchedColIndices = matched.columns.get(i);
                for (let colIdx = 0; colIdx < table.columns.length; colIdx++) {
                    if (!(colIdx in table.columns))
                        continue;
                    const column = table.columns[colIdx];
                    if (!column)
                        continue;
                    // Only include columns that matched
                    if (!matchedColIndices.has(colIdx))
                        continue;
                    // Skip if before our pagination start point
                    if (globalIndex >= startGlobalIndex) {
                        results.push({
                            type: "column",
                            name: column.name,
                            table: table.name,
                            schema: table.schema,
                            columnType: column.type,
                            _globalIndex: globalIndex,
                            details: includeDetails
                                ? {
                                    type: column.type,
                                }
                                : undefined,
                        });
                    }
                    globalIndex++;
                }
            }
        }
    }
    // Walk database schemas (separate from table schemas)
    if (json.dbSchema?.schemas) {
        for (let i = 0; i < json.dbSchema.schemas.length; i++) {
            if (!(i in json.dbSchema.schemas))
                continue;
            const schema = json.dbSchema.schemas[i];
            if (!schema)
                continue;
            // Only include matched schemas
            if (!matched.schemas.has(i))
                continue;
            if (globalIndex >= startGlobalIndex) {
                results.push({
                    type: "schema",
                    name: schema.name,
                    _globalIndex: globalIndex,
                    details: includeDetails ? {} : undefined,
                });
            }
            globalIndex++;
        }
    }
    // Walk OpenAPI paths (object with string keys, not array)
    if (json.openApiSpec?.paths && typeof json.openApiSpec.paths === "object") {
        for (const [pathKey, pathItem] of Object.entries(json.openApiSpec.paths)) {
            if (!matched.paths.has(pathKey))
                continue;
            if (!pathItem || typeof pathItem !== "object")
                continue;
            // Extract methods from path item (get, post, put, etc.)
            const methods = Object.keys(pathItem).filter((k) => [
                "get",
                "post",
                "put",
                "delete",
                "patch",
                "head",
                "options",
                "trace",
            ].includes(k));
            // Return each operation (method) with full parameter details and top-level request/response schemas
            for (const method of methods) {
                const operation = pathItem[method];
                if (!operation || typeof operation !== "object")
                    continue;
                const operationName = operation.operationId ||
                    operation.summary ||
                    `${method.toUpperCase()} ${pathKey}`;
                // NOTE: Could think about just letting clark query the API to see the response structure.
                // Returning the full response schema from the spec often goes over the character limit.
                // Current approach: Extract property names and basic types (1-2 levels deep) as a middle ground.
                /**
                 * Extract property names and types from a JSON schema (1-2 levels deep)
                 * to avoid massive nested schemas that hit character limits
                 */
                const extractSchemaProperties = (schema) => {
                    if (!schema || typeof schema !== "object")
                        return undefined;
                    const result = {
                        type: schema.type,
                    };
                    // Handle array types - extract item type and properties if it's an object
                    if (schema.type === "array" && schema.items) {
                        result.items = {
                            type: schema.items.type,
                        };
                        if (schema.items.type === "object" && schema.items.properties) {
                            result.items.properties = {};
                            for (const [propName, propSchema] of Object.entries(schema.items.properties)) {
                                const ps = propSchema;
                                result.items.properties[propName] = {
                                    type: ps.type,
                                    required: schema.items.required?.includes(propName),
                                    enum: ps.enum,
                                };
                            }
                        }
                    }
                    // Handle object types - extract top-level properties only
                    if (schema.type === "object" && schema.properties) {
                        result.properties = {};
                        for (const [propName, propSchema] of Object.entries(schema.properties)) {
                            const ps = propSchema;
                            result.properties[propName] = {
                                type: ps.type,
                                required: schema.required?.includes(propName),
                                enum: ps.enum,
                            };
                        }
                    }
                    return result;
                };
                // Summarize responses (status codes, content types, and top-level schema properties)
                const responsesSummary = {};
                if (operation.responses && typeof operation.responses === "object") {
                    for (const [statusCode, response] of Object.entries(operation.responses)) {
                        if (response && typeof response === "object") {
                            const r = response;
                            const contentTypes = r.content ? Object.keys(r.content) : [];
                            const primaryContentType = contentTypes[0]; // Usually application/json
                            responsesSummary[statusCode] = {
                                description: r.description,
                                contentType: primaryContentType,
                                schema: primaryContentType && r.content?.[primaryContentType]?.schema
                                    ? extractSchemaProperties(r.content[primaryContentType].schema)
                                    : undefined,
                            };
                        }
                    }
                }
                // Summarize request body (content type and top-level properties)
                let requestBodySummary;
                if (operation.requestBody &&
                    typeof operation.requestBody === "object") {
                    const rb = operation.requestBody;
                    const contentTypes = rb.content ? Object.keys(rb.content) : [];
                    const primaryContentType = contentTypes[0]; // Usually application/json
                    requestBodySummary = {
                        required: rb.required,
                        contentType: primaryContentType,
                        schema: primaryContentType && rb.content?.[primaryContentType]?.schema
                            ? extractSchemaProperties(rb.content[primaryContentType].schema)
                            : undefined,
                    };
                }
                if (globalIndex >= startGlobalIndex) {
                    results.push({
                        type: "operation",
                        name: operationName,
                        method: method.toUpperCase(),
                        path: pathKey,
                        description: operation.description || operation.summary,
                        parameters: operation.parameters || [],
                        requestBody: requestBodySummary,
                        responses: responsesSummary,
                        _globalIndex: globalIndex,
                        details: includeDetails
                            ? {
                                operationId: operation.operationId,
                                method: method.toUpperCase(),
                                path: pathKey,
                                parameterCount: (operation.parameters || []).length,
                            }
                            : undefined,
                    });
                }
                globalIndex++;
            }
        }
    }
    // OpenAPI schemas: Not currently tracked in matched indices
    // Would need to add to extractParentPrefixes if needed
    // Walk GraphQL types (standard introspection format)
    const graphqlTypes = json.graphql?.data?.__schema?.types;
    if (graphqlTypes && Array.isArray(graphqlTypes)) {
        // only including fields when there aren't too many matches (otherwise we hit the character limit)
        const typeMatchCount = matched.types.size;
        const totalFieldMatches = Array.from(matched.fields.values()).reduce((sum, fields) => sum + fields.size, 0);
        const shouldIncludeFields = totalFieldMatches > 0 || typeMatchCount <= 10;
        // Get Query/Mutation type names to handle them specially
        const queryTypeName = json.graphql?.data?.__schema?.queryType?.name;
        const mutationTypeName = json.graphql?.data?.__schema?.mutationType?.name;
        for (let i = 0; i < graphqlTypes.length; i++) {
            if (!(i in graphqlTypes))
                continue;
            if (!matched.types.has(i))
                continue;
            const type = graphqlTypes[i];
            if (!type)
                continue;
            // Check if any fields in this type were matched
            const hasMatchedFields = matched.fields.has(i);
            // Special handling for Query/Mutation types: never include full fields array
            // because their fields are returned separately as query/mutation results
            const isQueryOrMutationType = type.name === queryTypeName || type.name === mutationTypeName;
            const includeFullFieldsForThisType = shouldIncludeFields && !isQueryOrMutationType;
            // When includeDetails is false, return field names only (compact)
            // When includeDetails is true, return full field objects (detailed)
            // Note: GraphQL INPUT_OBJECT types use 'inputFields' instead of 'fields'
            let fieldNames = [];
            if (!includeDetails) {
                const fieldsArray = type.fields || type.inputFields;
                if (fieldsArray) {
                    // For includeDetails: false, extract just the field names (lightweight)
                    fieldNames = fieldsArray
                        .filter((f) => f != null)
                        .map((f) => f.name);
                }
            }
            // Only return the type object if:
            // 1. includeDetails is false (compact mode with fieldNames) - ALWAYS return type, OR
            // 2. Not a field search (searching for types themselves)
            const isFieldSearch = totalFieldMatches > 0;
            const shouldReturnTypeObject = !includeDetails || (!isFieldSearch && !hasMatchedFields);
            if (shouldReturnTypeObject) {
                if (globalIndex >= startGlobalIndex) {
                    const fieldsArray = type.fields || type.inputFields;
                    results.push({
                        type: "graphql_type",
                        name: type.name,
                        kind: type.kind,
                        description: type.description,
                        fields: includeDetails && includeFullFieldsForThisType
                            ? fieldsArray?.filter((f) => f != null) || []
                            : undefined,
                        fieldNames: !includeDetails && fieldNames.length > 0 ? fieldNames : undefined,
                        _globalIndex: globalIndex,
                        details: includeDetails
                            ? {
                                kind: type.kind,
                                fieldCount: fieldsArray?.filter((f) => f != null).length || 0,
                            }
                            : undefined,
                    });
                }
                globalIndex++;
            }
            // Walk fields within this type if they were matched
            // Handle both 'fields' (for OBJECT types) and 'inputFields' (for INPUT_OBJECT types)
            const fieldsArray = type.fields || type.inputFields;
            if (fieldsArray && Array.isArray(fieldsArray) && matched.fields.has(i)) {
                const matchedFieldIndices = matched.fields.get(i);
                for (let j = 0; j < fieldsArray.length; j++) {
                    if (!(j in fieldsArray))
                        continue;
                    if (!matchedFieldIndices.has(j))
                        continue;
                    const field = fieldsArray[j];
                    if (!field)
                        continue;
                    if (globalIndex >= startGlobalIndex) {
                        results.push({
                            type: "field",
                            name: field.name,
                            typeName: type.name,
                            description: field.description,
                            returnType: field.type?.name || field.type?.kind,
                            args: field.args?.filter((a) => a != null) || [],
                            _globalIndex: globalIndex,
                            details: includeDetails
                                ? {
                                    typeName: type.name,
                                    fieldType: field.type?.name || field.type?.kind,
                                    description: field.description,
                                    argCount: field.args?.filter((a) => a != null).length || 0,
                                }
                                : undefined,
                        });
                    }
                    globalIndex++;
                }
            }
        }
    }
    // Walk GraphQL queries (standard introspection format)
    // In GraphQL introspection, queries are fields of the Query type
    // First, find the Query type in the types array
    const queryTypeName = json.graphql?.data?.__schema?.queryType?.name;
    const queryTypeIndex = queryTypeName
        ? graphqlTypes?.findIndex((t) => t?.name === queryTypeName)
        : -1;
    const queryType = queryTypeIndex >= 0 ? graphqlTypes[queryTypeIndex] : null;
    const graphqlQueries = queryType?.fields;
    // Only process queries if the Query type was matched (i.e., something in it matched the search)
    if (graphqlQueries &&
        Array.isArray(graphqlQueries) &&
        matched.types.has(queryTypeIndex)) {
        // Return all query fields since we already know this type was matched
        for (let i = 0; i < graphqlQueries.length; i++) {
            if (!(i in graphqlQueries))
                continue;
            const query = graphqlQueries[i];
            if (!query)
                continue;
            if (globalIndex >= startGlobalIndex) {
                results.push({
                    type: "query",
                    name: query.name,
                    description: query.description,
                    returnType: query.returnType || query.type?.name || query.type,
                    args: query.args?.filter((a) => a != null) || [],
                    _globalIndex: globalIndex,
                    details: includeDetails
                        ? {
                            returnType: query.returnType || query.type?.name,
                            argCount: query.args?.filter((a) => a != null).length || 0,
                        }
                        : undefined,
                });
            }
            globalIndex++;
        }
    }
    // Walk GraphQL mutations (standard introspection format)
    // In GraphQL introspection, mutations are fields of the Mutation type
    // First, find the Mutation type in the types array
    const mutationTypeName = json.graphql?.data?.__schema?.mutationType?.name;
    const mutationTypeIndex = mutationTypeName
        ? graphqlTypes?.findIndex((t) => t?.name === mutationTypeName)
        : -1;
    const mutationType = mutationTypeIndex >= 0 ? graphqlTypes[mutationTypeIndex] : null;
    const graphqlMutations = mutationType?.fields;
    // Only process mutations if the Mutation type was matched (i.e., something in it matched the search)
    if (graphqlMutations &&
        Array.isArray(graphqlMutations) &&
        matched.types.has(mutationTypeIndex)) {
        // Return all mutation fields since we already know this type was matched
        for (let i = 0; i < graphqlMutations.length; i++) {
            if (!(i in graphqlMutations))
                continue;
            const mutation = graphqlMutations[i];
            if (!mutation)
                continue;
            if (globalIndex >= startGlobalIndex) {
                results.push({
                    type: "mutation",
                    name: mutation.name,
                    description: mutation.description,
                    returnType: mutation.returnType || mutation.type?.name || mutation.type,
                    args: mutation.args?.filter((a) => a != null) || [],
                    _globalIndex: globalIndex,
                    details: includeDetails
                        ? {
                            returnType: mutation.returnType || mutation.type?.name,
                            argCount: mutation.args?.filter((a) => a != null).length || 0,
                        }
                        : undefined,
                });
            }
            globalIndex++;
        }
    }
    // Add raw value results (e.g., info.description, example values)
    // These are non-structured matches that don't fit into tables/paths/types
    for (const rawValue of matched.rawValues) {
        if (globalIndex >= startGlobalIndex) {
            // Extract a name from the path (last segment)
            const pathParts = rawValue.path.split(".");
            const name = pathParts[pathParts.length - 1] || rawValue.path;
            results.push({
                type: "value",
                name,
                jsonPath: rawValue.path,
                value: rawValue.value,
                _globalIndex: globalIndex,
            });
        }
        globalIndex++;
    }
    // Track if we hit the limit (truncation)
    const totalMatched = matched.tables.size +
        Array.from(matched.columns.values()).reduce((sum, cols) => sum + cols.size, 0) +
        matched.schemas.size +
        matched.paths.size +
        matched.operations.size +
        matched.types.size +
        Array.from(matched.fields.values()).reduce((sum, fields) => sum + fields.size, 0) +
        matched.rawValues.length; // Include raw values in total count
    const truncated = results.length >= limit && totalMatched > limit;
    logger.debug(`[extract] Built ${results.length} results from ${totalMatched} total matched items (truncated: ${truncated})`);
    return {
        matches: results,
        totalCount: totalMatched,
        truncated,
    };
}
/**
 * Build a GrepMetadataResult from gron search results.
 * Uses the path-prefix approach with batching: extracts parent prefixes,
 * fetches parent data in batches of 250, stops when character limit is reached.
 *
 * PAGINATION NOTE: If we want to implement pagination so that Clark can get all
 * results from this grep, we can return the currentIndex and store the prefixArray
 * in memory (or ContextManager) so we can continue from the next index on subsequent calls.
 */
export async function buildResultFromGronMatches(gronPath, matches, limit = Infinity, // Optional - defaults to no count limit
includeDetails, maxResponseChars = 25_000, startIndex = 0) {
    const logger = getLogger();
    if (matches.length === 0) {
        return { matches: [], totalCount: 0, truncated: false };
    }
    logger.debug(`[gron-prefix] Processing ${matches.length} matches (startIndex: ${startIndex})`);
    // Step 1: Extract unique parent prefixes AND matched indices from all matches
    const { prefixes, matched } = extractParentPrefixes(matches);
    logger.debug(`[gron-prefix] Found ${prefixes.size} unique parent prefixes`);
    logger.debug(`[gron-prefix] Matched indices: tables=${matched.tables.size}, columns=${matched.columns.size}, rawValues=${matched.rawValues.length}`);
    // If we only have raw values (no structured prefixes), return them directly
    if (prefixes.size === 0) {
        if (matched.rawValues.length > 0) {
            logger.debug(`[gron-prefix] No structured prefixes, returning ${matched.rawValues.length} raw values`);
            // Build results from raw values only
            const rawResults = matched.rawValues
                .slice(startIndex)
                .map((rawValue, idx) => {
                const pathParts = rawValue.path.split(".");
                const name = pathParts[pathParts.length - 1] || rawValue.path;
                return {
                    type: "value",
                    name,
                    jsonPath: rawValue.path,
                    value: rawValue.value,
                    _globalIndex: startIndex + idx,
                };
            });
            return {
                matches: rawResults,
                totalCount: matched.rawValues.length,
                truncated: false,
            };
        }
        return { matches: [], totalCount: 0, truncated: false };
    }
    // Convert Set to Array for index-based access
    const prefixArray = Array.from(prefixes);
    const totalPrefixes = prefixArray.length;
    // Step 2: Fetch and ungron parent objects in batches
    // Start ungronning from exactly startIndex to avoid re-processing prefixes
    // NOTE: This trades correctness for efficiency - if a prefix spans multiple results,
    // we might miss some if we start in the middle of it. This is accepted as a trade-off.
    const BATCH_SIZE = 250;
    const startPrefixIndex = startIndex; // Start exactly where Clark left off
    let currentIndex = startPrefixIndex;
    let mergedJSON = {};
    let totalProcessed = 0;
    logger.debug(`[gron-prefix] Starting ungronning from prefix ${startPrefixIndex} (startIndex ${startIndex})`);
    while (currentIndex < prefixArray.length) {
        // Fetch next batch
        const batchEnd = Math.min(currentIndex + BATCH_SIZE, prefixArray.length);
        const batchPrefixes = new Set(prefixArray.slice(currentIndex, batchEnd));
        const batchResult = await ungronMultiplePrefixes(gronPath, batchPrefixes, 0, // Always start at 0 within this batch
        batchPrefixes.size);
        // Deep merge this batch into accumulated JSON
        mergedJSON = deepMerge(mergedJSON, batchResult.json);
        totalProcessed += batchResult.processedCount;
        currentIndex += batchResult.processedCount;
        // Check if we've exceeded the character limit
        // We do a quick estimate by checking JSON size
        const currentSize = JSON.stringify(mergedJSON).length;
        logger.debug(`[gron-prefix] After batch ${Math.floor(totalProcessed / BATCH_SIZE)}: ` +
            `processed ${totalProcessed}/${prefixes.size} prefixes, JSON size: ${currentSize} chars`);
        // Stop if we're approaching the character limit (leave 20% buffer for formatting)
        if (currentSize > maxResponseChars * 0.8) {
            logger.info(`[gron-prefix] Stopping after ${totalProcessed}/${prefixes.size} prefixes ` +
                `(JSON size ${currentSize} approaching limit ${maxResponseChars})`);
            break;
        }
        // Optimization: Stop early if user explicitly requested a small limit
        // Use 2x buffer since not all prefixes will yield results
        // Only applies when limit < 100 (explicit small requests, not Infinity)
        if (limit < 100 && totalProcessed >= limit * 2) {
            break;
        }
        // Also stop if we have no more batches
        if (currentIndex >= prefixes.size) {
            break;
        }
    }
    // Step 3: Extract results from the accumulated JSON using matched indices
    // The extraction will filter by startIndex internally
    // IMPORTANT: When using windowed ungronning, globalIndexOffset should match startPrefixIndex
    // so that the first result from this window gets numbered correctly
    const result = extractResultsFromJSON(mergedJSON, matched, Infinity, // No limit here - we'll slice in grep-metadata.ts
    includeDetails, startIndex, // Filter: only return results with globalIndex >= this
    startPrefixIndex);
    logger.debug(`[gron-prefix] Extracted ${result.matches.length} results (starting from global index ${startIndex})`);
    return {
        ...result,
        truncated: result.truncated || currentIndex < totalPrefixes,
        hasMore: currentIndex < totalPrefixes,
    };
}
/**
 * Deep merge utility for combining JSON objects from multiple batches
 */
function deepMerge(target, source) {
    if (!source || typeof source !== "object") {
        return target;
    }
    if (!target || typeof target !== "object") {
        return source;
    }
    for (const key of Object.keys(source)) {
        if (Array.isArray(source[key])) {
            if (!target[key]) {
                target[key] = [];
            }
            // Merge arrays by index, preserving sparse arrays
            for (let i = 0; i < source[key].length; i++) {
                if (source[key][i] !== undefined) {
                    if (typeof source[key][i] === "object" && source[key][i] !== null) {
                        target[key][i] = deepMerge(target[key][i] || {}, source[key][i]);
                    }
                    else {
                        target[key][i] = source[key][i];
                    }
                }
            }
        }
        else if (typeof source[key] === "object" && source[key] !== null) {
            target[key] = deepMerge(target[key] || {}, source[key]);
        }
        else {
            target[key] = source[key];
        }
    }
    return target;
}
//# sourceMappingURL=grep-metadata-ripgrep.js.map