"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var qdrant_index_exports = {};
__export(qdrant_index_exports, {
  DASHBOARD_SUBDIR: () => DASHBOARD_SUBDIR,
  QdrantIndex: () => QdrantIndex
});
module.exports = __toCommonJS(qdrant_index_exports);
var path = __toESM(require("path"));
var crypto = __toESM(require("crypto"));
var http = __toESM(require("http"));
var fs = __toESM(require("fs"));
var import_errors = require("../errors");
const DEFAULT_DIM = 768;
const STORAGE_DIR = "qdrant";
const COLLECTION = "kg_nodes";
const READYZ_TIMEOUT_MS = 1e4;
const READYZ_POLL_MS = 100;
const SERVER_STATE_FILE = "qdrant-server.json";
const DASHBOARD_SUBDIR = "qdrant/dashboard";
function toUuid(nodeId) {
  const h = crypto.createHash("md5").update(nodeId).digest("hex");
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
}
function getFreePort() {
  return new Promise((resolve, reject) => {
    const srv = http.createServer().listen(0, "127.0.0.1", () => {
      const port = srv.address().port;
      srv.close(() => resolve(port));
    });
    srv.on("error", reject);
  });
}
function waitReady(port, timeoutMs) {
  return new Promise((resolve, reject) => {
    const deadline = Date.now() + timeoutMs;
    function check() {
      const req = http.get(`http://127.0.0.1:${port}/readyz`, (res) => {
        if (res.statusCode === 200) return resolve();
        scheduleRetry();
      });
      req.on("error", scheduleRetry);
      req.setTimeout(200, () => {
        req.destroy();
        scheduleRetry();
      });
    }
    function scheduleRetry() {
      if (Date.now() >= deadline) return reject(new Error("Qdrant readyz timeout"));
      setTimeout(check, READYZ_POLL_MS);
    }
    check();
  });
}
class QdrantIndex {
  constructor(kirographDir, dim = DEFAULT_DIM) {
    this.kirographDir = kirographDir;
    this.dim = dim;
    this.client = null;
    this.child = null;
    this._available = false;
    this._ownedProcess = false;
    this.storagePath = path.join(kirographDir, STORAGE_DIR);
    this.stateFile = path.join(kirographDir, SERVER_STATE_FILE);
  }
  isAvailable() {
    return this._available;
  }
  readState() {
    try {
      return JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
    } catch {
      return null;
    }
  }
  writeState(s) {
    try {
      fs.writeFileSync(this.stateFile, JSON.stringify(s));
    } catch {
    }
  }
  clearState() {
    try {
      fs.unlinkSync(this.stateFile);
    } catch {
    }
  }
  isProcessAlive(pid) {
    try {
      process.kill(pid, 0);
      return true;
    } catch {
      return false;
    }
  }
  /** The HTTP port of the running server, or null if not available. */
  getPort() {
    if (this._available) {
      const s2 = this.readState();
      return s2?.port ?? null;
    }
    const s = this.readState();
    if (s && this.isProcessAlive(s.pid)) return s.port;
    return null;
  }
  /**
   * Locate the Qdrant binary (via qdrant-local), spawn it with a project-scoped
   * storage path, wait until /readyz responds, then ensure the collection exists.
   * Silent no-op when qdrant-local is not installed.
   */
  async initialize() {
    if (this._available) return;
    let binPath;
    try {
      const pkgPath = require.resolve("qdrant-local/package.json");
      const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
      const { directory, name } = pkg.qdrantBinary;
      const exeName = ["win32", "cygwin"].includes(process.platform) ? `${name}.exe` : name;
      binPath = path.join(path.dirname(pkgPath), directory, exeName);
      if (!fs.existsSync(binPath)) {
        (0, import_errors.logDebug)("QdrantIndex: qdrant binary not found \u2014 run: npm install qdrant-local");
        return;
      }
    } catch {
      (0, import_errors.logDebug)("QdrantIndex: qdrant-local not installed \u2014 Qdrant engine unavailable");
      return;
    }
    let QdrantClient;
    try {
      QdrantClient = require("@qdrant/qdrant-js").QdrantClient;
    } catch {
      (0, import_errors.logDebug)("QdrantIndex: @qdrant/qdrant-js not available \u2014 Qdrant engine unavailable");
      return;
    }
    const saved = this.readState();
    if (saved) {
      if (this.isProcessAlive(saved.pid)) {
        try {
          await waitReady(saved.port, 3e3);
          this.client = new QdrantClient({ host: "127.0.0.1", port: saved.port, checkCompatibility: false });
          await this.ensureCollection();
          this._available = true;
          this._ownedProcess = false;
          (0, import_errors.logDebug)("QdrantIndex: reused running server", { pid: saved.pid, port: saved.port });
          return;
        } catch {
          try {
            process.kill(saved.pid, "SIGKILL");
          } catch {
          }
          await new Promise((r) => setTimeout(r, 300));
        }
      }
      this.clearState();
    }
    try {
      fs.mkdirSync(this.storagePath, { recursive: true });
      const port = await getFreePort();
      const dashboardDir = path.join(this.kirographDir, DASHBOARD_SUBDIR);
      const staticEnv = fs.existsSync(dashboardDir) ? { QDRANT__SERVICE__STATIC_CONTENT_DIR: dashboardDir } : {};
      const { spawn } = require("child_process");
      this.child = spawn(binPath, [], {
        stdio: "ignore",
        env: {
          ...process.env,
          QDRANT__SERVICE__HTTP_PORT: String(port),
          QDRANT__STORAGE__STORAGE_PATH: this.storagePath,
          QDRANT__LOG_LEVEL: "WARN",
          ...staticEnv
        }
      });
      this.child.unref();
      await waitReady(port, READYZ_TIMEOUT_MS);
      this.writeState({ pid: this.child.pid, port });
      this.client = new QdrantClient({ host: "127.0.0.1", port, checkCompatibility: false });
      await this.ensureCollection();
      this._available = true;
      this._ownedProcess = true;
      (0, import_errors.logDebug)("QdrantIndex: ready", { storagePath: this.storagePath, port, dim: this.dim });
    } catch (err) {
      (0, import_errors.logError)("QdrantIndex: initialization failed", { error: String(err) });
    }
  }
  async ensureCollection() {
    const existsResult = await this.client.collectionExists(COLLECTION);
    const exists = typeof existsResult === "object" ? existsResult.exists : existsResult;
    if (!exists) {
      await this.client.createCollection(COLLECTION, {
        vectors: { size: this.dim, distance: "Cosine" }
      });
    }
  }
  /**
   * Upsert a node's embedding with retry logic.
   * Qdrant's upsert is idempotent — the same point ID is overwritten if it already exists.
   * Retries up to 3 times with exponential backoff to handle transient connection failures.
   */
  async upsert(node, embedding) {
    if (!this._available || !this.client) return;
    const point = {
      id: toUuid(node.id),
      vector: Array.from(embedding),
      payload: {
        node_id: node.id,
        name: node.name,
        kind: node.kind,
        file_path: node.filePath,
        signature: node.signature ?? ""
      }
    };
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        await this.client.upsert(COLLECTION, { points: [point] });
        return;
      } catch (err) {
        if (attempt < 2) {
          await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
        } else {
          (0, import_errors.logWarn)("QdrantIndex: upsert failed", { nodeId: node.id, error: String(err) });
        }
      }
    }
  }
  /**
   * Batch upsert multiple nodes' embeddings in a single HTTP request.
   * Much more efficient than individual upserts for bulk indexing.
   * Retries up to 3 times with exponential backoff.
   */
  async bulkUpsert(nodes, embeddings) {
    if (!this._available || !this.client) return;
    if (nodes.length === 0) return;
    const points = nodes.map((node, i) => ({
      id: toUuid(node.id),
      vector: Array.from(embeddings[i]),
      payload: {
        node_id: node.id,
        name: node.name,
        kind: node.kind,
        file_path: node.filePath,
        signature: node.signature ?? ""
      }
    }));
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        await this.client.upsert(COLLECTION, { points });
        return;
      } catch (err) {
        if (attempt < 2) {
          await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
        } else {
          (0, import_errors.logWarn)("QdrantIndex: bulk upsert failed", { count: nodes.length, error: String(err) });
        }
      }
    }
  }
  /**
   * Remove a point from the collection with retry logic.
   */
  async delete(nodeId) {
    if (!this._available || !this.client) return;
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        await this.client.delete(COLLECTION, { points: [toUuid(nodeId)] });
        return;
      } catch (err) {
        if (attempt < 2) {
          await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
        } else {
          (0, import_errors.logWarn)("QdrantIndex: delete failed", { nodeId, error: String(err) });
        }
      }
    }
  }
  /**
   * ANN vector search. Returns node IDs ordered by cosine similarity (descending).
   */
  async search(queryVec, topN = 10) {
    if (!this._available || !this.client) return [];
    try {
      const results = await this.client.search(COLLECTION, {
        vector: Array.from(queryVec),
        limit: topN,
        with_payload: ["node_id"]
      });
      return results.map((r) => r.payload.node_id);
    } catch (err) {
      (0, import_errors.logWarn)("QdrantIndex: search failed", { error: String(err) });
      return [];
    }
  }
  /**
   * Return all node IDs currently stored in the collection (paginated scroll).
   */
  async getEmbeddedNodeIds() {
    if (!this._available || !this.client) return [];
    const ids = [];
    let offset = null;
    try {
      do {
        const page = await this.client.scroll(COLLECTION, {
          with_payload: ["node_id"],
          with_vector: false,
          limit: 1e3,
          ...offset !== null ? { offset } : {}
        });
        for (const pt of page.points) {
          if (pt.payload?.node_id) ids.push(pt.payload.node_id);
        }
        offset = page.next_page_offset ?? null;
      } while (offset !== null);
    } catch {
      return ids;
    }
    return ids;
  }
  /** Disconnect the client. The Qdrant server keeps running as a daemon. */
  close() {
    this._available = false;
    this.child = null;
    this.client = null;
  }
  /** Number of points currently in the collection. */
  async count() {
    if (!this._available || !this.client) return 0;
    try {
      const result = await this.client.count(COLLECTION, { exact: true });
      return result.count;
    } catch {
      return 0;
    }
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  DASHBOARD_SUBDIR,
  QdrantIndex
});
//# sourceMappingURL=qdrant-index.js.map
