// ─── Date helpers ─────────────────────────────────────────────────────────────

export function addDays(date: Date, days: number): Date {
  const d = new Date(date);
  d.setDate(d.getDate() + days);
  return d;
}

export function daysBetween(a: Date, b: Date): number {
  return Math.round((b.getTime() - a.getTime()) / (1000 * 60 * 60 * 24));
}

/**
 * Converts a date to a left-pixel offset within the visible timeline.
 * Accounts for weekend-skipping when showWeekends is false.
 */
export function dateToLeft(
  date: Date,
  viewStart: Date,
  viewEnd: Date,
  cellWidth: number,
  showWeekends: boolean,
): number {
  const clamped = date < viewStart ? viewStart : date > viewEnd ? viewEnd : date;
  if (showWeekends) {
    return daysBetween(viewStart, clamped) * cellWidth;
  }
  let count = 0;
  const cur = new Date(viewStart);
  cur.setHours(0, 0, 0, 0);
  const target = new Date(clamped);
  target.setHours(0, 0, 0, 0);
  while (cur < target) {
    const dow = cur.getDay();
    if (dow !== 0 && dow !== 6) count++;
    cur.setDate(cur.getDate() + 1);
  }
  return count * cellWidth;
}

/**
 * Computes the pixel width of a date range within the visible timeline.
 * Accounts for weekend-skipping when showWeekends is false.
 */
export function dateRangeToWidth(
  start: Date,
  end: Date,
  viewStart: Date,
  viewEnd: Date,
  cellWidth: number,
  showWeekends: boolean,
): number {
  let count = 0;
  const cur = new Date(start < viewStart ? viewStart : start);
  cur.setHours(0, 0, 0, 0);
  const endC = new Date(end > viewEnd ? viewEnd : end);
  endC.setHours(0, 0, 0, 0);
  while (cur <= endC) {
    const dow = cur.getDay();
    if (showWeekends || (dow !== 0 && dow !== 6)) count++;
    cur.setDate(cur.getDate() + 1);
  }
  return count * cellWidth;
}

// ─── O(1) position cache ──────────────────────────────────────────────────────

/**
 * Pre-computes a pixel-offset lookup table for the entire visible date range.
 * Returns `toLeft` / `toWidth` with O(1) lookups instead of O(n) loops.
 * Use inside a `useMemo` that depends on viewStart / viewDays / cellWidth / showWeekends.
 */
export function createDatePositionCache(
  viewStart: Date,
  viewDays: number,
  cellWidth: number,
  showWeekends: boolean,
): { toLeft: (date: Date) => number; toWidth: (start: Date, end: Date) => number } {
  // offsetMap: day-start-timestamp → pixel left   rankMap: same → 0-based visible-day index
  const offsetMap = new Map<number, number>();
  const rankMap = new Map<number, number>();

  let rank = 0;
  const cur = new Date(viewStart);
  cur.setHours(0, 0, 0, 0);
  const viewStartT = cur.getTime();

  for (let i = 0; i < viewDays; i++) {
    const dow = cur.getDay();
    if (showWeekends || (dow !== 0 && dow !== 6)) {
      const t = cur.getTime();
      offsetMap.set(t, rank * cellWidth);
      rankMap.set(t, rank);
      rank++;
    }
    cur.setDate(cur.getDate() + 1);
  }

  const totalWidth = rank * cellWidth;
  const viewEndT = cur.getTime(); // timestamp of the day AFTER the last visible day

  function toLeft(date: Date): number {
    const d = new Date(date);
    d.setHours(0, 0, 0, 0);
    const t = d.getTime();
    if (t <= viewStartT) return 0;
    if (t >= viewEndT) return totalWidth;
    const cached = offsetMap.get(t);
    if (cached !== undefined) return cached;
    // Weekend when showWeekends=false: return the *next* visible day's offset.
    // This matches the original dateToLeft which counts strictly-before business days,
    // so Saturday gets the same offset as the following Monday.
    const next = new Date(d);
    for (let i = 1; i <= 6; i++) {
      next.setDate(next.getDate() + 1);
      if (next.getTime() >= viewEndT) return totalWidth;
      const v = offsetMap.get(next.getTime());
      if (v !== undefined) return v;
    }
    return totalWidth;
  }

  function toWidth(start: Date, end: Date): number {
    const sNorm = new Date(start < viewStart ? viewStart : start);
    sNorm.setHours(0, 0, 0, 0);
    const eNorm = new Date(end);
    eNorm.setHours(0, 0, 0, 0);

    // Rank of the first visible day at-or-after sNorm
    let sRank: number;
    const sT = sNorm.getTime();
    const rS = rankMap.get(sT);
    if (rS !== undefined) {
      sRank = rS;
    } else {
      sRank = rank; // default: past end → 0 width
      const next = new Date(sNorm);
      for (let i = 1; i <= 6; i++) {
        next.setDate(next.getDate() + 1);
        if (next.getTime() >= viewEndT) break;
        const r = rankMap.get(next.getTime());
        if (r !== undefined) { sRank = r; break; }
      }
    }

    // Rank of the last visible day at-or-before eNorm
    let eRank: number;
    const eT = eNorm.getTime();
    if (eT >= viewEndT) {
      eRank = rank - 1; // clamp to last visible day
    } else {
      const rE = rankMap.get(eT);
      if (rE !== undefined) {
        eRank = rE;
      } else {
        eRank = sRank - 1; // default: no visible day → 0 width
        const prev = new Date(eNorm);
        for (let i = 1; i <= 6; i++) {
          prev.setDate(prev.getDate() - 1);
          if (prev.getTime() < viewStartT) break;
          const r = rankMap.get(prev.getTime());
          if (r !== undefined) { eRank = r; break; }
        }
      }
    }

    if (eRank < sRank) return 0;
    return (eRank - sRank + 1) * cellWidth;
  }

  return { toLeft, toWidth };
}

// ─── Sub-lane computation ──────────────────────────────────────────────────────

export interface SubLaneAlloc {
  id: string;
  startDate: Date;
  endDate: Date;
}

/**
 * Greedy lane assignment for overlapping allocations.
 * Returns a map of allocationId → lane index (0-based).
 * Allocations are sorted by startDate then assigned to the first lane
 * that doesn't overlap with the previous occupant.
 */
export function computeSubLanes(allocs: SubLaneAlloc[]): Map<string, number> {
  const sorted = [...allocs].sort(
    (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime(),
  );

  // laneEnds[i] = end date of the last allocation placed in lane i
  const laneEnds: Date[] = [];
  const result = new Map<string, number>();

  for (const alloc of sorted) {
    const start = new Date(alloc.startDate);
    const end = new Date(alloc.endDate);

    let placed = false;
    for (let i = 0; i < laneEnds.length; i++) {
      const laneEnd = laneEnds[i]!;
      if (start > laneEnd) {
        // Lane is free
        laneEnds[i] = end;
        result.set(alloc.id, i);
        placed = true;
        break;
      }
    }

    if (!placed) {
      result.set(alloc.id, laneEnds.length);
      laneEnds.push(end);
    }
  }

  return result;
}
