import { SecurityError } from "./errors.ts";
import { assertSafeObject, isDangerousObjectKey } from "./object.ts";

export interface SecureSerializeOptions {
  maxDepth?: number;
  maxKeys?: number;
  maxBytes?: number;
  redact?: RegExp | string[];
  redactedValue?: string;
}

const DEFAULT_SECRET =
  /(?:password|passwd|secret|token|api[-_]?key|private[-_]?key|otp|authorization|cookie)/i;

function shouldRedact(key: string, rule: SecureSerializeOptions["redact"]): boolean {
  if (Array.isArray(rule)) return rule.includes(key);
  return (rule ?? DEFAULT_SECRET).test(key);
}

export function secureJsonStringify(value: unknown, options: SecureSerializeOptions = {}): string {
  assertSafeObject(value, { maxDepth: options.maxDepth, maxKeys: options.maxKeys });
  const redactedValue = options.redactedValue ?? "[REDACTED]";

  const json = JSON.stringify(value, function secureReplacer(key, current) {
    if (isDangerousObjectKey(key)) {
      throw new SecurityError(
        "WRN-SEC-PROTOTYPE-POLLUTION",
        `Dangerous key '${key}' is not allowed.`,
      );
    }
    if (key && shouldRedact(key, options.redact)) return redactedValue;
    if (typeof current === "bigint") return current.toString();
    if (typeof current === "function" || typeof current === "symbol") {
      throw new SecurityError(
        "WRN-SEC-SERIALIZE-TYPE",
        `Cannot serialize ${typeof current} values.`,
      );
    }
    return current;
  });

  if (json === undefined) {
    throw new SecurityError("WRN-SEC-SERIALIZE-EMPTY", "Value cannot be serialized.");
  }

  const safe = json
    .replace(/&/g, "\\u0026")
    .replace(/</g, "\\u003c")
    .replace(/>/g, "\\u003e")
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029");
  const bytes = new TextEncoder().encode(safe).byteLength;
  const maxBytes = options.maxBytes ?? 256 * 1024;
  if (bytes > maxBytes) {
    throw new SecurityError(
      "WRN-SEC-SERIALIZE-SIZE",
      `Serialized payload exceeds ${maxBytes} bytes.`,
      413,
    );
  }
  return safe;
}

export function serializeForHtml(value: unknown, options: SecureSerializeOptions = {}): string {
  return secureJsonStringify(value, options);
}
