// Privacy Guard — regex-based auto-redactor for trace + recovery ledger.
//
// Deep-walks an object, finds string values, applies an ordered list of
// regex patterns, returns a redacted COPY (immutable; input untouched).
// Each match is replaced with `<redacted:<kind>>`.
//
// Default-ON patterns (7): anthropic, openai, aws, gh-pat, jwt, bearer, slack.
// Opt-in (env-gated): email PII (OLAM_REDACT_PII=1), high-entropy strings
// (OLAM_REDACT_HIGH_ENTROPY=1). Hard short-circuit: OLAM_REDACTION_DISABLED=1.
//
// Precedence matters: anthropic runs before openai (otherwise the OpenAI
// `sk-...` regex would steal `sk-ant-...` and emit the wrong tag). Bearer
// runs after the high-specificity key patterns so a bearer-wrapped key
// gets the tighter tag.

const DEFAULT_PATTERNS = [
  { kind: 'anthropic-key', re: /\bsk-ant-(?:api|admin)[A-Za-z0-9_-]{20,}\b/g },
  { kind: 'openai-key',    re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/g },
  { kind: 'aws-key',       re: /\bAKIA[A-Z0-9]{16}\b/g },
  { kind: 'gh-token',      re: /\bgh[poursa]_[A-Za-z0-9_]{36,}\b/g },
  { kind: 'jwt',           re: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
  { kind: 'slack-token',   re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g },
  { kind: 'bearer',        re: /Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, replacement: 'Bearer <redacted:bearer>' },
];

const EMAIL_PATTERN = { kind: 'email', re: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi };
const HIGH_ENTROPY_PATTERN = { kind: 'high-entropy', re: /\b[A-Z0-9_-]{32,}\b/g };
const HIGH_ENTROPY_ALLOWLIST = new Set(['UUID', 'CHUNK_ID', '__filename', '__dirname']);

function redactString(s) {
  if (process.env.OLAM_REDACTION_DISABLED === '1') return s;
  let out = s;
  for (const { kind, re, replacement } of DEFAULT_PATTERNS) {
    out = out.replace(re, replacement ?? `<redacted:${kind}>`);
  }
  if (process.env.OLAM_REDACT_PII === '1') {
    out = out.replace(EMAIL_PATTERN.re, `<redacted:${EMAIL_PATTERN.kind}>`);
  }
  if (process.env.OLAM_REDACT_HIGH_ENTROPY === '1') {
    out = out.replace(HIGH_ENTROPY_PATTERN.re, (m) =>
      HIGH_ENTROPY_ALLOWLIST.has(m) || /^<redacted:/.test(m) ? m : `<redacted:${HIGH_ENTROPY_PATTERN.kind}>`,
    );
  }
  return out;
}

/**
 * Deep-walk `value`, redacting strings. Returns a new value; input is
 * never mutated. Primitives + null pass through unchanged (except strings,
 * which are run through `redactString`). Cycles produce `'<cycle>'`.
 *
 * @template T
 * @param {T} value
 * @returns {T}
 */
export function redactSensitive(value) {
  if (process.env.OLAM_REDACTION_DISABLED === '1') return value;
  return walk(value, new WeakSet());
}

function walk(value, seen) {
  if (typeof value === 'string') return redactString(value);
  if (value === null || typeof value !== 'object') return value;
  if (seen.has(value)) return '<cycle>';
  seen.add(value);
  if (Array.isArray(value)) return value.map((v) => walk(v, seen));
  const out = {};
  for (const k of Object.keys(value)) {
    out[k] = walk(value[k], seen);
  }
  return out;
}
