import { getDB } from '../db.js';
/**
 * Convert a user query into a Postgres tsquery-compatible string.
 * Splits on whitespace, strips non-word chars, joins with & for AND matching.
 */
function toTsQuery(query) {
    const terms = query
        .replace(/[^\w\s]/g, ' ')
        .trim()
        .split(/\s+/)
        .filter(Boolean)
        .map((t) => `${t}:*`);
    return terms.join(' & ');
}
/** Search documents using Postgres full-text search with relevance ranking. */
export async function searchDocuments(query, options = {}) {
    const db = getDB();
    const { collection, locale, status, page = 1, pageSize = 20 } = options;
    const offset = (page - 1) * pageSize;
    const sanitized = query.replace(/[^\w\s]/g, ' ').trim();
    if (!sanitized) {
        return { results: [], total: 0, page, pageSize };
    }
    const tsQueryStr = toTsQuery(sanitized);
    if (!tsQueryStr) {
        return { results: [], total: 0, page, pageSize };
    }
    const conditions = [`d."deletedAt" IS NULL`];
    const params = [];
    let paramIdx = 1;
    // tsquery as a parameter instead of inline interpolation
    params.push(tsQueryStr);
    const tsQueryParam = `$${paramIdx++}`;
    if (collection) {
        conditions.push(`d."collection" = $${paramIdx++}`);
        params.push(collection);
    }
    if (locale) {
        conditions.push(`d."locale" = $${paramIdx++}`);
        params.push(locale);
    }
    if (status) {
        conditions.push(`d."status"::text = $${paramIdx++}`);
        params.push(status);
    }
    const whereClause = conditions.join(' AND ');
    const ftsVector = `to_tsvector('english', COALESCE(d."title", '') || ' ' || COALESCE(d."plainText", ''))`;
    const ftsCondition = `(${ftsVector} @@ to_tsquery('english', ${tsQueryParam}))`;
    const ilikePlaceholder = `$${paramIdx}`;
    params.push(`%${sanitized}%`);
    paramIdx++;
    const fallbackCondition = `(d."title" ILIKE ${ilikePlaceholder} OR d."plainText" ILIKE ${ilikePlaceholder})`;
    const rankExpr = `ts_rank_cd(${ftsVector}, to_tsquery('english', ${tsQueryParam}))`;
    const headline = `ts_headline('english', COALESCE(d."plainText", ''), to_tsquery('english', ${tsQueryParam}), 'StartSel=<mark>, StopSel=</mark>, MaxWords=35, MinWords=15, MaxFragments=2')`;
    const limitParam = `$${paramIdx++}`;
    params.push(pageSize);
    const offsetParam = `$${paramIdx++}`;
    params.push(offset);
    const sql = `
    SELECT
      d."id",
      d."title",
      d."slug",
      d."collection",
      d."status",
      d."updatedAt",
      ${rankExpr} AS score,
      ${headline} AS headline,
      LEFT(COALESCE(d."plainText", ''), 200) AS excerpt
    FROM "actuate_documents" d
    WHERE ${whereClause}
      AND (${ftsCondition} OR ${fallbackCondition})
    ORDER BY score DESC, d."updatedAt" DESC
    LIMIT ${limitParam}
    OFFSET ${offsetParam}
  `;
    const countSql = `
    SELECT COUNT(*)::int AS total
    FROM "actuate_documents" d
    WHERE ${whereClause}
      AND (${ftsCondition} OR ${fallbackCondition})
  `;
    const countParams = params.slice(0, -2);
    try {
        const [rows, countRows] = await Promise.all([
            db.$queryRawUnsafe(sql, ...params),
            db.$queryRawUnsafe(countSql, ...countParams),
        ]);
        const total = countRows[0]?.total ?? 0;
        const results = rows.map((row) => {
            const headlineText = row.headline ?? '';
            const highlights = headlineText
                .match(/<mark>(.*?)<\/mark>/g)
                ?.map((m) => m.replace(/<\/?mark>/g, '')) ?? [];
            return {
                id: row.id,
                title: row.title ?? '',
                slug: row.slug ?? '',
                collection: row.collection,
                excerpt: headlineText || row.excerpt || '',
                score: parseFloat(row.score) || 0,
                highlights,
            };
        });
        return { results, total, page, pageSize };
    }
    catch {
        return fallbackSearch(sanitized, options);
    }
}
/** Fallback search using Prisma contains (for non-Postgres or when FTS fails). */
async function fallbackSearch(query, options) {
    const db = getDB();
    const { collection, locale, status, page = 1, pageSize = 20 } = options;
    const offset = (page - 1) * pageSize;
    const where = {
        deletedAt: null,
        OR: [
            { plainText: { contains: query, mode: 'insensitive' } },
            { title: { contains: query, mode: 'insensitive' } },
        ],
    };
    if (collection)
        where.collection = collection;
    if (locale)
        where.locale = locale;
    if (status)
        where.status = status;
    const [results, total] = await Promise.all([
        db.document.findMany({
            where,
            orderBy: { updatedAt: 'desc' },
            skip: offset,
            take: pageSize,
            select: {
                id: true,
                title: true,
                slug: true,
                collection: true,
                plainText: true,
            },
        }),
        db.document.count({ where }),
    ]);
    return {
        results: results.map((doc) => {
            const text = doc.plainText ?? '';
            const idx = text.toLowerCase().indexOf(query.toLowerCase());
            const start = Math.max(0, idx - 40);
            const excerpt = idx >= 0 ? '...' + text.substring(start, start + 200) + '...' : text.substring(0, 200);
            return {
                id: doc.id,
                title: doc.title ?? '',
                slug: doc.slug ?? '',
                collection: doc.collection,
                excerpt,
                score: doc.title?.toLowerCase().includes(query.toLowerCase()) ? 2 : 1,
                highlights: [query],
            };
        }),
        total,
        page,
        pageSize,
    };
}
//# sourceMappingURL=index.js.map