import {
  readEnvVar
} from "./chunk-MARWOCVP.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/embedding-fallback.ts
import path from "path";
import { mkdir, readFile, writeFile } from "fs/promises";
var DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
var EmbeddingTimeoutError = class extends Error {
  name = "EmbeddingTimeoutError";
  constructor(message) {
    super(message);
  }
};
var DEFAULT_EMBEDDING_LOOKUP_TIMEOUT_MS = 5e3;
var DEFAULT_EMBEDDING_INDEX_TIMEOUT_MS = 12e4;
function resolveEmbeddingLookupTimeoutMs() {
  const raw = readEnvVar("REMNIC_EMBEDDING_FETCH_TIMEOUT_MS");
  if (raw) {
    const parsed = Number(raw);
    if (Number.isFinite(parsed) && parsed > 0) {
      return Math.floor(parsed);
    }
  }
  return DEFAULT_EMBEDDING_LOOKUP_TIMEOUT_MS;
}
function resolveEmbeddingIndexTimeoutMs() {
  const raw = readEnvVar("REMNIC_EMBEDDING_INDEX_TIMEOUT_MS");
  if (raw) {
    const parsed = Number(raw);
    if (Number.isFinite(parsed) && parsed > 0) {
      return Math.floor(parsed);
    }
  }
  return DEFAULT_EMBEDDING_INDEX_TIMEOUT_MS;
}
var EmbeddingFallback = class {
  constructor(config) {
    this.config = config;
    this.indexPath = path.join(config.memoryDir, "state", "embeddings.json");
  }
  config;
  indexPath;
  loaded = null;
  async isAvailable() {
    return await this.resolveProvider() !== null;
  }
  /**
   * Embed an array of texts and return their embedding vectors.
   *
   * This is the public batch-embed interface used by semantic chunking
   * (Finding 1, PR #420 post-merge). Texts are grouped into batches of
   * `embeddingBatchSize` (from `semanticChunkingConfig`, default 32) and
   * each batch is dispatched concurrently via `Promise.all()`. This
   * preserves the semantic intent of `embeddingBatchSize` — without batching,
   * every text incurred a sequential HTTP round-trip, making the batch size
   * config ineffective. (PR #439 post-merge Finding 2.)
   *
   * If the provider is unavailable or any single embedding fails, the method
   * throws so the caller can fall back to recursive chunking.
   */
  async embedTexts(texts) {
    const provider = await this.resolveProvider();
    if (!provider) {
      throw new Error("Embedding provider is not available");
    }
    const batchSize = Math.max(
      1,
      this.config.semanticChunkingConfig?.embeddingBatchSize ?? 32
    );
    const vectors = [];
    for (let i = 0; i < texts.length; i += batchSize) {
      const batch = texts.slice(i, i + batchSize);
      const batchResults = await Promise.all(
        batch.map((text) => this.embed(text, provider, { mode: "lookup" }))
      );
      for (const vec of batchResults) {
        if (!vec) {
          throw new Error("Embedding returned null for input text");
        }
        vectors.push(vec);
      }
    }
    return vectors;
  }
  /**
   * Nearest-neighbor search against the embedding index.
   *
   * @param query         The query string to embed and search for.
   * @param limit         Max number of hits to return.
   * @param options       Optional filters.
   *   - `pathPrefix`   Restrict candidates to entries whose indexed `path`
   *                    starts with this prefix (relative to `memoryDir`).
   *                    Used by the semantic dedup guard to scope lookups
   *                    to the target namespace so a high-similarity hit
   *                    from a different namespace can't suppress a write
   *                    in the target namespace. Default: no filter.
   *   - `pathExcludePrefixes`
   *                    Exclude any entry whose indexed `path` starts with
   *                    any of these prefixes. Used for the default
   *                    namespace case: when the default namespace lives at
   *                    `memoryDir` root (legacy layout) we still want to
   *                    exclude `namespaces/<other>/…` entries.
   */
  async search(query, limit, options = {}) {
    const provider = await this.resolveProvider();
    if (!provider) return [];
    const index = await this.loadIndex(provider);
    const ids = Object.keys(index.entries);
    if (ids.length === 0) return [];
    let queryVector;
    try {
      queryVector = await this.embed(query, provider, { mode: "lookup" });
    } catch (err) {
      if (err instanceof EmbeddingTimeoutError) {
        if (options.throwOnTimeout) {
          throw err;
        }
        log.debug("embedding fallback search: timeout on lookup, returning [] (throwOnTimeout=false)");
        return [];
      }
      throw err;
    }
    if (!queryVector) return [];
    const includePrefix = normalizePathPrefix(options.pathPrefix);
    const excludePrefixes = (options.pathExcludePrefixes ?? []).map((p) => normalizePathPrefix(p)).filter((p) => typeof p === "string");
    const scored = ids.map((id) => {
      const entry = index.entries[id];
      return {
        id,
        path: entry.path,
        score: cosineSimilarity(queryVector, entry.vector)
      };
    }).filter((r) => {
      if (!Number.isFinite(r.score)) return false;
      const normalized = normalizeEntryPath(r.path);
      if (includePrefix !== void 0 && !normalized.startsWith(includePrefix)) {
        return false;
      }
      for (const excl of excludePrefixes) {
        if (normalized.startsWith(excl)) return false;
      }
      return true;
    }).sort((a, b) => b.score - a.score).slice(0, Math.max(1, limit));
    return scored;
  }
  async indexFile(memoryId, content, filePath) {
    const provider = await this.resolveProvider();
    if (!provider) return;
    const vector = await this.embed(content, provider, { mode: "index" });
    if (!vector) return;
    const index = await this.loadIndex(provider);
    const relPath = toMemoryRelativePath(this.config.memoryDir, filePath);
    index.entries[memoryId] = {
      vector,
      path: relPath
    };
    await this.saveIndex(index);
  }
  async removeFromIndex(memoryId) {
    const provider = await this.resolveProvider();
    if (!provider) return;
    const index = await this.loadIndex(provider);
    if (!index.entries[memoryId]) return;
    delete index.entries[memoryId];
    await this.saveIndex(index);
  }
  async resolveProvider() {
    if (!this.config.embeddingFallbackEnabled) return null;
    const preferred = this.config.embeddingFallbackProvider;
    const providers = preferred === "auto" ? ["openai", "local"] : [preferred];
    for (const p of providers) {
      if (p === "openai" && this.config.openaiApiKey) {
        const baseUrl = this.config.openaiBaseUrl ?? "https://api.openai.com/v1";
        return {
          type: "openai",
          model: DEFAULT_OPENAI_MODEL,
          endpoint: `${baseUrl.replace(/\/$/, "")}/embeddings`,
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${this.config.openaiApiKey}`
          }
        };
      }
      if (p === "local" && this.config.localLlmEnabled && this.config.localLlmUrl) {
        const base = this.config.localLlmUrl.replace(/\/$/, "");
        const endpoint = /\/v1$/i.test(base) ? `${base}/embeddings` : `${base}/v1/embeddings`;
        const headers = {
          "Content-Type": "application/json",
          ...this.config.localLlmHeaders ?? {}
        };
        if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {
          headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;
        }
        return {
          type: "local",
          model: this.config.localLlmModel || DEFAULT_OPENAI_MODEL,
          endpoint,
          headers
        };
      }
    }
    return null;
  }
  async embed(input, provider, options = {}) {
    const mode = options.mode ?? "lookup";
    const timeoutMs = mode === "index" ? resolveEmbeddingIndexTimeoutMs() : resolveEmbeddingLookupTimeoutMs();
    try {
      const res = await fetch(provider.endpoint, {
        method: "POST",
        headers: provider.headers,
        body: JSON.stringify({
          model: provider.model,
          input: input.slice(0, 8e3),
          encoding_format: "float"
        }),
        signal: AbortSignal.timeout(timeoutMs)
      });
      if (!res.ok) {
        log.debug(`embedding fallback request failed: ${provider.type} ${res.status}`);
        if (mode === "lookup") {
          throw new EmbeddingTimeoutError(
            `embedding backend returned ${res.status} (${provider.type})`
          );
        }
        return null;
      }
      const payload = await res.json();
      const vector = payload?.data?.[0]?.embedding;
      if (!Array.isArray(vector)) return null;
      return vector.map((n) => Number(n)).filter((n) => Number.isFinite(n));
    } catch (err) {
      if (err instanceof EmbeddingTimeoutError) {
        throw err;
      }
      const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
      if (isTimeout) {
        log.warn(
          `embedding fallback fetch timed out after ${timeoutMs}ms (${provider.type}, mode=${mode})`
        );
        if (mode === "lookup") {
          throw new EmbeddingTimeoutError(
            `embedding backend timed out after ${timeoutMs}ms (${provider.type})`
          );
        }
      } else {
        if (mode === "lookup") {
          log.warn(
            `embedding fallback transport error on lookup path (${provider.type}): ${err}`
          );
          throw new EmbeddingTimeoutError(
            `embedding backend transport failure (${provider.type}): ${err instanceof Error ? err.message : String(err)}`
          );
        }
        log.debug(`embedding fallback error: ${err}`);
      }
      return null;
    }
  }
  async loadIndex(provider) {
    if (this.loaded && this.loaded.provider === provider.type && this.loaded.model === provider.model) {
      return this.loaded;
    }
    try {
      const raw = await readFile(this.indexPath, "utf-8");
      const parsed = JSON.parse(raw);
      if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === "object") {
        this.loaded = {
          version: 1,
          provider: provider.type,
          model: provider.model,
          entries: parsed.entries
        };
        return this.loaded;
      }
    } catch {
    }
    this.loaded = {
      version: 1,
      provider: provider.type,
      model: provider.model,
      entries: {}
    };
    return this.loaded;
  }
  async saveIndex(index) {
    await mkdir(path.dirname(this.indexPath), { recursive: true });
    await writeFile(this.indexPath, JSON.stringify(index), "utf-8");
    this.loaded = index;
  }
};
function toMemoryRelativePath(memoryDir, filePath) {
  if (!path.isAbsolute(filePath)) return filePath;
  const rel = path.relative(memoryDir, filePath);
  return rel.startsWith("..") ? filePath : rel;
}
function normalizeEntryPath(p) {
  let out = p.replace(/\\/g, "/");
  if (out.startsWith("./")) out = out.slice(2);
  return out;
}
function normalizePathPrefix(prefix) {
  if (prefix === void 0 || prefix === null) return void 0;
  let p = String(prefix).replace(/\\/g, "/");
  if (p.startsWith("./")) p = p.slice(2);
  if (p.length === 0) return void 0;
  if (!p.endsWith("/")) p = `${p}/`;
  return p;
}
function cosineSimilarity(a, b) {
  const n = Math.min(a.length, b.length);
  if (n === 0) return 0;
  let dot = 0;
  let normA = 0;
  let normB = 0;
  for (let i = 0; i < n; i++) {
    const av = a[i] ?? 0;
    const bv = b[i] ?? 0;
    dot += av * bv;
    normA += av * av;
    normB += bv * bv;
  }
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
  if (denom === 0) return 0;
  return dot / denom;
}

export {
  EmbeddingTimeoutError,
  EmbeddingFallback
};
//# sourceMappingURL=chunk-Z2E7VW55.js.map