/**
 * Context Assembly Utilities
 *
 * Provides utilities for assembling, formatting, and optimizing context
 * from retrieved chunks for LLM consumption.
 *
 * Features:
 * - Context window management (token-aware truncation)
 * - Citation formatting
 * - Context deduplication
 * - Relevance-based ordering
 * - Context summarization
 */
import { logger } from "../../utils/logger.js";
import { estimateTokens } from "../../utils/tokenEstimation.js";
/**
 * Assemble context from retrieved results
 *
 * Combines multiple chunks into a coherent context string
 * suitable for LLM consumption.
 *
 * @param results - Retrieved chunks or query results
 * @param options - Assembly options
 * @returns Assembled context string
 *
 * @example
 * ```typescript
 * const context = assembleContext(results, {
 *   maxTokens: 4000,
 *   citationFormat: 'numbered',
 *   deduplicate: true
 * });
 * ```
 */
export function assembleContext(results, options) {
    const { maxChars, maxTokens = 4000, citationFormat = "none", separator = "\n\n---\n\n", includeMetadata = false, deduplicate = false, dedupeThreshold = 0.8, orderByRelevance = true, includeSectionHeaders = false, headerTemplate = "[{index}] Source: {source}", } = options || {};
    if (results.length === 0) {
        return "";
    }
    // Convert to unified format
    let items = results.map((r, index) => ({
        id: "id" in r ? r.id : `chunk-${index}`,
        text: "text" in r ? r.text || "" : "",
        score: "score" in r ? r.score || 0 : 0,
        metadata: "metadata" in r ? r.metadata : {},
        index,
    }));
    // Get text from metadata if not directly available
    items = items.map((item) => ({
        ...item,
        text: item.text ||
            item.metadata?.text ||
            "",
    }));
    // Order by relevance if requested
    if (orderByRelevance) {
        items.sort((a, b) => b.score - a.score);
    }
    // Deduplicate if requested
    if (deduplicate) {
        // Ensure metadata is defined for deduplication
        const itemsWithMetadata = items.map((item) => ({
            ...item,
            metadata: item.metadata || {},
        }));
        items = deduplicateChunks(itemsWithMetadata, dedupeThreshold);
    }
    // Calculate max characters
    const effectiveMaxChars = maxChars || maxTokens * 4;
    // Assemble context with token awareness
    const parts = [];
    let totalChars = 0;
    for (const item of items) {
        const header = includeSectionHeaders
            ? formatHeader(headerTemplate, {
                index: parts.length + 1,
                source: item.metadata?.source || item.id,
                score: item.score,
            })
            : "";
        const metadata = includeMetadata ? formatMetadata(item.metadata) : "";
        const citation = formatCitation(citationFormat, parts.length + 1, item.metadata);
        const chunkText = [
            header,
            citation ? `${citation}\n` : "",
            item.text,
            metadata,
        ]
            .filter(Boolean)
            .join("\n");
        // Check if adding this chunk would exceed limit
        const newTotalChars = totalChars + chunkText.length + separator.length;
        if (newTotalChars > effectiveMaxChars) {
            // Try to include partial chunk
            const remainingChars = effectiveMaxChars - totalChars - separator.length - 50; // Buffer
            if (remainingChars > 200) {
                const truncatedText = truncateText(item.text, remainingChars);
                parts.push([
                    header,
                    citation ? `${citation}\n` : "",
                    truncatedText,
                    "[truncated]",
                ]
                    .filter(Boolean)
                    .join("\n"));
            }
            break;
        }
        parts.push(chunkText);
        totalChars = newTotalChars;
    }
    return parts.join(separator);
}
/**
 * Format context with inline citations
 *
 * @param results - Retrieved results
 * @param options - Formatting options
 * @returns Context with citations and citation list
 */
export function formatContextWithCitations(results, options) {
    const citations = [];
    const items = results.map((r, index) => {
        const id = "id" in r ? r.id : `chunk-${index}`;
        const metadata = "metadata" in r ? r.metadata : {};
        const source = metadata?.source || id;
        citations.push(`[${index + 1}] ${source}`);
        return {
            ...r,
            citationMarker: `[${index + 1}]`,
        };
    });
    const context = assembleContext(items, {
        ...options,
        citationFormat: "numbered",
        includeSectionHeaders: true,
        headerTemplate: "[{index}]",
    });
    return { context, citations };
}
/**
 * Create a context window with detailed tracking
 *
 * @param results - Retrieved results
 * @param options - Assembly options
 * @returns Context window with metadata
 */
