/**
 * Browser Bookmark Import Service
 *
 * Parses Netscape Bookmark File format (exported from Chrome, Firefox, Safari, Edge, Arc, Brave)
 * and creates `reference` entities. Deterministic — no LLM needed.
 */
/** Roots that don't carry useful tag information. */
const SKIP_ROOTS = new Set([
    'bookmarks bar', 'bookmarks toolbar', 'other bookmarks',
    'bookmarks menu', 'favorites', 'mobile bookmarks', 'bookmarks',
]);
/** Convert a string to a kebab-case slug for use as an entity ID. */
function slugify(str) {
    return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}
/** Decode common HTML entities. */
function decodeEntities(str) {
    return str
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&#x27;/g, "'")
        .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
        .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
}
/**
 * Detect whether an HTML string is a Netscape Bookmark File.
 */
export function isNetscapeBookmarkFile(html) {
    if (!html || html.length < 20)
        return false;
    const upper = html.slice(0, 500).toUpperCase();
    // Explicit doctype
    if (upper.includes('<!DOCTYPE NETSCAPE-BOOKMARK-FILE-1>'))
        return true;
    // Structural pattern: <DL + <DT + <A HREF all present
    const full = html.toUpperCase();
    return full.includes('<DL') && full.includes('<DT') && full.includes('<A HREF');
}
/**
 * Parse a Netscape Bookmark File into structured bookmark data.
 * Walks <DT>, <A>, <H3>, <DL>, </DL>, <DD> tags via regex.
 */
export function parseNetscapeBookmarks(html) {
    const bookmarks = [];
    const folderStack = [];
    // Regex to find relevant tags
    const tagPattern = /<(\/?)(\w+)([^>]*)>/gi;
    let match;
    let lastBookmark = null;
    while ((match = tagPattern.exec(html)) !== null) {
        const closing = match[1] === '/';
        const tag = match[2].toUpperCase();
        const attrs = match[3];
        if (tag === 'H3' && !closing) {
            // Folder name — extract text until </H3>
            const textEnd = html.indexOf('</H3', match.index + match[0].length);
            if (textEnd !== -1) {
                // Also check for </h3 (case insensitive)
                const textEndLower = html.indexOf('</h3', match.index + match[0].length);
                const actualEnd = textEndLower !== -1 && (textEnd === -1 || textEndLower < textEnd) ? textEndLower : textEnd;
                const folderName = decodeEntities(html.slice(match.index + match[0].length, actualEnd).trim());
                folderStack.push(folderName);
            }
        }
        else if (tag === 'A' && !closing) {
            // Bookmark link
            const hrefMatch = /HREF\s*=\s*"([^"]*)"/i.exec(attrs);
            const addDateMatch = /ADD_DATE\s*=\s*"(\d+)"/i.exec(attrs);
            const tagsMatch = /TAGS\s*=\s*"([^"]*)"/i.exec(attrs);
            if (hrefMatch) {
                const url = decodeEntities(hrefMatch[1]);
                // Skip javascript: URLs
                if (url.startsWith('javascript:'))
                    continue;
                // Extract title text until </A>
                const titleEnd = html.indexOf('</A', match.index + match[0].length);
                const titleEndLower = html.indexOf('</a', match.index + match[0].length);
                const actualTitleEnd = titleEndLower !== -1 && (titleEnd === -1 || titleEndLower < titleEnd) ? titleEndLower : titleEnd;
                let title = '';
                if (actualTitleEnd !== -1) {
                    title = decodeEntities(html.slice(match.index + match[0].length, actualTitleEnd).trim());
                }
                if (!title && !url)
                    continue;
                // Parse Firefox TAGS attribute (comma-separated)
                const parsedTags = tagsMatch
                    ? tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean)
                    : undefined;
                const bookmark = {
                    title,
                    url,
                    folderPath: [...folderStack],
                    ...(addDateMatch ? { addDate: Number(addDateMatch[1]) } : {}),
                    ...(parsedTags?.length ? { tags: parsedTags } : {}),
                };
                bookmarks.push(bookmark);
                lastBookmark = bookmark;
            }
        }
        else if (tag === 'DD' && !closing) {
            // Description for the preceding bookmark
            if (lastBookmark) {
                const ddEnd = html.indexOf('<', match.index + match[0].length);
                if (ddEnd !== -1) {
                    const desc = decodeEntities(html.slice(match.index + match[0].length, ddEnd).trim());
                    if (desc)
                        lastBookmark.description = desc;
                }
            }
        }
        else if (tag === 'DL' && closing) {
            // End of folder
            folderStack.pop();
            lastBookmark = null;
        }
        else if (tag === 'DL' && !closing) {
            // Opening DL — lastBookmark from previous DT no longer applies
            lastBookmark = null;
        }
    }
    return bookmarks;
}
/**
 * Convert a parsed bookmark to reference entity data.
 */
