'use strict';

/**
 * Deterministic ordering helpers — Item G of prompt-catalog deep-dive.
 *
 * Anthropic's prompt cache has a 5-min TTL and a strict prefix-match
 * requirement: if any byte in the system prompt or early messages
 * differs between two calls, the cache misses. Wall-E's prompts had
 * accidental ordering — registries iterated in insertion order,
 * search results came back in DB row order, files in directory listing
 * order — meaning two calls within the cache window would routinely
 * miss because some collection serialized differently.
 *
 * Inspired by OpenClaw's AGENTS.md:156-158:
 *   "Use deterministic ordering for maps/sets/registries/plugin lists/
 *    files/network results before model/tool payloads to keep the
 *    prompt cache hit."
 *
 * This module provides three ordering helpers:
 *
 *   - `sortByKey(items, keyFn)`  — stable sort by a key function. Use
 *     for arrays of objects whose serialization order matters.
 *   - `sortStrings(arr)`         — locale-independent stable sort for
 *     strings (id lists, file paths).
 *   - `sortRegistry(registry)`   — given a registry-like { list() }
 *     returns its entries sorted by id. Used for provider/channel/
 *     skill/adapter lists in system prompts.
 *
 * Use these EVERY TIME a collection ends up in a system prompt or in
 * the FIRST message. Order doesn't matter for the LAST user message
 * (which is always different anyway), so don't over-apply.
 */

/**
 * Stable sort `items` by `keyFn(item)`. Returns a new array; original
 * is not mutated. Comparator uses < / > so it works on strings, numbers,
 * and ISO date strings.
 *
 * Stable across Node versions (Array.prototype.sort is stable since
 * Node 12 / V8 7.0).
 */
function sortByKey(items, keyFn) {
  if (!Array.isArray(items)) return items;
  if (typeof keyFn !== 'function') keyFn = (x) => x;
  return [...items].sort((a, b) => {
    const ka = keyFn(a);
    const kb = keyFn(b);
    if (ka < kb) return -1;
    if (ka > kb) return 1;
    return 0;
  });
}

/**
 * Locale-independent stable sort for strings. Uses byte-comparison
 * (sort()), not localeCompare() — locale-dependent sorting would
 * defeat the cache-hit purpose since the same input could sort
 * differently in different locales.
 */
function sortStrings(arr) {
  return sortByKey(arr, (s) => String(s));
}

/**
 * Given a registry exposing `.list()` returning entries with `.id`,
 * return entries sorted by id. If the registry has no `.list()` or
 * entries don't have `.id`, returns the input unchanged (defensive).
 */
function sortRegistry(registry) {
  if (!registry || typeof registry.list !== 'function') return registry;
  try {
    const entries = registry.list();
    if (!Array.isArray(entries)) return entries;
    return sortByKey(entries, (e) => e.id || '');
  } catch {
    return [];
  }
}

/**
 * Compose a deterministic JSON snapshot of a registry. Keys are sorted,
 * so the same registry state always serializes to the same string.
 * Useful for inclusion in system prompts.
 *
 * @param {object} registry - has `.list()` returning {id, manifest, ...}
 * @param {object} [opts]
 * @param {string[]} [opts.fields] - which fields to include from each entry
 * @returns {string} - JSON.stringify with sorted keys
 */
function snapshotRegistry(registry, opts = {}) {
  const entries = sortRegistry(registry);
  if (!Array.isArray(entries)) return '[]';
  const fields = opts.fields || ['id'];
  const projected = entries.map((e) => {
    const out = {};
    for (const f of fields) {
      if (e[f] !== undefined) out[f] = e[f];
    }
    return out;
  });
  return JSON.stringify(projected);
}

/**
 * Sort an object's keys for deterministic JSON serialization. Returns
 * a NEW object with keys in sorted order. Recursively sorts nested
 * plain objects (arrays preserved as-is unless sortArrays=true).
 */
function sortObjectKeys(obj, opts = {}) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) {
    if (opts.sortArrays) {
      return [...obj].sort();
    }
    return obj.map((v) => sortObjectKeys(v, opts));
  }
  const keys = Object.keys(obj).sort();
  const out = {};
  for (const k of keys) {
    out[k] = sortObjectKeys(obj[k], opts);
  }
  return out;
}

/**
 * Stringify with sorted keys (for cache-hit-friendly prompt inclusion).
 * Wrapper around sortObjectKeys + JSON.stringify.
 */
function deterministicJsonStringify(obj, indent) {
  return JSON.stringify(sortObjectKeys(obj), null, indent);
}

module.exports = {
  sortByKey,
  sortStrings,
  sortRegistry,
  snapshotRegistry,
  sortObjectKeys,
  deterministicJsonStringify,
};
