export interface StructuredByteEstimateOptions {
  maxNodes?: number;
  maxDepth?: number;
}

const DEFAULT_MAX_NODES = 250;
const DEFAULT_MAX_DEPTH = 6;

export function estimateStructuredBytes(
  value: unknown,
  options: StructuredByteEstimateOptions = {}
): number {
  const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
  const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
  const budget = { remaining: maxNodes };

  return walkValue(value, 0, maxDepth, budget);
}

function walkValue(
  value: unknown,
  depth: number,
  maxDepth: number,
  budget: { remaining: number }
): number {
  if (budget.remaining <= 0) {
    return 0;
  }

  budget.remaining -= 1;

  const primitiveBytes = estimatePrimitiveBytes(value);

  if (primitiveBytes != null) {
    return primitiveBytes;
  }

  if (depth >= maxDepth) {
    return 32;
  }

  if (Array.isArray(value)) {
    return walkArray(value, depth, maxDepth, budget);
  }

  if (value instanceof Map || value instanceof Set) {
    return walkIterable(value, depth, maxDepth, budget);
  }

  if (typeof value === 'object') {
    return walkObject(value as Record<string, unknown>, depth, maxDepth, budget);
  }

  return 16;
}

function estimatePrimitiveBytes(value: unknown): number | null {
  if (value == null) {
    return 0;
  }

  if (typeof value === 'string') {
    return value.length * 2;
  }

  if (typeof value === 'number' || typeof value === 'boolean') {
    return 8;
  }

  if (typeof value === 'bigint') {
    return 16;
  }

  if (value instanceof Date) {
    return 24;
  }

  if (ArrayBuffer.isView(value)) {
    return value.byteLength;
  }

  if (value instanceof ArrayBuffer) {
    return value.byteLength;
  }

  return null;
}

function walkArray(
  value: unknown[],
  depth: number,
  maxDepth: number,
  budget: { remaining: number }
): number {
  let total = 16;

  for (const item of value) {
    if (budget.remaining <= 0) {
      break;
    }

    total += walkValue(item, depth + 1, maxDepth, budget);
  }

  return total;
}

function walkIterable(
  value: Map<unknown, unknown> | Set<unknown>,
  depth: number,
  maxDepth: number,
  budget: { remaining: number }
): number {
  let total = 32;
  let walked = 0;

  for (const item of value) {
    if (budget.remaining <= 0 || walked >= 25) {
      break;
    }

    walked += 1;
    total += walkValue(item, depth + 1, maxDepth, budget);
  }

  return total;
}

function walkObject(
  value: Record<string, unknown>,
  depth: number,
  maxDepth: number,
  budget: { remaining: number }
): number {
  let total = 16;

  for (const [key, nested] of Object.entries(value)) {
    if (budget.remaining <= 0) {
      break;
    }

    total += key.length * 2;
    total += walkValue(nested, depth + 1, maxDepth, budget);
  }

  return total;
}
