// Shared "is this a real owner-typed turn" classifier.
// Single source of truth for compact-context-restore.mjs (SessionStart compaction
// recovery) and the prompt-journal backfill/indexer (modules/workstation/bin/prompt-journal).
// Never fork this logic — a second copy silently changes what counts as a prompt.

import os from "node:os";
import path from "node:path";

export function expandHome(p) {
  if (typeof p !== "string" || !p) return p;
  if (p === "~") return os.homedir();
  if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
  return p;
}

export function isPureHarnessSignal(s) {
  const t = s.trimStart();
  return (
    t.startsWith("<task-notification>") ||
    t.startsWith("<local-command-stdout>") ||
    t.startsWith("Caveat:") ||
    t.startsWith("[Request interrupted")
  );
}

export function cleanText(raw) {
  let s = String(raw);
  s = s.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
  s = s.replace(/<command-[a-z-]+>[\s\S]*?<\/command-[a-z-]+>/g, "");
  s = s.replace(/<local-command-[a-z-]+>[\s\S]*?<\/local-command-[a-z-]+>/g, "");
  s = s.replace(
    /^\\?(?:Pre|Post)ToolUse[^\n]*(?:\n|\s)*(?:Failed with non-blocking status code:\s*)?(?:\/bin\/sh:[^\n]*?:\s*(?:not found|Permission denied|cannot execute[^\n]*?)\s*)?/i,
    ""
  );
  return s.trim();
}

export const NOISE = new Set([
  "resume", "continue", "go", "ok", "okay", "k", "kk", "yes", "y", "yep",
  "yeah", "yup", "ya", "yea", "sure", "proceed", "next", "do it", "go on",
  "go ahead", "carry on", "keep going", "continue please", "done", "thanks",
  "thank you", "ty", "cool", "great", "nice", "perfect", "fine", "no", "nope",
  "continue from where you left off", "pick up", "pickup", "pick up where you left off",
]);

export function isNoise(s) {
  const n = s.toLowerCase().replace(/[.!?,;:]+$/, "").replace(/\s+/g, " ").trim();
  return n.length < 2 || NOISE.has(n);
}

// rec.type === "attachment" (queued human command) or rec.type === "user" honouring
// promptSource=="typed" / origin.kind=="human" / the live-transcript-journal version
// marker. Returns the raw (unfiltered) string content, or null when rec is not an
// owner-typed turn at all (tool_result, assistant text, non-human origin, ...).
export function extractUserString(rec) {
  if (!rec) return null;
  const att = rec.type === "attachment" ? rec.attachment : null;
  if (att) {
    if (att.type !== "queued_command") return null;
    if (att.commandMode !== "prompt") return null;
    if (!att.origin || att.origin.kind !== "human") return null;
    return typeof att.prompt === "string" ? att.prompt : null;
  }
  if (rec.type !== "user") return null;
  const journaled =
    typeof rec.version === "string" &&
    rec.version.startsWith("live-transcript-journal/") &&
    typeof rec.promptId === "string";
  const human = rec.promptSource === "typed" || (rec.origin && rec.origin.kind === "human") || journaled;
  if (!human) return null;
  const c = rec.message && rec.message.content;
  if (typeof c !== "string") return null;
  return c;
}

// Full pipeline: null when rec is not an owner-typed turn; otherwise the cleaned text
// plus whether it is harness/ack noise (e.g. "ok", "continue") — callers decide whether
// to keep or drop noise (a durable journal should keep it; a compaction hint should not).
export function classifyPromptRecord(rec) {
  const raw = extractUserString(rec);
  if (raw == null) return null;
  if (isPureHarnessSignal(raw)) return null;
  const cleaned = cleanText(raw);
  if (!cleaned) return null;
  return { text: cleaned, noise: isNoise(cleaned) };
}
