import { SecurityError } from "./errors.ts";

const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]);

export interface SafeObjectOptions {
  maxDepth?: number;
  maxKeys?: number;
  allowInstances?: boolean;
}

export function isDangerousObjectKey(key: string): boolean {
  return DANGEROUS_KEYS.has(key);
}

export function assertSafeObject(value: unknown, options: SafeObjectOptions = {}): void {
  const maxDepth = options.maxDepth ?? 32;
  const maxKeys = options.maxKeys ?? 10_000;
  const seen = new WeakSet<object>();
  let keyCount = 0;

  const visit = (current: unknown, depth: number): void => {
    if (current === null || typeof current !== "object") return;
    if (depth > maxDepth) {
      throw new SecurityError("WRN-SEC-OBJECT-DEPTH", `Object depth exceeds ${maxDepth}.`, 413);
    }
    if (seen.has(current)) {
      throw new SecurityError("WRN-SEC-OBJECT-CYCLE", "Cyclic objects are not accepted.");
    }
    seen.add(current);

    const proto = Object.getPrototypeOf(current);
    if (
      !options.allowInstances &&
      !Array.isArray(current) &&
      proto !== Object.prototype &&
      proto !== null &&
      !(current instanceof Date) &&
      !(current instanceof URL)
    ) {
      throw new SecurityError(
        "WRN-SEC-OBJECT-PROTOTYPE",
        "Only plain objects, arrays, dates, and URLs are accepted.",
      );
    }

    for (const key of Object.keys(current)) {
      keyCount++;
      if (keyCount > maxKeys) {
        throw new SecurityError(
          "WRN-SEC-OBJECT-KEYS",
          `Object contains more than ${maxKeys} keys.`,
          413,
        );
      }
      if (isDangerousObjectKey(key)) {
        throw new SecurityError(
          "WRN-SEC-PROTOTYPE-POLLUTION",
          `Dangerous object key '${key}' is not allowed.`,
        );
      }
      visit((current as Record<string, unknown>)[key], depth + 1);
    }
    seen.delete(current);
  };

  visit(value, 0);
}

export function safeMerge<T extends Record<string, unknown>>(
  target: T,
  ...sources: Array<Record<string, unknown> | undefined | null>
): T {
  for (const source of sources) {
    if (!source) continue;
    assertSafeObject(source);
    for (const [key, value] of Object.entries(source)) {
      if (isDangerousObjectKey(key)) continue;
      const existing = target[key];
      if (
        value &&
        existing &&
        typeof value === "object" &&
        typeof existing === "object" &&
        !Array.isArray(value) &&
        !Array.isArray(existing)
      ) {
        safeMerge(existing as Record<string, unknown>, value as Record<string, unknown>);
      } else {
        target[key as keyof T] = value as T[keyof T];
      }
    }
  }
  return target;
}
