#!/usr/bin/env node
import {
  redactJson
} from "./chunk-SLSWPBK3.js";

// src/session/session-manager.ts
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, renameSync, openSync, readSync, closeSync } from "fs";
import { join } from "path";
import { v4 as uuidv4 } from "uuid";

// src/core/types.ts
function getContentText(content) {
  if (typeof content === "string") return content;
  return content.filter((p) => p.type === "text").map((p) => p.text).join("");
}

// src/session/session.ts
function makeBranchId() {
  return Math.random().toString(16).slice(2, 8);
}
function messagesEqual(a, b) {
  if (a.role !== b.role) return false;
  if (JSON.stringify(a.content) !== JSON.stringify(b.content)) return false;
  if (JSON.stringify(a.toolCalls ?? null) !== JSON.stringify(b.toolCalls ?? null)) return false;
  return true;
}
var Session = class _Session {
  id;
  provider;
  model;
  created;
  updated;
  messages = [];
  title;
  /**
   * 标题是否已由 AI 生成过短摘要（v0.4.126+）。
   * - false / undefined：title 仍是 addMessage 截取的首条用户消息前 50 字（heuristic）
   * - true：已由 maybeGenerateSessionTitle 替换为 AI 摘要，不再二次生成
   *
   * 持久化在 toJSON / fromJSON 中，重启后不会重复请求。
   */
  titleAiGenerated;
  tokenUsage = {
    inputTokens: 0,
    outputTokens: 0,
    cacheCreationTokens: 0,
    cacheReadTokens: 0
  };
  checkpoints = [];
  // ── B2 Branches (v0.4.74+) ──────────────────────────────────────
  /**
   * All branches in this session. The 'main' branch is auto-created and
   * represents the linear conversation for pre-B2 sessions.
   */
  branches = [];
  /** Currently active branch — its messages live in `this.messages`. */
  activeBranchId = "main";
  /**
   * Stashed message arrays for INACTIVE branches. The active branch's
   * messages are always in `this.messages`, never duplicated here.
   */
  _inactiveBranchMessages = /* @__PURE__ */ new Map();
  constructor(id, provider, model) {
    this.id = id;
    this.provider = provider;
    this.model = model;
    this.created = /* @__PURE__ */ new Date();
    this.updated = /* @__PURE__ */ new Date();
    this.branches.push({
      id: "main",
      title: "main",
      parentBranchId: null,
      parentMessageIndex: 0,
      created: this.created
    });
  }
  /**
   * 更新 session 关联的 provider 和 model（在 /provider 或 /model 切换时调用）。
   * 保留对话历史和所有状态，仅更新元数据。
   */
  updateProvider(provider, model) {
    this.provider = provider;
    this.model = model;
    this.updated = /* @__PURE__ */ new Date();
  }
  addMessage(message) {
    this.messages.push(message);
    this.updated = /* @__PURE__ */ new Date();
    if (!this.title && message.role === "user") {
      this.title = getContentText(message.content).slice(0, 50).replace(/\n/g, " ");
    }
  }
  addTokenUsage(usage) {
    this.tokenUsage.inputTokens += usage.inputTokens;
    this.tokenUsage.outputTokens += usage.outputTokens;
    this.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens ?? 0;
    this.tokenUsage.cacheReadTokens += usage.cacheReadTokens ?? 0;
  }
  clear() {
    this.messages = [];
    this.title = void 0;
    this.titleAiGenerated = false;
    this.tokenUsage = {
      inputTokens: 0,
      outputTokens: 0,
      cacheCreationTokens: 0,
      cacheReadTokens: 0
    };
    this.updated = /* @__PURE__ */ new Date();
  }
  /**
   * 上下文压缩：用摘要消息替换旧消息，保留最近 keepLast 条。
   *
   * Tool-history-aware: if the cut point lands inside a tool round
   * (assistant+toolCalls followed by tool results), expand to keep the
   * entire round intact. This prevents orphaned tool results.
   *
   * 压缩后消息结构：
   *   [summaryMsg（user）, ackMsg（assistant）, ...最近 N 条原始消息]
   *
   * @returns 被删除的消息条数
   */
  compact(summaryMsg, ackMsg, keepLast) {
    let cutIndex = this.messages.length - keepLast;
    if (cutIndex <= 0) {
      return 0;
    }
    while (cutIndex > 0 && this.messages[cutIndex]?.role === "tool") {
      cutIndex--;
    }
    const preserved = this.messages.slice(cutIndex);
    const removedCount = cutIndex;
    this.messages = [summaryMsg, ackMsg, ...preserved];
    this.updated = /* @__PURE__ */ new Date();
    return removedCount;
  }
  /** 在当前消息位置创建检查点 */
  createCheckpoint(name) {
    this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
    this.checkpoints.push({
      name,
      messageIndex: this.messages.length,
      timestamp: /* @__PURE__ */ new Date()
    });
  }
  /** 恢复到指定检查点：截断消息到检查点位置，移除后续检查点 */
  restoreCheckpoint(name) {
    const cp = this.checkpoints.find((c) => c.name === name);
    if (!cp) return false;
    this.messages = this.messages.slice(0, cp.messageIndex);
    this.checkpoints = this.checkpoints.filter((c) => c.messageIndex <= cp.messageIndex);
    this.updated = /* @__PURE__ */ new Date();
    return true;
  }
  listCheckpoints() {
    return [...this.checkpoints];
  }
  deleteCheckpoint(name) {
    const len = this.checkpoints.length;
    this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
    return this.checkpoints.length < len;
  }
  // ── B2 Branch operations ────────────────────────────────────────
  /** Deep-clone a messages array (matches `Session.fork` semantics). */
  static cloneMessages(msgs) {
    return msgs.map((m) => {
      const cloned = { ...m };
      if (Array.isArray(cloned.content)) {
        cloned.content = cloned.content.map(
          (part) => typeof part === "object" && part !== null ? { ...part } : part
        );
      }
      if (cloned.toolCalls) {
        cloned.toolCalls = cloned.toolCalls.map((tc) => ({ ...tc }));
      }
      return cloned;
    });
  }
  /** List all branches (metadata only). */
  listBranches() {
    return this.branches.map((b) => ({ ...b }));
  }
  /** Current active branch metadata. */
  getActiveBranch() {
    const b = this.branches.find((b2) => b2.id === this.activeBranchId);
    if (!b) {
      this.activeBranchId = this.branches[0]?.id ?? "main";
      return this.branches[0] ?? { id: "main", title: "main", parentBranchId: null, parentMessageIndex: 0, created: /* @__PURE__ */ new Date() };
    }
    return b;
  }
  /**
   * Create a new branch by forking the active branch at message index
   * `fromIndex`. Copies `messages[0..fromIndex]` into the new branch
   * and switches to it. The original active branch is preserved intact
   * in the stash.
   *
   * @returns new branch id
   * @throws if fromIndex is out of range
   */
  createBranch(fromIndex, title) {
    if (fromIndex < 0 || fromIndex > this.messages.length) {
      throw new Error(
        `createBranch: fromIndex ${fromIndex} out of range [0, ${this.messages.length}]`
      );
    }
    this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
    const id = makeBranchId();
    const meta = {
      id,
      title: title || `branch-${this.branches.length + 1}`,
      parentBranchId: this.activeBranchId,
      parentMessageIndex: fromIndex,
      created: /* @__PURE__ */ new Date()
    };
    this.branches.push(meta);
    this.messages = _Session.cloneMessages(this.messages.slice(0, fromIndex));
    this.activeBranchId = id;
    this.updated = /* @__PURE__ */ new Date();
    return id;
  }
  /**
   * Switch the active branch. Stashes current messages under the old
   * active id and loads the target branch's messages into `this.messages`.
   *
   * @returns true if switched, false if id not found or already active
   */
  switchBranch(id) {
    if (id === this.activeBranchId) return false;
    if (!this.branches.some((b) => b.id === id)) return false;
    this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
    const target = this._inactiveBranchMessages.get(id) ?? [];
    this._inactiveBranchMessages.delete(id);
    this.messages = target;
    this.activeBranchId = id;
    this.updated = /* @__PURE__ */ new Date();
    return true;
  }
  /**
   * Delete a branch by id. Cannot delete the active branch or the last
   * remaining branch. If other branches list this one as parent, their
   * parent pointer is retargeted to this branch's parent (transparent
   * to callers — branches still form a valid forest).
   *
   * @returns true if deleted
   */
  deleteBranch(id) {
    if (id === this.activeBranchId) return false;
    if (this.branches.length <= 1) return false;
    const idx = this.branches.findIndex((b) => b.id === id);
    if (idx === -1) return false;
    const deleted = this.branches[idx];
    for (const b of this.branches) {
      if (b.parentBranchId === id) {
        b.parentBranchId = deleted.parentBranchId;
      }
    }
    this.branches.splice(idx, 1);
    this._inactiveBranchMessages.delete(id);
    this.updated = /* @__PURE__ */ new Date();
    return true;
  }
  /** Rename a branch (affects only display title). */
  renameBranch(id, newTitle) {
    const b = this.branches.find((b2) => b2.id === id);
    if (!b) return false;
    b.title = newTitle;
    this.updated = /* @__PURE__ */ new Date();
    return true;
  }
  /**
   * Resolve a user-supplied branch reference (v0.4.81+).
   *
   * Accepts the 6-hex branch id, the branch's display title, or an unambiguous
   * id prefix (≥2 chars). This exists because users naturally type the title
   * they chose at `/branch new`, not the auto-generated id — every other
   * `/branch <sub>` used to error on that with "not found".
   *
   * Resolution order:
   *   1. Exact id match
   *   2. Exact title match (unique)
   *   3. Id prefix match (unique, length ≥ 2)
   *
   * @returns `{ ok: true, id }` on success; on failure `{ ok: false, reason, matches }`
   *          where `matches` lists candidates for ambiguous input so callers
   *          can render a helpful error.
   */
  resolveBranchRef(ref) {
    if (!ref) return { ok: false, reason: "not-found", matches: [] };
    const byId = this.branches.find((b) => b.id === ref);
    if (byId) return { ok: true, id: byId.id };
    const byTitle = this.branches.filter((b) => b.title === ref);
    if (byTitle.length === 1) return { ok: true, id: byTitle[0].id };
    if (byTitle.length > 1) return { ok: false, reason: "ambiguous", matches: byTitle };
    if (ref.length >= 2) {
      const byPrefix = this.branches.filter((b) => b.id.startsWith(ref));
      if (byPrefix.length === 1) return { ok: true, id: byPrefix[0].id };
      if (byPrefix.length > 1) return { ok: false, reason: "ambiguous", matches: byPrefix };
    }
    return { ok: false, reason: "not-found", matches: [] };
  }
  /** Messages of any branch (active or inactive) — read-only copy. */
  getBranchMessages(id) {
    if (id === this.activeBranchId) return this.messages.slice();
    const m = this._inactiveBranchMessages.get(id);
    return m ? m.slice() : null;
  }
  // ── B3 Branch diff + cherry-pick (v0.4.80+) ────────────────────────
  /**
   * Compare messages between two branches. Finds the longest common prefix
   * by message equality (role + content + toolCalls shape) — NOT by fork
   * point metadata, so user-edited histories still diff cleanly.
   *
   * @param sourceId  Branch to compare
   * @param targetId  Branch to compare against (defaults to active branch)
   * @returns BranchDiff, or null if either branch id is unknown
   */
  diffBranches(sourceId, targetId) {
    const tgt = targetId ?? this.activeBranchId;
    const src = this.getBranchMessages(sourceId);
    const dst = this.getBranchMessages(tgt);
    if (!src || !dst) return null;
    let i = 0;
    while (i < src.length && i < dst.length && messagesEqual(src[i], dst[i])) i++;
    return {
      sourceId,
      targetId: tgt,
      commonPrefix: i,
      sourceOnly: src.slice(i),
      targetOnly: dst.slice(i)
    };
  }
  /**
   * Copy a single message from another branch into the active branch
   * (appended at the end). Timestamp is reset to now so it's clear when
   * the cherry-pick happened.
   *
   * @returns the cloned message, or null if sourceId / msgIndex is invalid
   */
  cherryPickMessage(sourceId, msgIndex) {
    const src = this.getBranchMessages(sourceId);
    if (!src) return null;
    if (msgIndex < 0 || msgIndex >= src.length) return null;
    const orig = src[msgIndex];
    const copy = {
      ...orig,
      timestamp: /* @__PURE__ */ new Date()
    };
    this.messages.push(copy);
    this.updated = /* @__PURE__ */ new Date();
    return copy;
  }
  getMeta() {
    return {
      id: this.id,
      provider: this.provider,
      model: this.model,
      messageCount: this.messages.length,
      created: this.created,
      updated: this.updated,
      title: this.title
    };
  }
  toJSON() {
    const serializeMessages = (msgs) => msgs.map((m) => {
      const out = {
        role: m.role,
        content: m.content,
        timestamp: m.timestamp.toISOString()
      };
      if (m.toolCalls) out.toolCalls = m.toolCalls;
      if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
      if (m.toolCallId) out.toolCallId = m.toolCallId;
      if (m.toolName) out.toolName = m.toolName;
      if (m.isError !== void 0) out.isError = m.isError;
      return out;
    });
    const branchMessages = {};
    for (const [id, msgs] of this._inactiveBranchMessages.entries()) {
      branchMessages[id] = serializeMessages(msgs);
    }
    return {
      id: this.id,
      provider: this.provider,
      model: this.model,
      // 7th audit P1: active-branch 消息数，放在 messages 数组之前，
      // 保证落在文件前 1KB 内 —— readSessionMeta() 只读 header 即可拿到，
      // 不再为数消息把整个 session 文件读进内存。
      messageCount: this.messages.length,
      created: this.created.toISOString(),
      updated: this.updated.toISOString(),
      title: this.title,
      titleAiGenerated: this.titleAiGenerated ?? false,
      tokenUsage: { ...this.tokenUsage },
      checkpoints: this.checkpoints.map((c) => ({
        name: c.name,
        messageIndex: c.messageIndex,
        timestamp: c.timestamp.toISOString()
      })),
      // B2 Branches (v0.4.74+). Omitted for sessions with only the default
      // 'main' branch and no stashed messages (keeps file size identical
      // to pre-B2 for the common case).
      ...this.branches.length > 1 || this._inactiveBranchMessages.size > 0 ? {
        activeBranchId: this.activeBranchId,
        branches: this.branches.map((b) => ({
          id: b.id,
          title: b.title,
          parentBranchId: b.parentBranchId,
          parentMessageIndex: b.parentMessageIndex,
          created: b.created.toISOString()
        })),
        branchMessages
      } : {},
      messages: serializeMessages(this.messages)
    };
  }
  /**
   * 从现有 session 分叉创建新 session。
   *
   * 复制 messages[0..messageCount]，保留范围内的 checkpoints。
   * 新 session 拥有独立 UUID，title 默认 "Fork of <原标题>"。
   *
   * @param original  原始 session
   * @param newId     新 session 的 UUID
   * @param messageCount  复制的消息数量（0 = 空 session，> messages.length 则取全部）
   * @param newTitle  可选的新标题
   */
  static fork(original, newId, messageCount, newTitle) {
    const forked = new _Session(newId, original.provider, original.model);
    forked.title = newTitle ?? (original.title ? `Fork of ${original.title}` : void 0);
    const clampedCount = Math.min(Math.max(messageCount, 0), original.messages.length);
    forked.messages = original.messages.slice(0, clampedCount).map((m) => {
      const cloned = { ...m };
      if (Array.isArray(cloned.content)) {
        cloned.content = cloned.content.map(
          (part) => typeof part === "object" && part !== null ? { ...part } : part
        );
      }
      return cloned;
    });
    forked.checkpoints = original.checkpoints.filter((c) => c.messageIndex <= clampedCount).map((c) => ({ ...c, timestamp: new Date(c.timestamp.getTime()) }));
    forked.updated = /* @__PURE__ */ new Date();
    return forked;
  }
  /**
   * 从磁盘 JSON 数据恢复 Session 实例。
   * 添加运行时校验：损坏或不兼容的历史文件会抛出明确错误，而非 TypeError 崩溃。
   */
  static fromJSON(data) {
    const d = data;
    if (!d || typeof d !== "object") {
      throw new Error("Invalid session data: expected an object");
    }
    if (typeof d.id !== "string" || typeof d.provider !== "string" || typeof d.model !== "string") {
      throw new Error("Invalid session data: missing or invalid id/provider/model fields");
    }
    if (!Array.isArray(d.messages)) {
      throw new Error("Invalid session data: messages is not an array");
    }
    const session = new _Session(d.id, d.provider, d.model);
    session.title = typeof d.title === "string" ? d.title : void 0;
    session.titleAiGenerated = d.titleAiGenerated === true;
    const created = new Date(d.created);
    const updated = new Date(d.updated);
    session.created = isNaN(created.getTime()) ? /* @__PURE__ */ new Date() : created;
    session.updated = isNaN(updated.getTime()) ? /* @__PURE__ */ new Date() : updated;
    const tu = d.tokenUsage;
    if (tu && typeof tu === "object") {
      session.tokenUsage = {
        inputTokens: typeof tu.inputTokens === "number" ? tu.inputTokens : 0,
        outputTokens: typeof tu.outputTokens === "number" ? tu.outputTokens : 0,
        cacheCreationTokens: typeof tu.cacheCreationTokens === "number" ? tu.cacheCreationTokens : 0,
        cacheReadTokens: typeof tu.cacheReadTokens === "number" ? tu.cacheReadTokens : 0
      };
    }
    if (Array.isArray(d.checkpoints)) {
      session.checkpoints = d.checkpoints.map((c) => ({
        name: String(c.name ?? ""),
        messageIndex: typeof c.messageIndex === "number" ? c.messageIndex : 0,
        timestamp: new Date(c.timestamp)
      }));
    }
    const deserializeMessages = (arr) => arr.map((m) => {
      const ts = new Date(m.timestamp);
      const msg = {
        role: m.role ?? "user",
        content: Array.isArray(m.content) ? m.content : String(m.content ?? ""),
        timestamp: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
      };
      if (Array.isArray(m.toolCalls)) msg.toolCalls = m.toolCalls;
      if (typeof m.reasoningContent === "string") msg.reasoningContent = m.reasoningContent;
      if (typeof m.toolCallId === "string") msg.toolCallId = m.toolCallId;
      if (typeof m.toolName === "string") msg.toolName = m.toolName;
      if (typeof m.isError === "boolean") msg.isError = m.isError;
      return msg;
    });
    session.messages = deserializeMessages(d.messages);
    if (Array.isArray(d.branches) && d.branches.length > 0) {
      session.branches = d.branches.map((b) => {
        const ts = new Date(b.created);
        return {
          id: String(b.id ?? "main"),
          title: String(b.title ?? b.id ?? "main"),
          parentBranchId: typeof b.parentBranchId === "string" ? b.parentBranchId : null,
          parentMessageIndex: typeof b.parentMessageIndex === "number" ? b.parentMessageIndex : 0,
          created: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
        };
      });
      session.activeBranchId = typeof d.activeBranchId === "string" ? d.activeBranchId : session.branches[0]?.id ?? "main";
      const bm = d.branchMessages;
      if (bm && typeof bm === "object") {
        for (const [id, arr] of Object.entries(bm)) {
          if (id === session.activeBranchId) continue;
          if (!Array.isArray(arr)) continue;
          session._inactiveBranchMessages.set(
            id,
            deserializeMessages(arr)
          );
        }
      }
    }
    return session;
  }
};

