globalThis.process ??= {};
globalThis.process.env ??= {};
import { A as inArray } from "./d1_DTqC4o_t.mjs";
import { J as contentEntries } from "./crypto_CY2zBtG3.mjs";
import { s as createContentSearchProvider } from "./dist_TEPdmzZw.mjs";
//#region ../../packages/util/dist/fts/sanitize-tsquery.js
/** tsquery metacharacters that must not reach Postgres `to_tsquery` / `plainto_tsquery`. */
var TSQUERY_METACHAR_RE = /[!&|():*'"`<>\\]/g;
/**
* Sanitize user input for prefix tsquery (`token:* & …`).
* Strips metachars, AND-joins prefix tokens.
*
* SECURITY (shared host contract): the output is injection-safe end-to-end ONLY if
* the host passes it to `to_tsquery()`/`plainto_tsquery()` or a bound parameter, and
* NEVER string-interpolates it into raw SQL. `websearch_to_tsquery` is the throw-proof
* alternative. See `packages/util/src/fts/README.md`.
*/
function sanitizeTsquery(input) {
	const cleaned = input.replace(TSQUERY_METACHAR_RE, " ").trim();
	if (!cleaned) return "";
	const tokens = cleaned.split(/\s+/).filter(Boolean);
	if (tokens.length === 0) return "";
	return tokens.map((token) => `${token}:*`).join(" & ");
}
//#endregion
//#region ../../packages/search/dist/index.js
var MAX_CURSOR_OFFSET = 1e4;
function encodeCursor(cursor) {
	return btoa(JSON.stringify(cursor));
}
/**
* Decode a client-supplied cursor (untrusted trust-boundary input). Returns
* null on any malformed/forged token — caller treats null as "first page"
* (lenient reset), never throws.
*/
function decodeCursor(encoded) {
	let parsed;
	try {
		parsed = JSON.parse(atob(encoded));
	} catch {
		return null;
	}
	if (typeof parsed !== "object" || parsed === null) return null;
	const { entityType, offset } = parsed;
	if (typeof entityType !== "string") return null;
	if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0 || offset > MAX_CURSOR_OFFSET) return null;
	return {
		entityType,
		offset
	};
}
function resolveEntityTypes(registry, entityTypes) {
	const keys = Object.keys(registry);
	if (!entityTypes || entityTypes.length === 0) return keys;
	return entityTypes.filter((type) => type in registry);
}
async function searchEntities(registry, params) {
	const sanitizedQuery = sanitizeTsquery(params.query);
	const candidateTypes = resolveEntityTypes(registry, params.entityTypes);
	const isSingleType = candidateTypes.length === 1;
	let offset = 0;
	let singleEntityType;
	if (isSingleType) {
		singleEntityType = candidateTypes[0];
		if (params.cursor) {
			const decoded = decodeCursor(params.cursor);
			if (decoded && decoded.entityType === singleEntityType) offset = decoded.offset;
		}
	}
	const providerResults = await Promise.all(candidateTypes.map(async (entityType) => {
		const window = {
			limit: params.limit,
			offset: isSingleType ? offset : 0
		};
		const provider = registry[entityType];
		return {
			entityType,
			...await provider(sanitizedQuery, params.ctx, window)
		};
	}));
	const groups = providerResults.filter((result) => result.total > 0 || result.hits.length > 0).map(({ entityType, hits, total }) => ({
		entityType,
		hits,
		total
	})).sort((a, b) => b.total - a.total);
	let nextCursor = null;
	if (isSingleType && singleEntityType) {
		const raw = providerResults.find((result) => result.entityType === singleEntityType)?.total ?? 0;
		if (offset + params.limit < raw) nextCursor = encodeCursor({
			entityType: singleEntityType,
			offset: offset + params.limit
		});
	}
	return {
		groups,
		nextCursor
	};
}
//#endregion
//#region src/lib/content-search.ts
async function loadPublicContentSearch(db, q, cursor) {
	const trimmed = q.trim().slice(0, 256);
	if (!trimmed) return {
		hasQuery: false,
		hits: [],
		nextCursor: null
	};
	let result;
	try {
		result = await searchEntities({ content: createContentSearchProvider({ types: ["post"] }) }, {
			query: trimmed,
			ctx: {
				db,
				viewer: null
			},
			entityTypes: ["content"],
			limit: 20,
			cursor: cursor ?? null
		});
	} catch {
		return {
			hasQuery: true,
			hits: [],
			nextCursor: null
		};
	}
	const group = result.groups.find((g) => g.entityType === "content");
	if (!group || group.hits.length === 0) return {
		hasQuery: true,
		hits: [],
		nextCursor: result.nextCursor
	};
	const ids = group.hits.map((h) => h.id);
	let slugById = /* @__PURE__ */ new Map();
	try {
		const rows = await db.select({
			id: contentEntries.id,
			slug: contentEntries.slug
		}).from(contentEntries).where(inArray(contentEntries.id, ids));
		slugById = new Map(rows.map((r) => [r.id, r.slug]));
	} catch {
		return {
			hasQuery: true,
			hits: [],
			nextCursor: result.nextCursor
		};
	}
	const hits = [];
	for (const hit of group.hits) {
		const slug = slugById.get(hit.id);
		if (!slug) continue;
		hits.push({
			id: hit.id,
			slug,
			title: hit.title ?? slug,
			snippetHtml: hit.snippetHtml ?? "",
			rank: hit.rank
		});
	}
	return {
		hasQuery: true,
		hits,
		nextCursor: result.nextCursor
	};
}
//#endregion
export { loadPublicContentSearch as t };
