import { sql } from "kysely";
import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("query_index").child({ component: "search-tokens" });
const INSERT_BATCH_SIZE = 500;
function chunk(items, size) {
  if (size <= 0) return [items];
  const out = [];
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
  return out;
}
const DEFAULT_SCOPE = { organizationId: null, tenantId: null };
const isSearchDebugEnabled = () => {
  return parseBooleanToken(process.env.OM_SEARCH_DEBUG ?? "") === true;
};
const debug = (event, payload) => {
  if (!isSearchDebugEnabled()) return;
  try {
    logger.debug("Search token event", { event, payload });
  } catch {
  }
};
function collectTextValues(value) {
  if (typeof value === "string") return [value];
  if (Array.isArray(value)) {
    const out = [];
    for (const entry of value) {
      if (typeof entry === "string") out.push(entry);
    }
    return out;
  }
  return [];
}
function shouldIndexField(field, value, config) {
  if (typeof value !== "string" && !Array.isArray(value)) return false;
  const lower = field.toLowerCase();
  if (lower === "id" || lower.endsWith("_id") || lower.endsWith(".id")) return false;
  if (lower.endsWith("_at")) return false;
  if (["created_at", "updated_at", "deleted_at", "tenant_id", "organization_id"].includes(lower)) return false;
  if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return false;
  const values = collectTextValues(value);
  if (!values.length) return false;
  return values.some((text) => tokenizeText(text, config).tokens.length > 0);
}
function buildSearchTokenRows(params) {
  const config = params.config ?? resolveSearchConfig();
  if (!config.enabled) return [];
  if (!params.doc) return [];
  const tokens = [];
  const capturePairs = isSearchDebugEnabled() && params.entityType === "customers:customer_deal";
  const debugPairs = [];
  const scope = {
    organizationId: params.organizationId ?? DEFAULT_SCOPE.organizationId,
    tenantId: params.tenantId ?? DEFAULT_SCOPE.tenantId
  };
  for (const [field, rawValue] of Object.entries(params.doc)) {
    if (!shouldIndexField(field, rawValue, config)) continue;
    const values = collectTextValues(rawValue);
    const seen = /* @__PURE__ */ new Set();
    for (const text of values) {
      const { tokens: textTokens, hashes } = tokenizeText(text, config);
      for (let i = 0; i < textTokens.length; i += 1) {
        const token = textTokens[i];
        const hash = hashes[i];
        const dedupeKey = `${field}|${hash}`;
        if (seen.has(dedupeKey)) continue;
        seen.add(dedupeKey);
        debug("token.generated", { entityType: params.entityType, recordId: params.recordId, field, hash });
        tokens.push({
          entity_type: params.entityType,
          entity_id: String(params.recordId),
          organization_id: scope.organizationId,
          tenant_id: scope.tenantId,
          field,
          token_hash: hash,
          token: config.storeRawTokens ? token : null
        });
        if (capturePairs) {
          debugPairs.push({ field, hash });
        }
      }
    }
  }
  if (capturePairs) {
    debug("deal.tokens", {
      entityType: params.entityType,
      recordId: params.recordId,
      tokenCount: debugPairs.length,
      tokens: debugPairs
    });
  }
  debug("doc.completed", { entityType: params.entityType, recordId: params.recordId, tokenCount: tokens.length });
  return tokens;
}
function buildFieldPairs(recordId, doc) {
  if (!doc) return [];
  const pairs = [];
  const dedupe = /* @__PURE__ */ new Set();
  for (const field of Object.keys(doc)) {
    const key = `${recordId}|${field}`;
    if (dedupe.has(key)) continue;
    dedupe.add(key);
    pairs.push([recordId, field]);
  }
  return pairs;
}
async function replaceSearchTokensForRecord(db, params) {
  const rows = buildSearchTokenRows(params);
  const config = params.config ?? resolveSearchConfig();
  if (!config.enabled) return;
  const organizationId = params.organizationId ?? null;
  const tenantId = params.tenantId ?? null;
  const fieldPairs = buildFieldPairs(String(params.recordId), params.doc);
  await db.transaction().execute(async (trx) => {
    let deleteQuery = trx.deleteFrom("search_tokens").where("entity_type", "=", params.entityType).where(sql`organization_id is not distinct from ${organizationId}`).where(sql`tenant_id is not distinct from ${tenantId}`);
    if (fieldPairs.length) {
      deleteQuery = deleteQuery.where((eb) => eb.or(
        fieldPairs.map(([rid, field]) => eb.and([
          eb("entity_id", "=", rid),
          eb("field", "=", field)
        ]))
      ));
    } else {
      deleteQuery = deleteQuery.where("entity_id", "=", String(params.recordId));
    }
    await deleteQuery.execute();
    if (!rows.length) return;
    const payloads = rows.map((row) => ({ ...row, created_at: sql`now()` }));
    for (const batch of chunk(payloads, INSERT_BATCH_SIZE)) {
      await trx.insertInto("search_tokens").values(batch).execute();
    }
  });
}
async function deleteSearchTokensForRecord(db, params) {
  const organizationId = params.organizationId ?? null;
  const tenantId = params.tenantId ?? null;
  await db.deleteFrom("search_tokens").where("entity_type", "=", params.entityType).where("entity_id", "=", String(params.recordId)).where(sql`organization_id is not distinct from ${organizationId}`).where(sql`tenant_id is not distinct from ${tenantId}`).execute();
}
async function replaceSearchTokensForBatch(db, payloads) {
  if (!payloads.length) return;
  const config = resolveSearchConfig();
  if (!config.enabled) return;
  const rows = payloads.flatMap((payload) => buildSearchTokenRows({ ...payload, config }));
  if (!rows.length) {
    const entityType = payloads[0]?.entityType;
    if (!entityType) return;
    const ids = payloads.map((p) => String(p.recordId));
    await db.deleteFrom("search_tokens").where("entity_type", "=", entityType).where("entity_id", "in", ids).execute();
    return;
  }
  const scopeKey = (org, tenant) => `${org ?? "__null__"}|${tenant ?? "__null__"}`;
  const scopeBuckets = /* @__PURE__ */ new Map();
  for (const payload of payloads) {
    const org = payload.organizationId ?? null;
    const tenant = payload.tenantId ?? null;
    const key = scopeKey(org, tenant);
    const bucket = scopeBuckets.get(key) ?? { organizationId: org, tenantId: tenant, ids: /* @__PURE__ */ new Set() };
    bucket.ids.add(String(payload.recordId));
    scopeBuckets.set(key, bucket);
  }
  await db.transaction().execute(async (trx) => {
    for (const [, bucket] of scopeBuckets.entries()) {
      const deleteQuery = trx.deleteFrom("search_tokens").where("entity_type", "=", payloads[0].entityType).where(sql`organization_id is not distinct from ${bucket.organizationId}`).where(sql`tenant_id is not distinct from ${bucket.tenantId}`).where("entity_id", "in", Array.from(bucket.ids));
      await deleteQuery.execute();
    }
    const payloadWithTimestamps = rows.map((row) => ({ ...row, created_at: sql`now()` }));
    for (const batch of chunk(payloadWithTimestamps, INSERT_BATCH_SIZE)) {
      await trx.insertInto("search_tokens").values(batch).execute();
    }
  });
}
export {
  buildSearchTokenRows,
  deleteSearchTokensForRecord,
  isSearchDebugEnabled,
  replaceSearchTokensForBatch,
  replaceSearchTokensForRecord
};
//# sourceMappingURL=search-tokens.js.map
