'use strict';

/**
 * Silent-reply token registry — Item D of prompt-catalog deep-dive.
 *
 * Inspired by OpenClaw's `src/auto-reply/tokens.ts`. The agent can return
 * one of these tokens to mean "nothing to say / no action needed" without
 * burning tokens on filler text. Loops + chat surfaces strip these from
 * output before forwarding anywhere visible.
 *
 * Used by:
 *   - boot-check.js (Item B): "If BOOT.md asks nothing, reply NO_REPLY"
 *   - loop-prompt-discipline.js (Item C): default fast-exit token
 *   - chat.js: agent can opt out of replying when there's nothing useful
 *     to say (e.g. heartbeat-style chat ticks)
 */

/** Boot phase + one-shot agent turns where the agent has nothing to say. */
const SILENT_REPLY_TOKEN = 'NO_REPLY';

/** Heartbeat-style periodic checks: the agent saw HEARTBEAT.md, has nothing actionable. */
const HEARTBEAT_OK_TOKEN = 'HEARTBEAT_OK';

/** Wall-E-native default for autonomous loop ticks (matches existing loop-prompt-discipline). */
const NO_ACTION_TOKEN = 'NO_ACTION';

/**
 * Boot phase acknowledgement (matches existing loops/boot.js). The boot
 * pass uses a distinct token because callers care to distinguish "ran
 * successfully" from "had nothing to do" — both are silent from the
 * channel's perspective but have different operational meaning.
 */
const BOOT_OK_TOKEN = 'BOOT_OK';

/**
 * All recognized silent-reply tokens. Used by `stripSilentReply()` and
 * `isSilentReply()` to handle any of them uniformly.
 */
const SILENT_REPLY_TOKENS = Object.freeze([
  SILENT_REPLY_TOKEN,
  HEARTBEAT_OK_TOKEN,
  NO_ACTION_TOKEN,
  BOOT_OK_TOKEN,
  'no_reply', // case-insensitive variant per OpenClaw BOOT.md docs
  'NoReply',
]);

/**
 * Returns true if `text` is exactly (after trim) one of the silent-reply
 * tokens. Loops use this to decide "the agent had nothing to say, skip
 * downstream processing."
 */
function isSilentReply(text) {
  if (typeof text !== 'string') return false;
  const t = text.trim();
  if (!t) return false;
  for (const token of SILENT_REPLY_TOKENS) {
    if (t === token) return true;
  }
  // Case-insensitive match for the literal sentinels — `NO_REPLY` is meant
  // to be exact, but the docs also accept `no_reply` so be lenient.
  const upper = t.toUpperCase();
  if (
    upper === SILENT_REPLY_TOKEN ||
    upper === HEARTBEAT_OK_TOKEN ||
    upper === NO_ACTION_TOKEN ||
    upper === BOOT_OK_TOKEN
  ) {
    return true;
  }
  return false;
}

/**
 * If `text` ends with a silent-reply token, strip it (and any trailing
 * whitespace). Useful when the agent emits "Sent the DM. NO_REPLY" — we
 * want to preserve "Sent the DM." for logs but not forward the token.
 *
 * Returns { text: cleaned, hadToken: boolean }.
 */
function stripSilentReply(text) {
  if (typeof text !== 'string') return { text: text || '', hadToken: false };
  const trimmed = text.trim();
  for (const token of SILENT_REPLY_TOKENS) {
    if (trimmed === token) return { text: '', hadToken: true };
    // Token at end of message
    if (trimmed.endsWith(token)) {
      const cleaned = trimmed.slice(0, -token.length).trimEnd().replace(/[.,;:]+$/, '').trimEnd();
      return { text: cleaned, hadToken: true };
    }
  }
  return { text: trimmed, hadToken: false };
}

/** Standard instruction line agents can be given to opt into silent replies. */
function instructionLine(token = SILENT_REPLY_TOKEN) {
  return `If nothing needs attention, reply with ONLY: ${token}.`;
}

module.exports = {
  SILENT_REPLY_TOKEN,
  HEARTBEAT_OK_TOKEN,
  NO_ACTION_TOKEN,
  BOOT_OK_TOKEN,
  SILENT_REPLY_TOKENS,
  isSilentReply,
  stripSilentReply,
  instructionLine,
};
