/**
 * Log redaction utilities (BFF)
 *
 * Keep logs production-safe by redacting common sensitive fields and trimming large blobs.
 * Prefer allowlists at call sites for best safety; this helper provides a safe baseline.
 */

import { createHash } from "node:crypto";

export type RedactOptions = {
  /**
   * Extra keys to redact (case-insensitive, substring match).
   * Example: ["email", "address"]
   */
  extraSensitiveKeys?: readonly string[];
  /** Max string length to keep (defaults to 500). */
  maxStringLength?: number;
  /** Max depth to traverse (defaults to 6). */
  maxDepth?: number;
};

const DEFAULT_SENSITIVE_KEY_PARTS = [
  "password",
  "passwd",
  "secret",
  "token",
  "key",
  "auth",
  "authorization",
  "cookie",
  "set-cookie",
  "session",
  "sid",
  "credit",
  "card",
  "cvv",
  "cvc",
  "ssn",
  "social",
  "email",
  "phone",
  "address",
] as const;

const REDACTED = "[REDACTED]";
const TRUNCATED = "[TRUNCATED]";

function shouldRedactKey(key: string, extra: readonly string[]): boolean {
  const lower = key.toLowerCase();
  for (const part of DEFAULT_SENSITIVE_KEY_PARTS) {
    if (lower.includes(part)) return true;
  }
  for (const part of extra) {
    if (lower.includes(part.toLowerCase())) return true;
  }
  return false;
}

function truncateString(value: string, max: number): string {
  if (value.length <= max) return value;
  return value.slice(0, max) + TRUNCATED;
}

export function redactForLogs<T>(value: T, options: RedactOptions = {}): T {
  const maxStringLength = options.maxStringLength ?? 500;
  const maxDepth = options.maxDepth ?? 6;
  const extraSensitiveKeys = options.extraSensitiveKeys ?? [];

  const seen = new WeakSet<object>();

  const walk = (input: unknown, depth: number): unknown => {
    if (input === null || input === undefined) return input;
    if (depth > maxDepth) return "[MAX_DEPTH]";

    if (typeof input === "string") {
      return truncateString(input, maxStringLength);
    }
    if (typeof input === "number" || typeof input === "boolean" || typeof input === "bigint") {
      return input;
    }
    if (typeof input === "symbol") {
      return input.description ? `Symbol(${input.description})` : "Symbol()";
    }
    if (typeof input === "function") {
      return input.name ? `[Function ${input.name}]` : "[Function anonymous]";
    }
    if (input instanceof Date) {
      return input.toISOString();
    }
    if (Array.isArray(input)) {
      return input.map(entry => walk(entry, depth + 1));
    }
    if (typeof input === "object") {
      if (seen.has(input)) return "[CIRCULAR]";
      seen.add(input);

      const record = input as Record<string, unknown>;
      const out: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(record)) {
        out[k] = shouldRedactKey(k, extraSensitiveKeys) ? REDACTED : walk(v, depth + 1);
      }
      return out;
    }

    // Avoid implicit Object stringification like "[object Object]" when `input` is still `unknown`
    // to TypeScript/ESLint (even though the runtime cases above cover all standard JS types).
    return Object.prototype.toString.call(input);
  };

  return walk(value, 0) as T;
}

/**
 * Hash an email for logs (PII minimization).
 * Use a short, stable prefix to correlate events without storing the email itself.
 */
export function hashEmailForLogs(email: string): string {
  const normalized = email.trim().toLowerCase();
  return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
}
