import {
  EMBEDDING_DIM,
  embed,
  embedOne
} from "./chunk-JV5N65KN.js";

// src/memory/chat-index.ts
import fs from "fs";
import path from "path";
import os from "os";
import crypto from "crypto";

// src/security/redactor.ts
var DEFAULT_PATTERNS = [
  // password: xxx / password = xxx / password="xxx"
  // Covers YAML / JSON / shell-ish / env-file forms.
  { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
  // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
  { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
  // JDBC/PG/MySQL/Mongo connection strings with inline credentials
  // postgresql://user:pass@host/db → redact pass
  { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
  // Anthropic API keys
  { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
  // L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
  // Two segments separated by a dot, each safely identifiable by length
  // and char class. Conservative on the lower bound so we don't eat
  // version strings like `1.0.0` or filenames.
  { kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
  // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
  { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
  // GitHub personal access tokens
  { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
  { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
  { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
  // Slack tokens
  { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
  { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
  // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
  // we only catch the ID because secret key alone is indistinguishable from random base64.
  { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
  // Google API keys
  { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
  // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
  { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
  // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
  { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
  // Bearer <token> in Authorization headers
  { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
  // Private key PEM blocks — catch the header+footer together
  { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
];
var MAX_CUSTOM = 32;
var MAX_PATTERN_LEN = 500;
var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
function render(placeholder, kind) {
  return placeholder.replace("{kind}", kind);
}
function redactString(input, options) {
  if (!options.enabled || !input) return { redacted: input, hits: [] };
  const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
  const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
  const patterns = [
    ...options.patterns ?? DEFAULT_PATTERNS,
    ...customSrcs.flatMap((src, i) => {
      if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
      try {
        const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
        const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
        if (SUSPICIOUS_REDOS.test(body)) return [];
        const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
        return [{ kind: `custom-${i}`, regex }];
      } catch {
        return [];
      }
    })
  ];
  let redacted = input;
  const hits = [];
  for (const { kind, regex } of patterns) {
    const rx = new RegExp(regex.source, regex.flags);
    const captureCount = new RegExp(rx.source + "|").exec("").length - 1;
    redacted = redacted.replace(rx, (...args) => {
      const match = args[0];
      const g1 = captureCount >= 1 ? args[1] : void 0;
      const g2 = captureCount >= 2 ? args[2] : void 0;
      const offset = args[1 + captureCount];
      if (captureCount >= 2 && typeof g2 === "string") {
        hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
        return `${g1}${render(placeholder, kind)}`;
      }
      hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
      return render(placeholder, kind);
    });
  }
  return { redacted, hits };
}
function redactJson(value, options) {
  if (!options.enabled) return { value, hits: [] };
  const allHits = [];
  function walk(v) {
    if (typeof v === "string") {
      const r = redactString(v, options);
      allHits.push(...r.hits);
      return r.redacted;
    }
    if (Array.isArray(v)) return v.map(walk);
    if (v && typeof v === "object") {
      const out = {};
      for (const [k, vv] of Object.entries(v)) {
        out[k] = walk(vv);
      }
      return out;
    }
    return v;
  }
  const redacted = walk(value);
  return { value: redacted, hits: allHits };
}

// src/memory/chat-index.ts
var MEMORY_DIR_NAME = "memory-index";
var CHUNKS_FILE = "chunks.json";
var VECTORS_FILE = "vectors.vec";
var VEC_MAGIC = 1094929750;
var VEC_VERSION = 1;
var VEC_HEADER_BYTES = 16;
function memoryIndexDir() {
  return path.join(os.homedir(), ".aicli", MEMORY_DIR_NAME);
}
function chunksPath() {
  return path.join(memoryIndexDir(), CHUNKS_FILE);
}
function vectorsPath() {
  return path.join(memoryIndexDir(), VECTORS_FILE);
}
function historyDir() {
  return path.join(os.homedir(), ".aicli", "history");
}
var MAX_CHUNK_CHARS = 1200;
var MIN_CHUNK_CHARS = 40;
function extractMessageText(msg) {
  if (typeof msg.content === "string") return msg.content;
  if (Array.isArray(msg.content)) {
    return msg.content.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n");
  }
  return "";
}
function chunkSession(session) {
  const chunks = [];
  let pending = null;
  const flush = () => {
    if (!pending) return;
    const rawText = pending.parts.join("\n").trim();
    if (rawText.length < MIN_CHUNK_CHARS) {
      pending = null;
      return;
    }
    const { redacted } = redactString(rawText, { enabled: true });
    const id = crypto.createHash("sha1").update(`${session.id}|${pending.start}|${pending.end}|${redacted.length}`).digest("hex").slice(0, 16);
    chunks.push({
      id,
      sessionId: session.id,
      sessionTitle: session.title,
      provider: session.provider,
      model: session.model,
      startMessageIdx: pending.start,
      endMessageIdx: pending.end,
      text: redacted,
      timestamp: pending.latestTs,
      roles: pending.roles
    });
    pending = null;
  };
  for (let i = 0; i < session.messages.length; i++) {
    const m = session.messages[i];
    if (m.role !== "user" && m.role !== "assistant" && m.role !== "system") continue;
    const text = extractMessageText(m).trim();
    if (!text) continue;
    const ts = m.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
    const prefix = m.role === "user" ? "[USER] " : m.role === "assistant" ? "[AI] " : "[SYS] ";
    const part = `${prefix}${text}`;
    if (!pending) {
      pending = { start: i, end: i, parts: [part], roles: [m.role], latestTs: ts };
      continue;
    }
    const projected = pending.parts.reduce((n, p) => n + p.length + 1, 0) + part.length;
    if (projected > MAX_CHUNK_CHARS) {
      flush();
      pending = { start: i, end: i, parts: [part], roles: [m.role], latestTs: ts };
    } else {
      pending.parts.push(part);
      pending.end = i;
      pending.roles.push(m.role);
      pending.latestTs = ts;
    }
  }
  flush();
  return chunks;
}
function writeVectorsFile(chunks, vectors) {
  if (chunks.length * EMBEDDING_DIM !== vectors.length) {
    throw new Error(
      `writeVectorsFile: length mismatch \u2014 ${chunks.length} chunks vs ${vectors.length / EMBEDDING_DIM} vectors`
    );
  }
  const dir = memoryIndexDir();
  fs.mkdirSync(dir, { recursive: true });
  const totalBytes = VEC_HEADER_BYTES + vectors.byteLength;
  const buf = Buffer.alloc(totalBytes);
  buf.writeUInt32LE(VEC_MAGIC, 0);
  buf.writeUInt32LE(VEC_VERSION, 4);
  buf.writeUInt32LE(chunks.length, 8);
  buf.writeUInt32LE(EMBEDDING_DIM, 12);
  Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength).copy(buf, VEC_HEADER_BYTES);
  const target = vectorsPath();
  const tmp = `${target}.tmp`;
  fs.writeFileSync(tmp, buf);
  fs.renameSync(tmp, target);
}
function readVectorsFile(expectedCount) {
  const p = vectorsPath();
  if (!fs.existsSync(p)) return null;
  let buf;
  try {
    buf = fs.readFileSync(p);
  } catch {
    return null;
  }
  if (buf.length < VEC_HEADER_BYTES) return null;
  const magic = buf.readUInt32LE(0);
  const version = buf.readUInt32LE(4);
  const count = buf.readUInt32LE(8);
  const dim = buf.readUInt32LE(12);
  if (magic !== VEC_MAGIC || version !== VEC_VERSION || dim !== EMBEDDING_DIM) return null;
  if (count !== expectedCount) return null;
  const expected = VEC_HEADER_BYTES + count * dim * 4;
  if (buf.length !== expected) return null;
  return new Float32Array(
    buf.buffer.slice(buf.byteOffset + VEC_HEADER_BYTES, buf.byteOffset + expected)
  );
}
function writeIndexFile(idx) {
  const dir = memoryIndexDir();
  fs.mkdirSync(dir, { recursive: true });
  const target = chunksPath();
  const tmp = `${target}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf-8");
  fs.renameSync(tmp, target);
}
function readIndexFile() {
  const p = chunksPath();
  if (!fs.existsSync(p)) return null;
  try {
    const raw = fs.readFileSync(p, "utf-8");
    const data = JSON.parse(raw);
    if (data.version !== 1) return null;
    return data;
  } catch {
    return null;
  }
}
function loadChatIndex() {
  const idx = readIndexFile();
  if (!idx) return null;
  const vectors = readVectorsFile(idx.chunks.length);
  if (!vectors) return null;
  return { idx, vectors };
}
function clearChatIndex() {
  try {
    if (fs.existsSync(chunksPath())) fs.unlinkSync(chunksPath());
  } catch {
  }
  try {
    if (fs.existsSync(vectorsPath())) fs.unlinkSync(vectorsPath());
  } catch {
  }
}
function listSessionFiles() {
  const dir = historyDir();
  if (!fs.existsSync(dir)) return [];
  const out = [];
  for (const name of fs.readdirSync(dir)) {
    if (!name.endsWith(".json")) continue;
    const id = name.replace(/\.json$/, "");
    const p = path.join(dir, name);
    try {
      const st = fs.statSync(p);
      out.push({ id, path: p, mtime: st.mtimeMs });
    } catch {
    }
  }
  return out;
}
function readSession(p) {
  try {
    const data = JSON.parse(fs.readFileSync(p, "utf-8"));
    if (!data.id || !Array.isArray(data.messages)) return null;
    return data;
  } catch {
    return null;
  }
}
async function buildChatIndex(options = {}) {
  const t0 = Date.now();
  const onProgress = options.onProgress ?? (() => {
  });
  onProgress({ stage: "scanning" });
  const files = listSessionFiles();
  const existing = options.full ? null : loadChatIndex();
  const prevMtimes = existing?.idx.sessionMtimes ?? {};
  const prevChunksBySession = /* @__PURE__ */ new Map();
  const prevVectorsByChunkId = /* @__PURE__ */ new Map();
  if (existing) {
    for (let i = 0; i < existing.idx.chunks.length; i++) {
      const c = existing.idx.chunks[i];
      const arr = prevChunksBySession.get(c.sessionId) ?? [];
      arr.push(c);
      prevChunksBySession.set(c.sessionId, arr);
      prevVectorsByChunkId.set(
        c.id,
        existing.vectors.slice(i * EMBEDDING_DIM, (i + 1) * EMBEDDING_DIM)
      );
    }
  }
  onProgress({ stage: "chunking" });
  const stats = {
    sessionsScanned: files.length,
    sessionsIndexed: 0,
    sessionsSkipped: 0,
    chunksTotal: 0,
    chunksAdded: 0,
    chunksRemoved: 0,
    durationMs: 0
  };
  const newMtimes = {};
  const finalChunks = [];
  const finalVectors = [];
  const toEmbed = [];
  for (const f of files) {
    newMtimes[f.id] = f.mtime;
    const prevMtime = prevMtimes[f.id];
    if (prevMtime === f.mtime && prevChunksBySession.has(f.id)) {
      stats.sessionsSkipped++;
      const cached = prevChunksBySession.get(f.id);
      for (const c of cached) {
        const v = prevVectorsByChunkId.get(c.id);
        if (!v) continue;
        finalChunks.push(c);
        finalVectors.push(v);
      }
      continue;
    }
    const sess = readSession(f.path);
    if (!sess) continue;
    stats.sessionsIndexed++;
    const chunks = chunkSession(sess);
    for (const c of chunks) {
      finalChunks.push(c);
      toEmbed.push(c);
      stats.chunksAdded++;
    }
  }
  if (existing) {
    for (const prevId of Object.keys(prevMtimes)) {
      if (!(prevId in newMtimes)) {
        const removed = prevChunksBySession.get(prevId) ?? [];
        stats.chunksRemoved += removed.length;
      }
    }
  }
  stats.chunksTotal = finalChunks.length;
  const BATCH = 16;
  onProgress({ stage: "embedding", processed: 0, total: toEmbed.length });
  const newVectorsByChunkId = /* @__PURE__ */ new Map();
  for (let i = 0; i < toEmbed.length; i += BATCH) {
    const batch = toEmbed.slice(i, i + BATCH);
    const vecs = await embed(batch.map((c) => c.text));
    for (let j = 0; j < batch.length; j++) {
      newVectorsByChunkId.set(batch[j].id, vecs[j]);
    }
    onProgress({ stage: "embedding", processed: Math.min(i + BATCH, toEmbed.length), total: toEmbed.length });
  }
  const flat = new Float32Array(finalChunks.length * EMBEDDING_DIM);
  for (let i = 0; i < finalChunks.length; i++) {
    const c = finalChunks[i];
    const v = newVectorsByChunkId.get(c.id) ?? prevVectorsByChunkId.get(c.id);
    if (!v || v.length !== EMBEDDING_DIM) {
      continue;
    }
    flat.set(v, i * EMBEDDING_DIM);
  }
  onProgress({ stage: "saving" });
  const idx = {
    version: 1,
    built: (/* @__PURE__ */ new Date()).toISOString(),
    model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
    sessionMtimes: newMtimes,
    chunks: finalChunks
  };
  writeIndexFile(idx);
  writeVectorsFile(finalChunks, flat);
  stats.durationMs = Date.now() - t0;
  onProgress({ stage: "done" });
  return stats;
}
async function searchChatMemory(query, options = {}) {
  const topK = options.topK ?? 5;
  const minScore = options.minScore ?? 0.25;
  const loaded = loadChatIndex();
  if (!loaded || loaded.idx.chunks.length === 0) return [];
  const { idx, vectors } = loaded;
  const { redacted } = redactString(query, { enabled: true });
  const qvec = await embedOne(redacted);
  const candidates = [];
  for (let i = 0; i < idx.chunks.length; i++) {
    const c = idx.chunks[i];
    if (options.sessionId && c.sessionId !== options.sessionId) continue;
    if (options.excludeSessionId && c.sessionId === options.excludeSessionId) continue;
    let score = 0;
    const base = i * EMBEDDING_DIM;
    for (let d = 0; d < EMBEDDING_DIM; d++) {
      score += vectors[base + d] * qvec[d];
    }
    if (score < minScore) continue;
    candidates.push({ chunk: c, score });
  }
  candidates.sort((a, b) => b.score - a.score);
  return candidates.slice(0, topK);
}
function getChatIndexStatus() {
  const status = {
    exists: false,
    chunks: 0,
    sessions: 0,
    vecFileSizeBytes: 0,
    chunksFileSizeBytes: 0
  };
  try {
    if (fs.existsSync(vectorsPath())) status.vecFileSizeBytes = fs.statSync(vectorsPath()).size;
    if (fs.existsSync(chunksPath())) status.chunksFileSizeBytes = fs.statSync(chunksPath()).size;
  } catch {
  }
  const idx = readIndexFile();
  if (!idx) return status;
  status.exists = true;
  status.chunks = idx.chunks.length;
  status.sessions = Object.keys(idx.sessionMtimes).length;
  status.built = idx.built;
  status.model = idx.model;
  return status;
}

export {
  redactJson,
  chunkSession,
  loadChatIndex,
  clearChatIndex,
  buildChatIndex,
  searchChatMemory,
  getChatIndexStatus
};