// src/session/session-manager.ts
function safeDate(value) {
  const d = new Date(value);
  return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
}
function extractJsonField(header, field) {
  const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
  const m = header.match(re);
  return m ? m[1] : void 0;
}
function extractJsonNumberField(header, field) {
  const m = header.match(new RegExp(`"${field}"\\s*:\\s*(\\d+)`));
  return m ? Number(m[1]) : void 0;
}
var SessionManager = class {
  _current = null;
  historyDir;
  config;
  /** Last save's redaction hit count — exposed for /security status reporting */
  lastRedactionHits = 0;
  constructor(config) {
    this.config = config;
    this.historyDir = config.getHistoryDir();
  }
  /**
   * Build redaction options from config. Returns `{ enabled: false }` when
   * `security.redactOnSave` is off or `security.mode` is 'off'.
   */
  redactOptionsForSave() {
    const security = this.config.get("security");
    if (!security || !security.redactOnSave || security.mode === "off") {
      return { enabled: false };
    }
    return {
      enabled: true,
      customRegexes: security.customPatterns ?? []
    };
  }
  get current() {
    return this._current;
  }
  createSession(provider, model) {
    const session = new Session(uuidv4(), provider, model);
    this._current = session;
    return session;
  }
  /**
   * 直接设置当前会话（用于从内存缓存恢复未保存的会话）。
   * 与 `loadSession` 不同，此方法不读取磁盘，也不抛出错误。
   * Web 多 Tab 场景下，SessionHandler 会维护一份未保存会话的内存缓存，
   * 切换 Tab 时通过此方法将缓存中的会话设为当前会话，避免"Session not found"。
   */
  setCurrent(session) {
    this._current = session;
  }
  /** 清除当前会话引用（下次访问将触发 lazy 创建）。 */
  clearCurrent() {
    this._current = null;
  }
  async save() {
    if (!this._current) return;
    mkdirSync(this.historyDir, { recursive: true });
    const filePath = join(this.historyDir, `${this._current.id}.json`);
    const raw = this._current.toJSON();
    const opts = this.redactOptionsForSave();
    const { value: payload, hits } = redactJson(raw, opts);
    this.lastRedactionHits = hits.length;
    const tmpPath = filePath + ".tmp";
    writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
    renameSync(tmpPath, filePath);
  }
  loadSession(id) {
    const filePath = join(this.historyDir, `${id}.json`);
    if (!existsSync(filePath)) {
      throw new Error(`Session ${id} not found`);
    }
    let data;
    try {
      data = JSON.parse(readFileSync(filePath, "utf-8"));
    } catch (err) {
      throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
    }
    const session = Session.fromJSON(data);
    this._current = session;
    return session;
  }
  listSessions() {
    if (!existsSync(this.historyDir)) return [];
    const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
    const metas = [];
    for (const file of files) {
      try {
        const meta = this.readSessionMeta(join(this.historyDir, file));
        if (meta) metas.push(meta);
      } catch (err) {
        process.stderr.write(
          `[Warning] Skipping corrupted session file "${file}": ${err instanceof Error ? err.message : String(err)}
`
        );
      }
    }
    return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
  }
  /**
   * P1-B: Read only the first ~1KB of a session file to extract metadata fields.
   * Session JSON format puts id/provider/model/created/updated/title before the
   * large "messages" array, so a small header read suffices for metadata extraction.
   * Falls back to full file read if header parsing fails.
   */
  readSessionMeta(filePath) {
    const HEADER_SIZE = 1024;
    let header;
    try {
      const fd = openSync(filePath, "r");
      const buf = Buffer.alloc(HEADER_SIZE);
      const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
      closeSync(fd);
      header = buf.toString("utf-8", 0, bytesRead);
    } catch {
      return null;
    }
    const id = extractJsonField(header, "id");
    const provider = extractJsonField(header, "provider");
    const model = extractJsonField(header, "model");
    const created = extractJsonField(header, "created");
    const updated = extractJsonField(header, "updated");
    const title = extractJsonField(header, "title");
    if (id && provider && model) {
      let messageCount = extractJsonNumberField(header, "messageCount");
      if (messageCount === void 0) {
        messageCount = 0;
        try {
          const full = readFileSync(filePath, "utf-8");
          const matches = full.match(/"role"\s*:/g);
          messageCount = matches ? matches.length : 0;
        } catch {
        }
      }
      return {
        id,
        provider,
        model,
        messageCount,
        created: safeDate(created),
        updated: safeDate(updated),
        title: title || void 0
      };
    }
    const data = JSON.parse(readFileSync(filePath, "utf-8"));
    return {
      id: data.id,
      provider: data.provider,
      model: data.model,
      messageCount: data.messages?.length ?? 0,
      created: safeDate(data.created),
      updated: safeDate(data.updated),
      title: data.title
    };
  }
  deleteSession(id) {
    const filePath = join(this.historyDir, `${id}.json`);
    if (!existsSync(filePath)) return false;
    try {
      unlinkSync(filePath);
      if (this._current && this._current.id === id) {
        this._current = null;
      }
      return true;
    } catch {
      return false;
    }
  }
  /**
   * 从当前 session 分叉创建新 session。
   *
   * 先保存原始 session（保留完整历史），然后创建分叉（截取到 messageCount），
   * 将分叉设为当前 session 并保存。
   *
   * @param messageCount  复制的消息数量
   * @param title         可选的新标题
   * @returns 新的分叉 session
   */
  async forkSession(messageCount, title) {
    if (!this._current) {
      throw new Error("No active session to fork");
    }
    await this.save();
    const forked = Session.fork(this._current, uuidv4(), messageCount, title);
    this._current = forked;
    await this.save();
    return forked;
  }
  /**
   * 跨 session 全文搜索。
   * 遍历所有历史 JSON 文件，逐条匹配消息内容（不区分大小写），
   * 每个 session 最多返回 3 条匹配片段，全局最多 maxResults 个 session。
   */
  searchMessages(query, maxResults = 20) {
    if (!existsSync(this.historyDir)) return [];
    const q = query.toLowerCase();
    const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
    const results = [];
    for (const filePath of files) {
      if (results.length >= maxResults) break;
      try {
        const data = JSON.parse(readFileSync(filePath, "utf-8"));
        const messages = data.messages ?? [];
        const matches = [];
        for (const msg of messages) {
          if (matches.length >= 3) break;
          let text = "";
          if (typeof msg.content === "string") {
            text = msg.content;
          } else if (Array.isArray(msg.content)) {
            text = msg.content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
          }
          const lowerText = text.toLowerCase();
          const idx = lowerText.indexOf(q);
          if (idx !== -1) {
            const start = Math.max(0, idx - 30);
            const end = Math.min(text.length, idx + query.length + 60);
            const snippet = (start > 0 ? "\u2026" : "") + text.slice(start, end).replace(/\n/g, " ") + (end < text.length ? "\u2026" : "");
            matches.push({ role: msg.role, snippet });
          }
        }
        if (matches.length > 0) {
          results.push({
            sessionMeta: {
              id: data.id,
              provider: data.provider,
              model: data.model,
              messageCount: messages.length,
              created: safeDate(data.created),
              updated: safeDate(data.updated),
              title: data.title
            },
            matches
          });
        }
      } catch (err) {
        process.stderr.write(
          `[Warning] Skipping corrupted session file "${filePath}": ${err instanceof Error ? err.message : String(err)}
`
        );
      }
    }
    return results.sort((a, b) => b.sessionMeta.updated.getTime() - a.sessionMeta.updated.getTime());
  }
};

export {
  getContentText,
  SessionManager
};
