/**
 * Citation management utilities for tracking and deduplicating citations.
 *
 * Port of `backend/agent/citations.py` and `backend/agent/tools/process_citations.py`
 * to TypeScript. Produces identical IDs via djb2 hash for cross-language compatibility.
 *
 * Citations have numeric string IDs that can be inlined in LLM messages using
 * {{citation-ID}} syntax so the frontend can render source links.
 */
// ─── djb2 hash ───────────────────────────────────────────────────────────────
/**
 * djb2 hash algorithm — matches Python implementation for cross-language compatibility.
 *
 * CRITICAL: Python uses `& 0xFFFFFFFF` to mask to 32-bit unsigned and then `abs()`.
 * JavaScript's bitwise `<<` operates on signed 32-bit ints. We use `>>> 0` to
 * convert to unsigned 32-bit, matching Python's `& 0xFFFFFFFF` + `abs()` behavior.
 */
export function createTextHash(text) {
    let hash = 5381;
    for (const char of text) {
        // for..of iterates by code point, matching Python's `for char in text` / `ord(char)`
        const cp = char.codePointAt(0);
        hash = (((hash << 5) + hash + cp) & 0xffffffff) >>> 0;
    }
    return hash;
}
// ─── Source key generation ────────────────────────────────────────────────────
/**
 * Generate source key for deduplication and ordering.
 * Matches Python's `get_source_key()` exactly.
 */
export function getSourceKey(citation) {
    if (citation.type === 'web') {
        return `web-${citation.url}`;
    }
    if (citation.type === 'document') {
        const pageStart = citation.pageStart ?? '';
        const pageEnd = citation.pageEnd ?? '';
        return `document-${citation.title}:${pageStart}:${pageEnd}`;
    }
    return '';
}
// ─── Citation ID generation ──────────────────────────────────────────────────
/**
 * Generate a citation ID from the citation's source key hash.
 * Produces the same numeric string as Python's `get_citation_id()`.
 */
export function getCitationId(citation) {
    const sourceKey = getSourceKey(citation);
    return String(createTextHash(sourceKey));
}
// ─── Inline citation formatting ──────────────────────────────────────────────
/**
 * Format a single citation ID for inline embedding in message text.
 * Example: formatInlineCitation("1240308948") => "{{citation-1240308948}}"
 */
export function formatInlineCitation(id) {
    return `{{citation-${id}}}`;
}
/**
 * Format multiple citations for appending after a text block.
 * Matches Python's `format_inline_citations_for_text_block()`.
 */
export function formatInlineCitations(citations) {
    if (citations.length === 0)
        return '';
    return ' ' + citations.map((c) => formatInlineCitation(c.id)).join('');
}
// ─── File extension removal ──────────────────────────────────────────────────
function removeFileExtension(filename) {
    const dotIndex = filename.lastIndexOf('.');
    if (dotIndex > 0) {
        return filename.slice(0, dotIndex);
    }
    return filename;
}
// ─── Anthropic citation processing ───────────────────────────────────────────
/**
 * Process a page_location citation from Anthropic API.
 * @see https://docs.anthropic.com/en/docs/build-with-claude/citations#example-pdf-citation
 */
function processPageCitation(citation) {
    const draft = {
        type: 'document',
        title: citation.document_title ? removeFileExtension(citation.document_title) : 'Document',
        citedText: citation.cited_text,
        pageStart: citation.start_page_number,
        pageEnd: citation.end_page_number
    };
    return { ...draft, id: getCitationId(draft) };
}
/**
 * Process a char_location citation from Anthropic API.
 * Extracts page numbers from <!--page=N--> markers in cited text when present.
 */
function processCharCitation(citation) {
    const citedText = citation.cited_text ?? '';
    let pageStart = null;
    let pageEnd = null;
    // Look for <!--page=N--> markers in cited text
    if (citedText.includes('<!--page=')) {
        try {
            const pageMarker = citedText.split('<!--page=')[1].split('-->')[0];
            const pageNum = parseInt(pageMarker, 10);
            if (!isNaN(pageNum)) {
                pageStart = pageNum;
                pageEnd = pageNum;
            }
        }
        catch {
            // Ignore parse errors
        }
    }
    const draft = {
        type: 'document',
        title: citation.document_title ? removeFileExtension(citation.document_title) : 'Document',
        citedText
    };
    if (pageStart !== null) {
        draft.pageStart = pageStart;
        draft.pageEnd = pageEnd;
    }
    return { ...draft, id: getCitationId(draft) };
}
/**
 * Process a web_search_result_location or web_location citation.
 */
function processWebCitation(citation) {
    const url = citation.url;
    if (!url)
        return null;
    const title = citation.title ?? url;
    const citedText = citation.cited_text ?? '';
    const draft = {
        type: 'web',
        url,
        title,
        citedText
    };
    return { ...draft, id: getCitationId(draft) };
}
/**
 * Process Anthropic API content blocks to extract citations and format inline markers.
 *
 * Takes the `content` array from an Anthropic API response and returns:
 * - `contentWithCitations`: the concatenated text with {{citation-ID}} markers appended
 *   after each text block that has citations
 * - `citations`: a deduped map of citation ID -> Citation object
 *
 * Handles three Anthropic citation types:
 * - `page_location` — PDF page citations
 * - `char_location` — character-offset citations (with optional <!--page=N--> markers)
 * - `web_search_result_location` / `web_location` — web search citations
 *
 * The LLM proxy emits `content_block` SSE events when text blocks carry
 * citation arrays from Anthropic's API, which are accumulated and passed here.
 */
export function processAnthropicCitations(contentBlocks) {
    const result = {
        contentWithCitations: '',
        citations: {}
    };
    // Handle string content (no citations possible)
    if (typeof contentBlocks === 'string') {
        result.contentWithCitations = contentBlocks;
        return result;
    }
    if (!contentBlocks || contentBlocks.length === 0) {
        return result;
    }
    for (const block of contentBlocks) {
        // Skip non-object blocks
        if (typeof block !== 'object' || block === null)
            continue;
        const typedBlock = block;
        if (typedBlock.type !== 'text')
            continue;
        const textContent = typedBlock.text ?? '';
        const blockCitations = [];
        if ('citations' in typedBlock && Array.isArray(typedBlock.citations)) {
            for (const rawCitation of typedBlock.citations) {
                let processed = null;
                switch (rawCitation.type) {
                    case 'page_location':
                        processed = processPageCitation(rawCitation);
                        break;
                    case 'char_location':
                        processed = processCharCitation(rawCitation);
                        break;
                    case 'web_search_result_location':
                    case 'web_location':
                        processed = processWebCitation(rawCitation);
                        break;
                }
                if (processed) {
                    blockCitations.push(processed);
                    result.citations[processed.id] = processed;
                }
            }
        }
        result.contentWithCitations += textContent;
        if (blockCitations.length > 0) {
            result.contentWithCitations += formatInlineCitations(blockCitations);
        }
    }
    return result;
}
/**
 * Merge new citations into an existing citations record, deduplicating by ID.
 * Returns the merged record (mutates `existing` for efficiency).
 */
export function mergeCitations(existing, incoming) {
    for (const [id, citation] of Object.entries(incoming)) {
        existing[id] = citation;
    }
    return existing;
}
//# sourceMappingURL=citations.js.map