export function createContextWindow(results, options) {
    const maxTokens = options?.maxTokens || 4000;
    const maxChars = options?.maxChars || maxTokens * 4;
    let text = "";
    let chunkCount = 0;
    let truncatedChunks = 0;
    const citations = new Map();
    const items = results.map((r, index) => ({
        id: "id" in r ? r.id : `chunk-${index}`,
        text: ("text" in r ? r.text : "") ||
            r.metadata?.text ||
            "",
        metadata: "metadata" in r ? r.metadata : {},
    }));
    for (const item of items) {
        const chunkText = item.text;
        const newLength = text.length + chunkText.length + 10; // Buffer for separators
        if (newLength > maxChars) {
            // Try partial inclusion
            const remaining = maxChars - text.length - 20;
            if (remaining > 100) {
                const truncated = truncateText(chunkText, remaining);
                text += (text ? "\n\n" : "") + truncated + "...";
                truncatedChunks++;
                citations.set(item.id, `[${chunkCount + 1}] ${item.metadata?.source || item.id} (truncated)`);
                chunkCount++;
            }
            else {
                truncatedChunks++;
            }
            continue;
        }
        text += (text ? "\n\n" : "") + chunkText;
        citations.set(item.id, `[${chunkCount + 1}] ${item.metadata?.source || item.id}`);
        chunkCount++;
    }
    return {
        text,
        chunkCount,
        charCount: text.length,
        tokenCount: estimateTokens(text),
        truncatedChunks,
        citations,
    };
}
/**
 * Summarize context using LLM
 *
 * @param context - Context to summarize
 * @param maxLength - Maximum summary length
 * @param provider - LLM provider instance
 * @returns Summarized context
 */
export async function summarizeContext(context, maxLength = 500, provider) {
    if (!provider) {
        // Simple truncation fallback
        return truncateText(context, maxLength * 4);
    }
    try {
        const result = await provider.generate({
            prompt: `Summarize the following context in no more than ${maxLength} words, preserving the key information:\n\n${context}\n\nSummary:`,
            maxTokens: Math.ceil(maxLength * 1.5),
            temperature: 0.3,
        });
        return result?.content?.trim() || truncateText(context, maxLength * 4);
    }
    catch (error) {
        logger.warn("[ContextAssembly] Summarization failed, using truncation", {
            error: error instanceof Error ? error.message : String(error),
        });
        return truncateText(context, maxLength * 4);
    }
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
 * Format section header using template
 */
function formatHeader(template, vars) {
    return template
        .replace("{index}", String(vars.index))
        .replace("{source}", vars.source)
        .replace("{score}", vars.score.toFixed(4));
}
/**
 * Format citation based on style
 */
function formatCitation(format, index, metadata) {
    switch (format) {
        case "inline":
            return `(Source: ${metadata?.source || `#${index}`})`;
        case "footnote":
            return `[^${index}]`;
        case "numbered":
            return `[${index}]`;
        case "none":
        default:
            return "";
    }
}
/**
 * Format metadata for display
 */
function formatMetadata(metadata) {
    if (!metadata) {
        return "";
    }
    const relevant = ["source", "title", "author", "date", "page"];
    const parts = [];
    for (const key of relevant) {
        if (metadata[key]) {
            parts.push(`${key}: ${metadata[key]}`);
        }
    }
    return parts.length > 0 ? `\n[${parts.join(" | ")}]` : "";
}
/**
 * Truncate text at word boundary
 */
function truncateText(text, maxLength) {
    if (text.length <= maxLength) {
        return text;
    }
    // Find last space before maxLength
    let truncateAt = text.lastIndexOf(" ", maxLength);
    if (truncateAt === -1 || truncateAt < maxLength * 0.7) {
        truncateAt = maxLength;
    }
    return text.slice(0, truncateAt).trim();
}
/**
 * Deduplicate chunks based on text similarity
 */
function deduplicateChunks(items, threshold) {
    const unique = [];
    for (const item of items) {
        // Check if this item is too similar to any already included
        const isDuplicate = unique.some((existing) => textSimilarity(item.text, existing.text) > threshold);
        if (!isDuplicate) {
            unique.push(item);
        }
    }
    return unique;
}
/**
 * Simple text similarity using Jaccard index
 */
function textSimilarity(a, b) {
    const wordsA = new Set(a.toLowerCase().split(/\s+/));
    const wordsB = new Set(b.toLowerCase().split(/\s+/));
    const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
    const union = new Set([...wordsA, ...wordsB]);
    return intersection.size / union.size;
}
/**
 * Order chunks by document structure (if available)
 */
export function orderByDocumentStructure(chunks) {
    // Group by document
    const byDocument = new Map();
    for (const chunk of chunks) {
        const docId = chunk.metadata.documentId;
        const group = byDocument.get(docId);
        if (group) {
            group.push(chunk);
        }
        else {
            byDocument.set(docId, [chunk]);
        }
    }
    // Sort each document's chunks by position
    for (const docChunks of byDocument.values()) {
        docChunks.sort((a, b) => (a.metadata.chunkIndex || 0) - (b.metadata.chunkIndex || 0));
    }
    // Flatten, keeping documents together
    return [...byDocument.values()].flat();
}
/**
 * Extract key sentences from chunks for summary
 */
export function extractKeySentences(text, count = 3) {
    const sentences = text
        .split(/[.!?]+/)
        .map((s) => s.trim())
        .filter((s) => s.length > 20);
    // Simple scoring: longer sentences with more unique words
    const scored = sentences.map((s) => ({
        text: s,
        score: s.length * new Set(s.toLowerCase().split(/\s+/)).size,
    }));
    scored.sort((a, b) => b.score - a.score);
    return scored.slice(0, count).map((s) => s.text);
}
//# sourceMappingURL=contextAssembly.js.map