import { ValidationError } from '@shade/core';
import type { LaneInitSpec, LanePartition } from './types.js';

/** Build a range-partition plan: contiguous byte ranges, one per lane. */
export function planRangePartition(
  totalBytes: number,
  laneCount: number,
): LaneInitSpec[] {
  if (!Number.isInteger(totalBytes) || totalBytes < 0) {
    throw new ValidationError(`totalBytes must be a non-negative integer`, 'totalBytes');
  }
  if (!Number.isInteger(laneCount) || laneCount < 1) {
    throw new ValidationError(`laneCount must be >= 1`, 'laneCount');
  }
  const lanes: LaneInitSpec[] = [];
  // Use ceil so the first lanes get the extra byte when not evenly divisible.
  // Each lane's start = previous end. Last lane's end = totalBytes.
  const baseSize = Math.floor(totalBytes / laneCount);
  const remainder = totalBytes - baseSize * laneCount;
  let cursor = 0;
  for (let i = 0; i < laneCount; i++) {
    const extra = i < remainder ? 1 : 0;
    const size = baseSize + extra;
    const startByte = cursor;
    const endByte = cursor + size;
    lanes.push({
      laneId: i,
      partition: { kind: 'range', startByte, endByte, startChunk: 0 },
    });
    cursor = endByte;
  }
  return lanes;
}

/** Build a round-robin partition plan: chunk i goes to lane (i mod count). */
export function planRoundRobinPartition(laneCount: number): LaneInitSpec[] {
  if (!Number.isInteger(laneCount) || laneCount < 1) {
    throw new ValidationError(`laneCount must be >= 1`, 'laneCount');
  }
  const lanes: LaneInitSpec[] = [];
  for (let i = 0; i < laneCount; i++) {
    lanes.push({
      laneId: i,
      partition: { kind: 'round-robin', lane: i, count: laneCount },
    });
  }
  return lanes;
}

/**
 * Split an arbitrary byte range into chunkSize-sized slices.
 * Returns an array of [startByte, endByte) tuples for each chunk.
 */
export function chunkRange(
  startByte: number,
  endByte: number,
  chunkSize: number,
): Array<{ start: number; end: number }> {
  if (chunkSize <= 0) {
    throw new ValidationError(`chunkSize must be positive`, 'chunkSize');
  }
  const out: Array<{ start: number; end: number }> = [];
  if (endByte === startByte) {
    // Empty range: emit one empty chunk so isLast can be carried.
    out.push({ start: startByte, end: startByte });
    return out;
  }
  for (let off = startByte; off < endByte; off += chunkSize) {
    out.push({ start: off, end: Math.min(off + chunkSize, endByte) });
  }
  return out;
}

/**
 * Validate that a `LanePartition` matches the global stream parameters.
 * Used by the receiver to detect partition-mismatch on resume.
 */
export function partitionsEqual(a: LanePartition, b: LanePartition): boolean {
  if (a.kind !== b.kind) return false;
  if (a.kind === 'range' && b.kind === 'range') {
    return a.startByte === b.startByte && a.endByte === b.endByte && a.startChunk === b.startChunk;
  }
  if (a.kind === 'round-robin' && b.kind === 'round-robin') {
    return a.lane === b.lane && a.count === b.count;
  }
  return false;
}
