/**
 * Default replacement used when a sensitive field is redacted.
 */
export const DEFAULT_REDACTED_VALUE = "[redacted]";
/**
 * Default replacement used when recursive redaction exceeds `maxDepth`.
 */
export const DEFAULT_TRUNCATED_VALUE = "[truncated]";
/**
 * Default replacement used when recursive redaction finds a circular object.
 */
export const DEFAULT_CIRCULAR_VALUE = "[circular]";

/**
 * Exact header/object keys redacted by default.
 */
export const DEFAULT_SENSITIVE_KEYS = [
  "authorization",
  "proxy-authorization",
  "cookie",
  "set-cookie",
  "x-api-key",
  "api-key",
  "apikey",
  "access-token",
  "refresh-token",
  "credentials",
  "accesskey",
  "jwt",
  "session",
] as const;

/**
 * Key substrings redacted by default.
 *
 * Matching is case-insensitive.
 */
export const DEFAULT_SENSITIVE_KEY_TERMS = [
  "token",
  "password",
  "secret",
  "credential",
  "accesskey",
  "jwt",
  "session",
  "private-key",
  "privatekey",
] as const;

/**
 * Context passed to custom redaction key decisions.
 */
export interface RedactionDecisionContext {
  /**
   * Current object/header key being evaluated.
   */
  key: string;
  /**
   * Path to the current value from the root object.
   */
  path: readonly string[];
  /**
   * Current value being evaluated.
   */
  value: unknown;
}

/**
 * Options that control recursive value and header redaction.
 */
export interface RedactionOptions {
  /**
   * Value used when a key is considered sensitive.
   */
  replacement?: string;
  /**
   * Value used when recursion exceeds `maxDepth`.
   */
  truncatedValue?: string;
  /**
   * Value used for circular references.
   */
  circularValue?: string;
  /**
   * Maximum object/array depth to traverse before truncating.
   */
  maxDepth?: number;
  /**
   * Additional exact keys to redact.
   */
  sensitiveKeys?: readonly string[];
  /**
   * Additional case-insensitive key substrings to redact.
   */
  sensitiveKeyTerms?: readonly string[];
  /**
   * Custom key-level redaction rule.
   */
  shouldRedactKey?: (context: RedactionDecisionContext) => boolean;
}

/**
 * Function that returns a redacted copy of a value.
 */
export type Redactor<T = unknown> = (value: T) => T;

/**
 * Header input shapes accepted by `redactHeaders(...)`.
 */
export type RedactableHeaders =
  | Headers
  | Iterable<readonly [string, unknown]>
  | Record<string, unknown>;

function normalizeKey(key: string): string {
  return key.toLowerCase();
}

