import { G as createContentManager, bO as buildMediaSearchClause, bN as dbQueryOne, E as dbQuery, bP as HookRegistry, bQ as SEARCHABLE_CONTENT_FIELD_TYPES, a as getSchemaManager, bR as emitEvent, bS as emitEventAsync } from '../nitro/nitro.mjs';
import 'node:crypto';
import 'jsonwebtoken';
import 'node:fs';
import 'node:module';
import 'node:path';
import 'node:url';
import 'node:http';
import 'node:https';
import 'node:events';
import 'node:buffer';
import 'pino';
import 'drizzle-orm/better-sqlite3';
import 'drizzle-orm/node-postgres';
import 'drizzle-orm/mysql2';
import 'better-sqlite3';
import 'pg';
import 'mysql2/promise';
import 'drizzle-orm';
import 'fs';
import 'path';
import 'prom-client';
import 'bcryptjs';
import '@node-rs/argon2';
import 'node:async_hooks';
import 'crypto';
import 'node:dns';
import 'node:util';
import 'node:net';
import 'fs/promises';
import 'url';
import 'semver';

var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
const DATABASE_ADAPTER_CAPABILITIES = {
  relevanceScoring: false,
  highlighting: false,
  faceting: false,
  fuzzySearch: false,
  synonyms: false,
  externalIndex: false
};
class DatabaseSearchAdapter {
  constructor() {
    __publicField$1(this, "name", "database");
    __publicField$1(this, "capabilities", DATABASE_ADAPTER_CAPABILITIES);
  }
  search(index, query) {
    const startTime = Date.now();
    if (index === "media") {
      return this.searchMedia(query, startTime);
    }
    return this.searchContent(index, query, startTime);
  }
  async indexDocument(_index, _document) {
  }
  async indexDocuments(_index, _documents) {
  }
  async removeDocument(_index, _documentId) {
  }
  async clearIndex(_index) {
  }
  healthCheck() {
    return Promise.resolve({ healthy: true, message: "Using database search (LIKE/ILIKE)" });
  }
  async dispose() {
  }
  // --------------------------------------------------------------------------
  // Private
  // --------------------------------------------------------------------------
  async searchContent(contentType, query, startTime) {
    const contentManager = createContentManager(contentType);
    const result = await contentManager.findMany({
      search: query.query,
      searchFields: query.searchFields,
      filters: query.filters,
      limit: query.limit,
      offset: query.offset,
      sort: query.sort,
      sortOrder: query.sortOrder,
      published: query.published,
      locale: query.locale
    });
    const hits = result.data.map((doc) => ({
      document: doc,
      index: contentType
    }));
    return {
      hits,
      total: result.total,
      processingTimeMs: Date.now() - startTime,
      query: query.query,
      limit: result.limit,
      offset: result.offset
    };
  }
  async searchMedia(query, startTime) {
    var _a, _b;
    const limit = Math.min((_a = query.limit) != null ? _a : 25, 200);
    const offset = (_b = query.offset) != null ? _b : 0;
    const clauses = [];
    const params = [];
    const mediaSearch = buildMediaSearchClause(query.query);
    if (mediaSearch.clause) {
      clauses.push(mediaSearch.clause);
      params.push(...mediaSearch.params);
    }
    const whereClause = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
    const countResult = await dbQueryOne(
      `SELECT COUNT(*) as count FROM media ${whereClause}`,
      params
    );
    const total = countResult ? Number(countResult.count) : 0;
    const rows = await dbQuery(
      `SELECT * FROM media ${whereClause} ORDER BY id DESC LIMIT ? OFFSET ?`,
      [...params, limit, offset]
    );
    const hits = rows.map((row) => ({
      document: row,
      index: "media"
    }));
    return {
      hits,
      total,
      processingTimeMs: Date.now() - startTime,
      query: query.query,
      limit,
      offset
    };
  }
}