export function bookmarkToReference(bookmark, userId) {
    // Domain extraction
    let source = '';
    try {
        source = new URL(bookmark.url).hostname.replace(/^www\./, '');
    }
    catch { /* invalid URL — leave source empty */ }
    // Title with fallback
    const name = bookmark.title || source || 'Untitled Bookmark';
    // Entity ID: slugified title, max 80 chars
    let entityId = slugify(name).slice(0, 80).replace(/-$/, '');
    if (!entityId || entityId.length < 2) {
        entityId = `ref-${slugify(source || 'bookmark')}-${Date.now()}`;
    }
    // Tags from folder path (skip common roots) + Firefox TAGS attribute, deduped
    const folderTags = bookmark.folderPath
        .filter(f => !SKIP_ROOTS.has(f.toLowerCase()))
        .map(f => slugify(f))
        .filter(t => t.length > 0);
    const firefoxTags = (bookmark.tags ?? [])
        .map(t => slugify(t))
        .filter(t => t.length > 0);
    const tags = [...new Set([...folderTags, ...firefoxTags])];
    // Date from ADD_DATE (Unix timestamp) or today
    let date;
    if (bookmark.addDate && bookmark.addDate > 0) {
        const d = new Date(bookmark.addDate * 1000);
        date = d.toISOString().split('T')[0];
    }
    else {
        date = new Date().toISOString().split('T')[0];
    }
    const now = new Date().toISOString();
    const data = {
        entity_type: 'reference',
        entity_id: entityId,
        name,
        source,
        link: bookmark.url,
        date,
        tags,
        created_at: now,
        updated_at: now,
        created_by: userId ?? 'import',
        updated_by: userId ?? 'import',
    };
    // Body: markdown link, plus description under heading if present
    const linkLine = `[${name}](${bookmark.url})`;
    const content = bookmark.description
        ? `${linkLine}\n\n## Description\n\n${bookmark.description}`
        : linkLine;
    return { entityId, data, content };
}
/**
 * Import bookmarks into a graph as reference entities.
 * Deduplicates by URL against existing references.
 */
export async function importBookmarks(graph, html, user) {
    const parsed = parseNetscapeBookmarks(html);
    const result = { created: 0, skipped: 0, errors: [], results: [] };
    if (parsed.length === 0) {
        result.errors.push('No bookmarks found in the file');
        return result;
    }
    // Build URL set from existing references for dedup
    const existing = graph.list('reference');
    const existingUrls = new Set();
    for (const entity of existing) {
        const link = entity.data.link;
        if (link)
            existingUrls.add(link);
    }
    // Track IDs used in this batch to handle collisions
    const usedIds = new Set(existing.map(e => e.data.entity_id));
    for (const bookmark of parsed) {
        // Skip duplicates by URL
        if (existingUrls.has(bookmark.url)) {
            result.skipped++;
            result.results.push({
                entityId: '',
                title: bookmark.title,
                status: 'skipped',
                error: 'URL already exists',
            });
            continue;
        }
        try {
            const ref = bookmarkToReference(bookmark, user?.id);
            // Handle ID collision with timestamp suffix
            let { entityId } = ref;
            if (usedIds.has(entityId)) {
                entityId = `${entityId}-${Date.now()}`.slice(0, 80);
                ref.data.entity_id = entityId;
            }
            await graph.create({
                entityType: 'reference',
                entityId,
                data: ref.data,
                content: ref.content,
                skipCommit: true,
                user,
            });
            usedIds.add(entityId);
            existingUrls.add(bookmark.url);
            result.created++;
            result.results.push({ entityId, title: ref.data.name, status: 'created' });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            if (msg.includes('already exists')) {
                result.skipped++;
                result.results.push({ entityId: '', title: bookmark.title, status: 'skipped', error: 'Entity already exists' });
            }
            else {
                result.errors.push(`${bookmark.title}: ${msg}`);
                result.results.push({ entityId: '', title: bookmark.title, status: 'error', error: msg });
            }
        }
    }
    return result;
}
//# sourceMappingURL=bookmark-import-service.js.map