function redactSensitiveText(value: string, replacement: string): string {
  return value
    .replace(
      /([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi,
      (_match, prefix: string) => `${prefix}${replacement}:${replacement}@`,
    )
    .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, (match) => {
      const scheme = match.slice(0, match.indexOf(" "));
      return `${scheme} ${replacement}`;
    })
    .replace(
      /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
      () => replacement,
    )
    .replace(
      /(\b(?:access[_-]?key|api[_-]?key|jwt|password|secret|session|token)\s*[=:]\s*)[^\s,;]+/gi,
      (_match, prefix: string) => `${prefix}${replacement}`,
    )
    .replace(
      /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
      () => replacement,
    );
}

/**
 * Return whether a key should be redacted.
 *
 * Checks default exact keys, default key terms, user-provided exact keys,
 * user-provided key terms, and finally `shouldRedactKey`.
 *
 * @param key - Object or header key to evaluate.
 * @param options - Optional redaction behavior.
 * @param context - Optional path/value context for custom decisions.
 * @returns `true` when the key should be replaced.
 */
export function isSensitiveKey(
  key: string,
  options: RedactionOptions = {},
  context?: Omit<RedactionDecisionContext, "key">,
): boolean {
  const normalized = normalizeKey(key);
  const exactKeys = new Set(
    [...DEFAULT_SENSITIVE_KEYS, ...(options.sensitiveKeys ?? [])].map(
      normalizeKey,
    ),
  );
  if (exactKeys.has(normalized)) return true;

  const terms = [
    ...DEFAULT_SENSITIVE_KEY_TERMS,
    ...(options.sensitiveKeyTerms ?? []),
  ].map(normalizeKey);
  if (terms.some((term) => normalized.includes(term))) return true;

  return (
    options.shouldRedactKey?.({
      key,
      path: context?.path ?? [],
      value: context?.value,
    }) ?? false
  );
}

function redactUnknown(
  value: unknown,
  options: RedactionOptions,
  path: readonly string[],
  seen: WeakSet<object>,
): unknown {
  const maxDepth = options.maxDepth ?? 6;
  if (path.length > maxDepth) {
    return options.truncatedValue ?? DEFAULT_TRUNCATED_VALUE;
  }

  if (value === null || value === undefined) return value;

  const valueType = typeof value;
  if (valueType === "number" || valueType === "boolean") {
    return value;
  }

  if (valueType === "string") {
    return redactSensitiveText(
      value as string,
      options.replacement ?? DEFAULT_REDACTED_VALUE,
    );
  }

  if (valueType === "bigint") {
    return value.toString();
  }

  if (value instanceof Date) {
    return value;
  }

  if (value instanceof Error) {
    return {
      name: value.name,
      message: redactUnknown(
        value.message,
        options,
        [...path, "message"],
        seen,
      ),
      stack: redactUnknown(value.stack, options, [...path, "stack"], seen),
    };
  }

  if (valueType !== "object") {
    return String(value);
  }

  const objectValue = value as object;
  if (seen.has(objectValue)) {
    return options.circularValue ?? DEFAULT_CIRCULAR_VALUE;
  }
  seen.add(objectValue);

  if (Array.isArray(value)) {
    const output = value.map((item, index) =>
      redactUnknown(item, options, [...path, String(index)], seen),
    );
    seen.delete(objectValue);
    return output;
  }

  const output: Record<string, unknown> = {};
  for (const [key, nestedValue] of Object.entries(value)) {
    output[key] = isSensitiveKey(key, options, {
      path: [...path, key],
      value: nestedValue,
    })
      ? (options.replacement ?? DEFAULT_REDACTED_VALUE)
      : redactUnknown(nestedValue, options, [...path, key], seen);
  }

  seen.delete(objectValue);
  return output;
}

/**
 * Recursively redact a value using Beignet's default sensitive-key rules plus
 * any custom rules in `options`.
 *
 * This returns a copy for objects and arrays. Numbers and booleans are returned
 * as is unless they are under a sensitive key. High-confidence credential
 * shapes inside strings are replaced, including authorization schemes, JWTs,
 * credential-bearing URLs, secret assignments, and private keys. Some runtime
 * shapes are normalized:
 * `bigint` becomes a string, `Error` becomes a plain object with `name`,
 * `message`, and `stack`, and class instances are copied from enumerable
 * entries.
 *
 * @param value - Value to redact.
 * @param options - Optional redaction behavior.
 * @returns A redacted value typed as the input type for caller convenience.
 */
export function redactValue<T = unknown>(
  value: T,
  options: RedactionOptions = {},
): T {
  return redactUnknown(value, options, [], new WeakSet()) as T;
}

function headerEntries(
  headers: RedactableHeaders,
): Iterable<readonly [string, unknown]> {
  if (typeof Headers !== "undefined" && headers instanceof Headers) {
    const entries: Array<readonly [string, unknown]> = [];
    headers.forEach((value, key) => {
      entries.push([key, value]);
    });
    return entries;
  }

  if (
    typeof (headers as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===
    "function"
  ) {
    return headers as Iterable<readonly [string, unknown]>;
  }

  return Object.entries(headers);
}

/**
 * Redact headers into a plain object.
 *
 * Sensitive header names such as `authorization`, `cookie`, and token-like keys
 * are replaced. Non-sensitive values are passed through `redactValue(...)` so
 * nested object values are still sanitized.
 *
 * @param headers - Headers object, iterable entries, or plain object.
 * @param options - Optional redaction behavior.
 * @returns A plain object with redacted header values.
 */
export function redactHeaders(
  headers: RedactableHeaders,
  options: RedactionOptions = {},
): Record<string, unknown> {
  const output: Record<string, unknown> = {};
  for (const [key, value] of headerEntries(headers)) {
    output[key] = isSensitiveKey(key, options, {
      path: [key],
      value,
    })
      ? (options.replacement ?? DEFAULT_REDACTED_VALUE)
      : redactValue(value, options);
  }
  return output;
}

/**
 * Create a reusable redactor function from options.
 *
 * @param options - Redaction behavior to apply on each call.
 * @returns A function that redacts values with the provided options.
 */
export function createRedactor<T = unknown>(
  options: RedactionOptions = {},
): Redactor<T> {
  return (value) => redactValue(value, options);
}