const PIPELINE_PLUGIN_NAME = "__search-indexing-pipeline";
function contentToSearchDocument(contentType, entry, schema) {
  const fields = {};
  for (const [fieldName, fieldDef] of Object.entries(schema.attributes)) {
    if (SEARCHABLE_CONTENT_FIELD_TYPES.has(fieldDef.type)) {
      const value = entry[fieldName];
      if (value !== null && value !== void 0) {
        fields[fieldName] = value;
      }
    }
  }
  fields._contentType = contentType;
  if (entry.locale !== void 0) {
    fields._locale = entry.locale;
  }
  if (entry.published !== void 0) {
    fields._published = entry.published;
  }
  if (entry.publishedAt !== void 0) {
    fields._publishedAt = entry.publishedAt;
  }
  return {
    id: entry.id,
    fields
  };
}
function mediaToSearchDocument(entry) {
  var _a, _b, _c, _d, _e, _f, _g;
  return {
    id: entry.id,
    fields: {
      filename: (_a = entry.filename) != null ? _a : "",
      originalName: (_c = (_b = entry.originalName) != null ? _b : entry.originalname) != null ? _c : "",
      alt: (_d = entry.alt) != null ? _d : "",
      caption: (_e = entry.caption) != null ? _e : "",
      mimeType: (_g = (_f = entry.mimeType) != null ? _f : entry.mimetype) != null ? _g : ""
    }
  };
}
function registerSearchIndexingHooks(getService) {
  HookRegistry.on(
    "content:afterCreate",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      const entry = payload.entry;
      const schema = getSchemaForType(payload.contentType);
      if (!schema || !entry) {
        return;
      }
      const doc = contentToSearchDocument(payload.contentType, entry, schema);
      await service.indexDocument(payload.contentType, doc);
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "content:afterUpdate",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      const entry = payload.entry;
      const schema = getSchemaForType(payload.contentType);
      if (!schema || !entry) {
        return;
      }
      const doc = contentToSearchDocument(payload.contentType, entry, schema);
      await service.indexDocument(payload.contentType, doc);
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "content:afterDelete",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      await service.removeDocument(payload.contentType, payload.id);
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "content:afterPublish",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      const entry = payload.entry;
      const schema = getSchemaForType(payload.contentType);
      if (!schema || !entry) {
        return;
      }
      const doc = contentToSearchDocument(payload.contentType, entry, schema);
      await service.indexDocument(payload.contentType, doc);
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "content:afterUnpublish",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      const entry = payload.entry;
      const schema = getSchemaForType(payload.contentType);
      if (!schema || !entry) {
        return;
      }
      const doc = contentToSearchDocument(payload.contentType, entry, schema);
      await service.indexDocument(payload.contentType, doc);
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "media:upload",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      const metadata = payload.metadata;
      if (metadata && metadata.id) {
        const doc = mediaToSearchDocument(metadata);
        await service.indexDocument("media", doc);
      }
    },
    PIPELINE_PLUGIN_NAME
  );
  HookRegistry.on(
    "media:delete",
    async (raw) => {
      const payload = raw;
      const service = getService();
      if (!service.getAdapter().capabilities.externalIndex) {
        return;
      }
      await service.removeDocument("media", payload.id);
    },
    PIPELINE_PLUGIN_NAME
  );
}
function unregisterSearchIndexingHooks() {
  HookRegistry.removeByPlugin(PIPELINE_PLUGIN_NAME);
}
function getSchemaForType(contentType) {
  var _a;
  try {
    const schemaManager = getSchemaManager();
    return (_a = schemaManager.getSchema(contentType)) != null ? _a : null;
  } catch {
    return null;
  }
}

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
const DEFAULT_REINDEX_BATCH_SIZE = 100;
class SearchService {
  constructor(adapter) {
    __publicField(this, "adapter");
    __publicField(this, "disposed", false);
    this.adapter = adapter != null ? adapter : new DatabaseSearchAdapter();
  }
  /**
   * Получить текущий адаптер
   */
  getAdapter() {
    return this.adapter;
  }
  /**
   * Зарегистрировать новый адаптер (заменяет текущий)
   */
  async registerAdapter(adapter) {
    if (this.adapter.name !== "database") {
      await this.adapter.dispose();
    }
    this.adapter = adapter;
  }
  /**
   * Выполнить поисковый запрос
   */
  async search(index, query) {
    const startTime = Date.now();
    await emitEvent("search:beforeSearch", { index, query });
    const result = await this.adapter.search(index, query);
    const processingTimeMs = Date.now() - startTime;
    emitEventAsync("search:afterSearch", {
      index,
      query,
      result,
      processingTimeMs
    });
    return result;
  }
  /**
   * Индексировать один документ
   */
  async indexDocument(index, document) {
    emitEventAsync("search:beforeIndex", {
      action: "index",
      index,
      documentId: document.id
    });
    try {
      await this.adapter.indexDocument(index, document);
      emitEventAsync("search:afterIndex", {
        action: "index",
        index,
        documentId: document.id,
        success: true
      });
    } catch (error) {
      emitEventAsync("search:afterIndex", {
        action: "index",
        index,
        documentId: document.id,
        success: false
      });
      console.error(`[search] Failed to index document ${document.id} in ${index}:`, error);
    }
  }
  /**
   * Индексировать пакет документов
   */
  async indexDocuments(index, documents) {
    await this.adapter.indexDocuments(index, documents);
  }
  /**
   * Удалить документ из индекса
   */
  async removeDocument(index, documentId) {
    emitEventAsync("search:beforeIndex", {
      action: "delete",
      index,
      documentId
    });
    try {
      await this.adapter.removeDocument(index, documentId);
      emitEventAsync("search:afterIndex", {
        action: "delete",
        index,
        documentId,
        success: true
      });
    } catch (error) {
      emitEventAsync("search:afterIndex", {
        action: "delete",
        index,
        documentId,
        success: false
      });
      console.error(`[search] Failed to remove document ${documentId} from ${index}:`, error);
    }
  }
  /**
   * Очистить индекс
   */
  async clearIndex(index) {
    await this.adapter.clearIndex(index);
  }
  /**
   * Полная переиндексация content type
   */
  async reindex(index, batchSize = DEFAULT_REINDEX_BATCH_SIZE) {
    const startTime = Date.now();
    let indexed = 0;
    let errors = 0;
    if (index === "media") {
      return this.reindexMedia(batchSize, startTime);
    }
    const contentManager = createContentManager(index);
    const countResult = await contentManager.findMany({ limit: 1, offset: 0 });
    const total = countResult.total;
    await emitEvent("search:reindexStart", { index, totalDocuments: total });
    await this.adapter.clearIndex(index);
    const schemaManager = getSchemaManager();
    const schema = schemaManager.getSchema(index);
    if (!schema) {
      throw new Error(`Schema '${index}' not found for reindexing`);
    }
    let offset = 0;
    while (offset < total) {
      const batch = await contentManager.findMany({
        limit: batchSize,
        offset,
        sort: "id",
        sortOrder: "asc"
      });
      if (batch.data.length === 0) {
        break;
      }
      const docs = batch.data.map(
        (entry) => contentToSearchDocument(index, entry, schema)
      );
      try {
        await this.adapter.indexDocuments(index, docs);
        indexed += docs.length;
      } catch (error) {
        errors += docs.length;
        console.error(`[search] Reindex batch error for ${index} at offset ${offset}:`, error);
      }
      offset += batchSize;
    }
    const durationMs = Date.now() - startTime;
    emitEventAsync("search:reindexComplete", { index, indexed, errors, durationMs });
    return { indexed, errors, durationMs };
  }
  /**
   * Проверить здоровье адаптера
   */
  async healthCheck() {
    const status = await this.adapter.healthCheck();
    return { ...status, adapter: this.adapter.name };
  }
  /**
   * Освободить ресурсы
   */
  async dispose() {
    if (this.disposed) {
      return;
    }
    this.disposed = true;
    unregisterSearchIndexingHooks();
    await this.adapter.dispose();
  }
  // --------------------------------------------------------------------------
  // Private
  // --------------------------------------------------------------------------
  async reindexMedia(batchSize, startTime) {
    let indexed = 0;
    let errors = 0;
    const countResult = await dbQueryOne(
      "SELECT COUNT(*) as count FROM media",
      []
    );
    const total = countResult ? Number(countResult.count) : 0;
    await emitEvent("search:reindexStart", { index: "media", totalDocuments: total });
    await this.adapter.clearIndex("media");
    let offset = 0;
    while (offset < total) {
      const rows = await dbQuery(
        `SELECT * FROM media ORDER BY id ASC LIMIT ? OFFSET ?`,
        [batchSize, offset]
      );
      if (rows.length === 0) {
        break;
      }
      const docs = rows.map((row) => mediaToSearchDocument(row));
      try {
        await this.adapter.indexDocuments("media", docs);
        indexed += docs.length;
      } catch (error) {
        errors += docs.length;
        console.error(`[search] Reindex media batch error at offset ${offset}:`, error);
      }
      offset += batchSize;
    }
    const durationMs = Date.now() - startTime;
    emitEventAsync("search:reindexComplete", {
      index: "media",
      indexed,
      errors,
      durationMs
    });
    return { indexed, errors, durationMs };
  }
}
let searchService = null;
function initSearchService(adapter) {
  if (searchService) {
    return searchService;
  }
  const service = new SearchService(adapter);
  searchService = service;
  registerSearchIndexingHooks(() => service);
  return searchService;
}
function getSearchService() {
  if (!searchService) {
    return initSearchService();
  }
  return searchService;
}

export { DatabaseSearchAdapter, SearchService, contentToSearchDocument, getSearchService, initSearchService, mediaToSearchDocument, registerSearchIndexingHooks, unregisterSearchIndexingHooks };
//# sourceMappingURL=index.mjs.map
