/**
 * Log Sanitizer Utility
 *
 * Sanitizes sensitive information from logs to prevent secret exposure.
 * This is critical for security when logging errors, especially to external
 * services like Discord.
 *
 * @module log-sanitizer
 */

/**
 * Patterns for detecting sensitive data in strings
 * Order matters - more specific patterns should come first
 */
const SENSITIVE_PATTERNS = [
  // Quoted passwords and secrets (must come before general key-value patterns)
  { pattern: /(password|secret|token|key)\s*=\s*"([^"]+)"/gi, replacement: '$1="[REDACTED]"' },
  { pattern: /(password|secret|token|key)\s*=\s*'([^']+)'/gi, replacement: "$1='[REDACTED]'" },
  // Discord bot tokens (specific format, must come before generic token patterns)
  {
    pattern: /\b[MN][A-Za-z\d]{23,25}\.[A-Za-z\d]{6}\.[A-Za-z\d_-]{27,}\b/g,
    replacement: "[REDACTED]",
  },
  // API Keys and tokens (Stripe-style)
  { pattern: /\b(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{16,}/gi, replacement: "[REDACTED]" },
  // Bearer tokens
  { pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, replacement: "Bearer [REDACTED]" },
  // JWT tokens
  { pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, replacement: "[REDACTED]" },
  // Authorization Basic
  { pattern: /Basic\s+[A-Za-z0-9+/]+=*/gi, replacement: "Basic [REDACTED]" },
  // Email addresses
  { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: "[REDACTED]" },
  // Connection string passwords
  { pattern: /(:\/\/[^:]+:)([^@]+)(@)/g, replacement: "$1[REDACTED]$3" },
  // Generic tokens in text with colon (e.g., "token: abc123")
  {
    pattern: /\b(token|password|secret|key)\s*:\s+([a-zA-Z0-9._-]{6,})/gi,
    replacement: "$1: [REDACTED]",
  },
  // Generic tokens in text without colon (e.g., "token abc123")
  {
    pattern: /\b(token|password|secret|key)\s+([a-zA-Z0-9._-]{6,})/gi,
    replacement: "$1 [REDACTED]",
  },
  // Key-value pairs with = sign (should be last as it's most general)
  {
    pattern:
      /\b(token|password|secret|api[_-]?key|apikey|client[_-]?secret|bearer)\s*=\s*[^\s,;)}\]"']+/gi,
    replacement: "$1=[REDACTED]",
  },
];

/**
 * Sensitive key names that should have their values redacted
 */
const SENSITIVE_KEYS = [
  "password",
  "secret",
  "token",
  "apikey",
  "api_key",
  "apiKey",
  "API_KEY",
  "bearertoken",
  "bearerToken",
  "bearer_token",
  "accesstoken",
  "accessToken",
  "access_token",
  "refreshtoken",
  "refreshToken",
  "refresh_token",
  "clientsecret",
  "clientSecret",
  "client_secret",
  "authorization",
  "Authorization",
];

/**
 * Checks if a key name is sensitive
 */
function isSensitiveKey(key: string): boolean {
  const lowerKey = key.toLowerCase();
  return SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey.toLowerCase()));
}

/**
 * Sanitizes a string by redacting sensitive patterns
 */
function sanitizeString(value: string): string {
  let sanitized = value;
  for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
    sanitized = sanitized.replace(pattern, replacement);
  }
  return sanitized;
}

/**
 * Type guard to check if value is an object
 */
function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
 * Sanitizes data for logging by redacting sensitive information
 *
 * @param data - The data to sanitize (can be string, object, array, etc.)
 * @param seen - Internal set to track circular references
 * @returns Sanitized version of the data with secrets redacted
 *
 * @example
 * ```typescript
 * const error = new Error("Auth failed");
 * error.config = { headers: { Authorization: "Bearer secret123" } };
 * const sanitized = sanitizeForLogging(error);
 * // sanitized.config.headers.Authorization === "[REDACTED]"
 * ```
 */
export function sanitizeForLogging(data: unknown, seen = new WeakSet()): unknown {
  // Handle primitives
  if (data === null || data === undefined) {
    return data;
  }

  if (typeof data === "boolean" || typeof data === "number") {
    return data;
  }

  if (typeof data === "string") {
    return sanitizeString(data);
  }

  // Handle arrays
  if (Array.isArray(data)) {
    return data.map((item) => sanitizeForLogging(item, seen));
  }

  // Handle Date objects (preserve them as-is)
  if (data instanceof Date) {
    return data;
  }

  // Handle objects (including Error objects)
  if (isObject(data)) {
    // Check for circular references
    if (seen.has(data)) {
      return "[Circular Reference]";
    }
    seen.add(data);

    const sanitized: Record<string, unknown> = {};

    // Handle Error objects specially to preserve their properties
    if (data instanceof Error) {
      sanitized.name = data.name;
      sanitized.message = sanitizeString(data.message);
      if (data.stack) {
        sanitized.stack = sanitizeString(data.stack);
      }
    }

    // Process all enumerable properties
    for (const key in data) {
      if (Object.prototype.hasOwnProperty.call(data, key)) {
        const value = data[key];

        // If the key is sensitive, redact the value
        if (isSensitiveKey(key)) {
          sanitized[key] = value === null || value === undefined ? value : "[REDACTED]";
        } else {
          // Recursively sanitize nested values
          sanitized[key] = sanitizeForLogging(value, seen);
        }
      }
    }

    return sanitized;
  }

  // Return other types as-is (functions, symbols, etc.)
  return data as unknown;
}
