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

// src/boxes.ts
import { mkdir, writeFile, readFile, readdir } from "fs/promises";
import path from "path";
import { createHash } from "crypto";
var BOX_DIR = "boxes";
var STATE_DIR = "state";
var TRACES_FILE = "traces.json";
var OPEN_BOX_STATE_FILE = "open-box.json";
function topicOverlapScore(a, b) {
  if (a.length === 0 || b.length === 0) return 0;
  const setA = new Set(a.map((t) => t.toLowerCase()));
  const setB = new Set(b.map((t) => t.toLowerCase()));
  const intersection = [...setA].filter((t) => setB.has(t)).length;
  const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
  return union === 0 ? 0 : intersection / union;
}
function makeBoxId() {
  return `box-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
}
function makeTraceId(topics) {
  const key = topics.slice().sort().join(",");
  return `trace-${createHash("sha256").update(key).digest("hex").slice(0, 8)}`;
}
function serializeBoxFrontmatter(fm) {
  const lines = [
    "---",
    `id: ${fm.id}`,
    `memoryKind: ${fm.memoryKind}`,
    `createdAt: ${fm.createdAt}`,
    `sealedAt: ${fm.sealedAt}`,
    `sealReason: ${fm.sealReason}`,
    `topics: [${fm.topics.map((t) => `"${t}"`).join(", ")}]`,
    `memoryIds: [${fm.memoryIds.map((m) => `"${m}"`).join(", ")}]`
  ];
  if (fm.sessionKey) lines.push(`sessionKey: ${fm.sessionKey}`);
  if (fm.traceId) lines.push(`traceId: ${fm.traceId}`);
  if (fm.goal) lines.push(`goal: ${fm.goal.replace(/[\r\n]+/g, " ")}`);
  if (fm.toolsUsed?.length) lines.push(`toolsUsed: [${fm.toolsUsed.map((t) => `"${t}"`).join(", ")}]`);
  if (fm.outcome) lines.push(`outcome: ${fm.outcome}`);
  lines.push("---");
  return lines.join("\n");
}
function parseBoxFrontmatter(raw) {
  const match = raw.match(/^---\n([\s\S]*?)\n---/);
  if (!match) return null;
  const fmBlock = match[1];
  const fm = {};
  for (const line of fmBlock.split("\n")) {
    const colonIdx = line.indexOf(":");
    if (colonIdx === -1) continue;
    fm[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
  }
  const parseArray = (val) => {
    if (!val) return [];
    const m = val.match(/\[(.*)]/);
    if (!m) return [];
    return m[1].split(",").map((s) => s.trim().replace(/^"|"$/g, "")).filter(Boolean);
  };
  const outcome = fm.outcome;
  return {
    id: fm.id ?? "",
    memoryKind: "box",
    createdAt: fm.createdAt ?? "",
    sealedAt: fm.sealedAt ?? "",
    sealReason: fm.sealReason ?? "forced",
    sessionKey: fm.sessionKey,
    topics: parseArray(fm.topics),
    memoryIds: parseArray(fm.memoryIds),
    traceId: fm.traceId,
    goal: fm.goal || void 0,
    toolsUsed: fm.toolsUsed ? parseArray(fm.toolsUsed) : void 0,
    outcome: outcome && ["success", "failure", "partial", "unknown"].includes(outcome) ? outcome : void 0
  };
}
var BoxBuilder = class {
  baseDir;
  cfg;
  openBox = null;
  stateLoaded = false;
  constructor(baseDir, cfg) {
    this.baseDir = baseDir;
    this.cfg = cfg;
  }
  get boxBaseDir() {
    return path.join(this.baseDir, BOX_DIR);
  }
  get stateDir() {
    return path.join(this.baseDir, STATE_DIR);
  }
  get openBoxStatePath() {
    return path.join(this.stateDir, OPEN_BOX_STATE_FILE);
  }
  get tracesPath() {
    return path.join(this.stateDir, TRACES_FILE);
  }
  // ── State persistence ────────────────────────────────────────────────────
  async loadOpenBox() {
    if (this.stateLoaded) return;
    this.stateLoaded = true;
    try {
      const raw = await readFile(this.openBoxStatePath, "utf-8");
      this.openBox = JSON.parse(raw);
    } catch {
      this.openBox = null;
    }
  }
  async saveOpenBox() {
    await mkdir(this.stateDir, { recursive: true });
    if (this.openBox) {
      await writeFile(this.openBoxStatePath, JSON.stringify(this.openBox, null, 2), "utf-8");
    } else {
      try {
        await writeFile(this.openBoxStatePath, "null", "utf-8");
      } catch {
      }
    }
  }
  async loadTraceIndex() {
    try {
      const raw = await readFile(this.tracesPath, "utf-8");
      const parsed = JSON.parse(raw);
      parsed.traceLastSeen ??= {};
      return parsed;
    } catch {
      return { traces: {}, boxToTrace: {}, traceTopics: {}, traceLastSeen: {} };
    }
  }
  async saveTraceIndex(idx) {
    try {
      await mkdir(this.stateDir, { recursive: true });
      await writeFile(this.tracesPath, JSON.stringify(idx, null, 2), "utf-8");
    } catch (err) {
      log.warn(`[engram/boxes] Failed to save trace index: ${err.message}`);
    }
  }
  // ── Core logic ────────────────────────────────────────────────────────────
  /**
   * Called after each extraction run.
   * Decides whether to seal the current open box and/or start a new one.
   */
  async onExtraction(event) {
    if (!this.cfg.memoryBoxesEnabled) return;
    await this.loadOpenBox();
    const newTopics = event.topics.filter(Boolean);
    const now = new Date(event.timestamp);
    const nowMs = now.getTime();
    if (this.openBox) {
      const lastActivity = new Date(this.openBox.lastActivityAt).getTime();
      const timeGapMs = nowMs - lastActivity;
      const overlap = topicOverlapScore(this.openBox.topics, newTopics);
      const topicShifted = newTopics.length > 0 && overlap < this.cfg.boxTopicShiftThreshold;
      const timeExpired = timeGapMs >= this.cfg.boxTimeGapMs;
      const tooManyMemories = this.openBox.memoryIds.length + event.memoryIds.length > this.cfg.boxMaxMemories;
      if (tooManyMemories) {
        const topicSet = /* @__PURE__ */ new Set([...this.openBox.topics, ...newTopics]);
        this.openBox.topics = [...topicSet];
        this.openBox.memoryIds.push(...event.memoryIds);
        if (event.toolsUsed?.length) {
          const toolSet = /* @__PURE__ */ new Set([...this.openBox.toolsUsed ?? [], ...event.toolsUsed]);
          this.openBox.toolsUsed = [...toolSet];
        }
        await this.sealCurrent("max_memories");
      } else if (topicShifted) {
        await this.sealCurrent("topic_shift");
        this.openBox = this.newBox(event, now.toISOString());
        await this.saveOpenBox();
      } else if (timeExpired) {
        await this.sealCurrent("time_gap");
        this.openBox = this.newBox(event, now.toISOString());
        await this.saveOpenBox();
      } else {
        this.openBox.memoryIds.push(...event.memoryIds);
        const topicSet = /* @__PURE__ */ new Set([...this.openBox.topics, ...newTopics]);
        this.openBox.topics = [...topicSet];
        this.openBox.lastActivityAt = now.toISOString();
        if (event.toolsUsed?.length) {
          const toolSet = /* @__PURE__ */ new Set([...this.openBox.toolsUsed ?? [], ...event.toolsUsed]);
          this.openBox.toolsUsed = [...toolSet];
        }
        await this.saveOpenBox();
      }
    } else {
      this.openBox = this.newBox(event, now.toISOString());
      if (this.openBox.memoryIds.length > this.cfg.boxMaxMemories) {
        await this.sealCurrent("max_memories");
      } else {
        await this.saveOpenBox();
      }
    }
  }
  newBox(event, ts) {
    return {
      id: makeBoxId(),
      createdAt: ts,
      lastActivityAt: ts,
      topics: event.topics.filter(Boolean),
      memoryIds: [...event.memoryIds],
      goal: event.goal,
      toolsUsed: event.toolsUsed?.length ? [...event.toolsUsed] : void 0
    };
  }
  /**
   * Seal the current open box and write it to disk.
   * Also runs trace weaving if enabled.
   */
  async sealCurrent(reason) {
    await this.loadOpenBox();
    if (!this.openBox) return null;
    const box = this.openBox;
    this.openBox = null;
    if (box.memoryIds.length === 0 && box.topics.length === 0) {
      await this.saveOpenBox();
      return null;
    }
    const sealedAt = (/* @__PURE__ */ new Date()).toISOString();
    const day = sealedAt.slice(0, 10);
    const dir = path.join(this.boxBaseDir, day);
    await mkdir(dir, { recursive: true });
    let traceId;
    if (this.cfg.traceWeaverEnabled && box.topics.length > 0) {
      traceId = await this.resolveTrace(box.id, box.topics);
    }
    const fm = {
      id: box.id,
      memoryKind: "box",
      createdAt: box.createdAt,
      sealedAt,
      sealReason: reason,
      topics: box.topics,
      memoryIds: box.memoryIds,
      traceId,
      goal: box.goal,
      toolsUsed: box.toolsUsed?.length ? box.toolsUsed : void 0,
      outcome: "unknown"
    };
    const content = `${serializeBoxFrontmatter(fm)}

<!-- Topics: ${box.topics.join(", ")} | Memories: ${box.memoryIds.length} -->
`;
    const filePath = path.join(dir, `${box.id}.md`);
    await writeFile(filePath, content, "utf-8");
    log.debug(`[boxes] sealed box ${box.id} (${reason}): ${box.memoryIds.length} memories, topics=[${box.topics.join(",")}]`);
    await this.saveOpenBox();
    return box.id;
  }
  // ── Trace Weaving ─────────────────────────────────────────────────────────
  /**
   * Find an existing trace that matches box topics, or create a new trace.
   * Returns the traceId to assign to this box.
   */
  async resolveTrace(boxId, topics) {
    const idx = await this.loadTraceIndex();
    const lookbackMs = this.cfg.traceWeaverLookbackDays * 24 * 60 * 60 * 1e3;
    const cutoff = new Date(Date.now() - lookbackMs);
    let bestTraceId;
    let bestScore = 0;
    for (const [tid, traceTopics] of Object.entries(idx.traceTopics)) {
      const lastSeen = idx.traceLastSeen[tid];
      if (lastSeen && new Date(lastSeen) < cutoff) continue;
      const score = topicOverlapScore(topics, traceTopics);
      if (score >= this.cfg.traceWeaverOverlapThreshold && score > bestScore) {
        bestScore = score;
        bestTraceId = tid;
      }
    }
    const traceId = bestTraceId ?? makeTraceId(topics);
    const now = (/* @__PURE__ */ new Date()).toISOString();
    if (!idx.traces[traceId]) idx.traces[traceId] = [];
    idx.traces[traceId].push(boxId);
    idx.boxToTrace[boxId] = traceId;
    idx.traceLastSeen[traceId] = now;
    if (idx.traceTopics[traceId]) {
      const merged = /* @__PURE__ */ new Set([...idx.traceTopics[traceId], ...topics]);
      idx.traceTopics[traceId] = [...merged];
    } else {
      idx.traceTopics[traceId] = [...topics];
    }
    await this.saveTraceIndex(idx);
    return traceId;
  }
  // ── Recall ────────────────────────────────────────────────────────────────
  /**
   * Read all sealed boxes from the last N days for recall injection.
   */
  async readRecentBoxes(days) {
    const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
    const cutoffDateStr = cutoff.toISOString().slice(0, 10);
    let topEntries;
    try {
      topEntries = await readdir(this.boxBaseDir, { withFileTypes: true, encoding: "utf-8" });
    } catch {
      return [];
    }
    const recentDirs = topEntries.filter((e) => e.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(e.name) && e.name >= cutoffDateStr).map((e) => path.join(this.boxBaseDir, e.name));
    const legacyEntries = topEntries.filter(
      (e) => !e.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(e.name)
    );
    const boxes = [];
    const readDir = async (dir) => {
      let files;
      try {
        files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
      } catch {
        return;
      }
      const results = await Promise.all(
        files.map(async (f) => {
          try {
            const raw = await readFile(path.join(dir, f), "utf-8");
            const parsed = parseBoxFrontmatter(raw);
            return parsed && new Date(parsed.sealedAt) >= cutoff ? parsed : null;
          } catch {
            return null;
          }
        })
      );
      for (const r of results) {
        if (r !== null) boxes.push(r);
      }
    };
    await Promise.all(recentDirs.map(readDir));
    for (const e of legacyEntries) {
      const full = path.join(this.boxBaseDir, e.name);
      if (e.isDirectory()) {
        await readDir(full);
      } else if (e.name.endsWith(".md")) {
        try {
          const raw = await readFile(full, "utf-8");
          const parsed = parseBoxFrontmatter(raw);
          if (parsed && new Date(parsed.sealedAt) >= cutoff) boxes.push(parsed);
        } catch {
        }
      }
    }
    boxes.sort((a, b) => new Date(b.sealedAt).getTime() - new Date(a.sealedAt).getTime());
    return boxes;
  }
};

export {
  BOX_DIR,
  topicOverlapScore,
  parseBoxFrontmatter,
  BoxBuilder
};
//# sourceMappingURL=chunk-URB2WSKZ.js.map