// Feeds new prompts.jsonl records into the `ft` FTS5 knowledge base so an agent can
// query "what has the owner told me about X" across sessions. Incremental: a byte-offset
// cursor into the journal means only newly-appended records are pushed to `ft` each run.
//
// Granularity: one `ft index --stdin --label prompt/<session>/<seq>` document per prompt.
// The label must be unique per document — `ft index` upserts by label, so a shared label
// across prompts silently drops all but the last one (verified empirically). Redaction
// (prompt-redact.mjs) is applied here, at the index seam, not at journal-write time.

import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";

import { stateRoot, journalPath, readNewLines } from "./prompt-journal.mjs";
import { redact } from "./prompt-redact.mjs";

export function ftCursorPath(env = process.env) {
  return path.join(stateRoot(env), ".ft-index-offset");
}

// Exported so the search-side CLI pins `ft search` to the exact same shard.
export function ftShardCwd(env = process.env) {
  return stateRoot(env);
}

export function loadFtOffset(cpath) {
  try {
    const raw = fs.readFileSync(cpath, "utf8").trim();
    const n = Number(raw);
    return Number.isFinite(n) && n >= 0 ? n : 0;
  } catch {
    return 0;
  }
}

export function saveFtOffset(cpath, offset) {
  fs.mkdirSync(path.dirname(cpath), { recursive: true, mode: 0o700 });
  const tmp = `${cpath}.${process.pid}.tmp`;
  fs.writeFileSync(tmp, `${offset}\n`, { encoding: "utf8", mode: 0o600 });
  fs.renameSync(tmp, cpath);
}

// `ft` shards its KB by the invoking process's cwd (verified empirically: the same
// content indexed from two different cwds landed in two different shard databases
// under ~/.ft/kb/, and `ft search` only searches the shard for ITS OWN cwd). Every
// prompt document — regardless of which project/session it came from — must be
// indexed (and later searched) from the SAME fixed directory, or cross-session query
// silently only covers whatever project happened to run the last `index`/`search`.
function defaultFtIndex({ label, content }, env = process.env) {
  const result = spawnSync("ft", ["index", "--stdin", "--label", label], {
    input: content,
    encoding: "utf8",
    cwd: ftShardCwd(env),
  });
  if (result.status !== 0) {
    throw new Error(`ft index failed (label=${label}): ${result.stderr || result.error || "unknown error"}`);
  }
}

function docContent(record, redactedText) {
  return `${record.ts} [session ${record.session}${record.cwd ? " " + record.cwd : ""}]\n${redactedText}`;
}

// Indexes every not-yet-indexed journal record (skips noise/ack turns — they add no
// search value). Stops at the first `ft index` failure so the offset cursor never skips
// past a record that was never actually indexed; returns what it did before stopping.
export function indexNew(env = process.env, deps = {}) {
  const jpath = journalPath(env);
  const cpath = ftCursorPath(env);
  const ftIndex = deps.ftIndex ?? defaultFtIndex;
  const offset = loadFtOffset(cpath);

  const { lines, nextOffset } = readNewLines(jpath, offset);
  let indexed = 0;
  let skippedNoise = 0;
  let cursor = offset;
  let bytesConsumed = 0;

  try {
    for (const line of lines) {
      const lineBytes = Buffer.byteLength(line, "utf8") + 1; // + the newline readNewLines stripped
      let record;
      try {
        record = JSON.parse(line);
      } catch {
        cursor += lineBytes;
        bytesConsumed += lineBytes;
        continue;
      }
      if (record && record.noise) {
        cursor += lineBytes;
        bytesConsumed += lineBytes;
        skippedNoise += 1;
        continue;
      }
      const { text: redactedText, redactedKinds } = redact(record.text ?? "");
      const label = `prompt/${record.session}/${record.seq}`;
      ftIndex({ label, content: docContent(record, redactedText) });
      if (redactedKinds.length && deps.onRedacted) deps.onRedacted(record, redactedKinds);
      cursor += lineBytes;
      bytesConsumed += lineBytes;
      indexed += 1;
    }
  } finally {
    if (bytesConsumed > 0) saveFtOffset(cpath, cursor);
  }
  return { indexed, skippedNoise, bytesConsumed, reachedOffset: cursor, journalEndOffset: nextOffset };
}
