"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 typesense_index_exports = {};
__export(typesense_index_exports, {
  TypesenseIndex: () => TypesenseIndex
});
module.exports = __toCommonJS(typesense_index_exports);
var path = __toESM(require("path"));
var fs = __toESM(require("fs"));
var http = __toESM(require("http"));
var import_os = require("os");
var import_errors = require("../errors");
var import_archive = require("../bin/installer/archive");
const DEFAULT_DIM = 768;
const STORAGE_DIR = "typesense";
const COLLECTION = "kg_nodes";
const API_KEY = "kirograph-local";
const TYPESENSE_VERSION = "28.0";
const BIN_CACHE_DIR = path.join((0, import_os.homedir)(), ".kirograph", "bin");
const HEALTH_TIMEOUT_MS = 6e4;
const HEALTH_POLL_MS = 200;
const SERVER_STATE_FILE = "typesense-server.json";
function getBinaryUrl() {
  const v = TYPESENSE_VERSION;
  const { platform, arch } = process;
  if (platform === "darwin") {
    const a = arch === "arm64" ? "darwin-arm64" : "darwin-amd64";
    return `https://dl.typesense.org/releases/${v}/typesense-server-${v}-${a}.tar.gz`;
  }
  if (platform === "linux") {
    const a = arch === "arm64" ? "linux-arm64" : "linux-amd64";
    return `https://dl.typesense.org/releases/${v}/typesense-server-${v}-${a}.tar.gz`;
  }
  return null;
}
function getBinPath() {
  return path.join(BIN_CACHE_DIR, `typesense-server-${TYPESENSE_VERSION}`);
}
async function downloadBinary(url, destPath) {
  fs.mkdirSync(BIN_CACHE_DIR, { recursive: true });
  await (0, import_archive.extractBinaryFromTarGz)(url, "typesense-server", destPath);
}
function toDocId(nodeId) {
  return nodeId.replace(/:/g, "__");
}
function getFreePorts(count) {
  return new Promise((resolve, reject) => {
    const ports = [];
    const servers = [];
    let done = 0;
    for (let i = 0; i < count; i++) {
      const srv = http.createServer();
      servers.push(srv);
      srv.listen(0, "127.0.0.1", () => {
        ports.push(srv.address().port);
        done++;
        if (done === count) {
          let closed = 0;
          for (const s of servers) s.close(() => {
            if (++closed === count) resolve(ports);
          });
        }
      });
      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}/health`, (res) => {
        let body = "";
        res.on("data", (c) => body += c);
        res.on("end", () => {
          try {
            if (JSON.parse(body).ok === true) return resolve();
          } catch {
          }
          scheduleRetry();
        });
      });
      req.on("error", scheduleRetry);
      req.setTimeout(300, () => {
        req.destroy();
        scheduleRetry();
      });
    }
    function scheduleRetry() {
      if (Date.now() >= deadline) return reject(new Error("Typesense health timeout"));
      setTimeout(check, HEALTH_POLL_MS);
    }
    check();
  });
}
const _silentLogger = { log: () => {
}, warn: () => {
}, error: () => {
}, debug: () => {
}, info: () => {
}, trace: () => {
}, setLevel: () => {
} };
function makeClientConfig(port) {
  return {
    nodes: [{ host: "127.0.0.1", port, protocol: "http" }],
    apiKey: API_KEY,
    connectionTimeoutSeconds: 10,
    retryIntervalSeconds: 0.1,
    numRetries: 3,
    logger: _silentLogger,
    logLevel: "silent"
  };
}
class TypesenseIndex {
  constructor(kirographDir, dim = DEFAULT_DIM) {
    this.kirographDir = kirographDir;
    this.dim = dim;
    this.client = null;
    this.child = null;
    this._available = false;
    this._ownedProcess = false;
    // true only when we spawned the child ourselves
    this._failReason = null;
    this.storagePath = path.join(kirographDir, STORAGE_DIR);
    this.stateFile = path.join(kirographDir, SERVER_STATE_FILE);
  }
  readState() {
    try {
      return JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
    } catch {
      return null;
    }
  }
  writeState(state) {
    try {
      fs.writeFileSync(this.stateFile, JSON.stringify(state));
    } catch {
    }
  }
  clearState() {
    try {
      fs.unlinkSync(this.stateFile);
    } catch {
    }
  }
  isProcessAlive(pid) {
    try {
      process.kill(pid, 0);
      return true;
    } catch {
      return false;
    }
  }
  isAvailable() {
    return this._available;
  }
  getFailReason() {
    return this._failReason;
  }
  /**
   * Download the Typesense binary if needed, spawn it with a project-scoped
   * data directory, wait until /health responds, then ensure the collection
   * exists. Silent no-op when `typesense` npm package is not installed.
   */
  async initialize() {
    if (this._available) return;
    let TypesenseClient;
    try {
      TypesenseClient = require("typesense").Client;
    } catch {
      (0, import_errors.logDebug)("TypesenseIndex: typesense not installed \u2014 Typesense engine unavailable");
      return;
    }
    const binPath = getBinPath();
    if (!fs.existsSync(binPath)) {
      const url = getBinaryUrl();
      if (!url) {
        (0, import_errors.logDebug)("TypesenseIndex: no Typesense binary available for this platform");
        this._failReason = "Typesense binary not available for this platform";
        return;
      }
      process.stdout.write(`  Downloading Typesense ${TYPESENSE_VERSION} binary (~37 MB, cached for future use)\u2026
`);
      try {
        await downloadBinary(url, binPath);
        process.stdout.write(`  Typesense binary ready.
`);
      } catch (err) {
        process.stdout.write(`  Typesense binary download failed: ${String(err)}
`);
        (0, import_errors.logError)("TypesenseIndex: binary download failed", { error: String(err) });
        this._failReason = `binary download failed: ${String(err)}`;
        return;
      }
    }
    const saved = this.readState();
    if (saved) {
      const alive = this.isProcessAlive(saved.pid);
      if (alive) {
        try {
          await waitReady(saved.apiPort, 5e3);
          this.client = new TypesenseClient(makeClientConfig(saved.apiPort));
          await this.ensureCollection();
          this._available = true;
          this._ownedProcess = false;
          (0, import_errors.logDebug)("TypesenseIndex: reused running server", { pid: saved.pid, port: saved.apiPort });
          return;
        } catch {
          (0, import_errors.logDebug)("TypesenseIndex: stale process detected, killing", { pid: saved.pid });
          try {
            process.kill(saved.pid, "SIGKILL");
          } catch {
          }
          await new Promise((r) => setTimeout(r, 500));
        }
      }
      this.clearState();
    }
    let startupLog = "";
    try {
      fs.mkdirSync(this.storagePath, { recursive: true });
      for (const sub of ["db", "meta"]) {
        const lockFile = path.join(this.storagePath, sub, "LOCK");
        try {
          fs.unlinkSync(lockFile);
        } catch {
        }
      }
      const [apiPort, peeringPort] = await getFreePorts(2);
      const { spawn } = require("child_process");
      this.child = spawn(binPath, [
        `--data-dir=${this.storagePath}`,
        `--api-key=${API_KEY}`,
        `--api-port=${apiPort}`,
        `--peering-port=${peeringPort}`,
        "--enable-cors"
      ], { stdio: ["ignore", "pipe", "pipe"] });
      const onLog = (chunk) => {
        startupLog += chunk.toString("utf8");
      };
      this.child.stdout?.on("data", onLog);
      this.child.stderr?.on("data", onLog);
      this.child.unref();
      process.stdout.write(`  Starting Typesense server on port ${apiPort}\u2026
`);
      const exitPromise = new Promise((_, reject) => {
        this.child.once("exit", (code, signal) => {
          this.clearState();
          const out = startupLog.trim();
          const detail = out ? `
${out}` : "";
          reject(new Error(`Typesense process exited (code=${code}, signal=${signal})${detail}`));
        });
      });
      await Promise.race([waitReady(apiPort, HEALTH_TIMEOUT_MS), exitPromise]);
      this.child.stdout?.destroy();
      this.child.stderr?.destroy();
      process.stdout.write(`  Typesense server ready.
`);
      this.writeState({ pid: this.child.pid, apiPort, peeringPort });
      this.client = new TypesenseClient(makeClientConfig(apiPort));
      await this.ensureCollection();
      this._available = true;
      this._ownedProcess = true;
      (0, import_errors.logDebug)("TypesenseIndex: ready", { storagePath: this.storagePath, port: apiPort, dim: this.dim });
    } catch (err) {
      const msg = String(err);
      const log = (startupLog ?? "").trim();
      const detail = log ? `
  Server output:
${log.split("\n").map((l) => `    ${l}`).join("\n")}` : "";
      process.stdout.write(`  Typesense initialization failed: ${msg}${detail}
`);
      (0, import_errors.logError)("TypesenseIndex: initialization failed", { error: msg });
      this._failReason = msg;
      try {
        this.child?.kill();
      } catch {
      }
    }
  }
  async ensureCollection() {
    const exists = await this.client.collections(COLLECTION).exists();
    if (!exists) {
      await this.client.collections().create({
        name: COLLECTION,
        fields: [
          { name: "node_id", type: "string" },
          { name: "name", type: "string" },
          { name: "kind", type: "string", facet: true },
          { name: "file_path", type: "string" },
          { name: "signature", type: "string" },
          { name: "vector", type: "float[]", num_dim: this.dim }
        ]
      });
    }
  }
  /**
   * Insert or update a node's embedding. Typesense's upsert overwrites the
   * document if the `id` already exists.
   */
  async upsert(node, embedding) {
    if (!this._available || !this.client) return;
    try {
      await this.client.collections(COLLECTION).documents().upsert({
        id: toDocId(node.id),
        node_id: node.id,
        name: node.name,
        kind: node.kind,
        file_path: node.filePath,
        signature: node.signature ?? "",
        vector: Array.from(embedding)
      });
    } catch (err) {
      (0, import_errors.logWarn)("TypesenseIndex: upsert failed", { nodeId: node.id, error: String(err) });
    }
  }
  /**
   * Bulk upsert a batch of nodes. Uses Typesense's import endpoint (single
   * HTTP request per batch) to avoid connection churn on large codebases.
   */
  async bulkUpsert(nodes, embeddings) {
    if (!this._available || !this.client || nodes.length === 0) return;
    const docs = nodes.map((node, i) => ({
      id: toDocId(node.id),
      node_id: node.id,
      name: node.name,
      kind: node.kind,
      file_path: node.filePath,
      signature: node.signature ?? "",
      vector: Array.from(embeddings[i])
    }));
    try {
      await this.client.collections(COLLECTION).documents().import(docs, { action: "upsert" });
    } catch (err) {
      (0, import_errors.logWarn)("TypesenseIndex: bulkUpsert failed", { count: docs.length, error: String(err) });
    }
  }
  /**
   * Remove a document from the collection.
   */
  async delete(nodeId) {
    if (!this._available || !this.client) return;
    try {
      await this.client.collections(COLLECTION).documents(toDocId(nodeId)).delete();
    } catch (err) {
      const msg = String(err);
      if (!msg.includes("404") && !msg.includes("ObjectNotFound")) {
        (0, import_errors.logWarn)("TypesenseIndex: delete failed", { nodeId, error: msg });
      }
    }
  }
  /**
   * ANN vector search. Returns node IDs ordered by cosine similarity (descending).
   */
  async search(queryVec, topN = 10) {
    if (!this._available || !this.client) return [];
    try {
      const result = await this.client.multiSearch.perform(
        {
          searches: [{
            collection: COLLECTION,
            q: "*",
            vector_query: `vector:([${Array.from(queryVec).join(",")}], k:${topN})`,
            per_page: topN,
            include_fields: "node_id"
          }]
        },
        {}
      );
      const hits = result?.results?.[0]?.hits ?? [];
      return hits.map((h) => h.document.node_id);
    } catch (err) {
      (0, import_errors.logWarn)("TypesenseIndex: search failed", { error: String(err) });
      return [];
    }
  }
  /**
   * Return all node IDs currently stored in the collection via paginated export.
   */
  async getEmbeddedNodeIds() {
    if (!this._available || !this.client) return [];
    const ids = [];
    try {
      const jsonl = await this.client.collections(COLLECTION).documents().export({ include_fields: "node_id", batch_size: 1e3 });
      for (const line of jsonl.split("\n")) {
        if (!line.trim()) continue;
        try {
          const doc = JSON.parse(line);
          if (doc.node_id) ids.push(doc.node_id);
        } catch {
        }
      }
    } catch {
      return ids;
    }
    return ids;
  }
  /** Number of documents currently in the collection. */
  async count() {
    if (!this._available || !this.client) return 0;
    try {
      const result = await this.client.collections(COLLECTION).documents().search({
        q: "*",
        per_page: 0
      });
      return result.found ?? 0;
    } catch {
      return 0;
    }
  }
  /**
   * Disconnect the client. The Typesense server keeps running as a background
   * daemon so the next command can reconnect instantly via the state file.
   */
  close() {
    this._available = false;
    this.child = null;
    this.client = null;
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  TypesenseIndex
});
//# sourceMappingURL=typesense-index.js.map
