import { getHours, getMinutes, isSameDay } from "date-fns";

import type { CalendarEvent, PositionedEvent } from "../types";

const MINUTES_PER_DAY = 24 * 60;
/** Minimum rendered duration so very short events stay tappable. */
const MIN_EVENT_MINUTES = 20;

function minutesFromMidnight(date: Date): number {
  return getHours(date) * 60 + getMinutes(date);
}

/** Split a day's entries into all-day and timed buckets, sorted by start. */
export function getDayEvents<TMeta>(
  events: CalendarEvent<TMeta>[],
  day: Date,
): { allDay: CalendarEvent<TMeta>[]; timed: CalendarEvent<TMeta>[] } {
  const onDay = events.filter((event) => isSameDay(event.start, day));
  return {
    allDay: onDay.filter((event) => event.allDay),
    timed: onDay
      .filter((event) => !event.allDay)
      .sort((a, b) => a.start.getTime() - b.start.getTime()),
  };
}

/**
 * Position a single day's timed events, packing overlaps side by side.
 *
 * Pure geometry: returns percent-based boxes (0–100) relative to the rendered
 * `[startHour, endHour)` range, independent of pixel height. Overlapping events
 * share the column width of their overlap cluster; non-overlapping events get
 * the full width.
 */
export function layoutDayEvents<TMeta>(
  events: CalendarEvent<TMeta>[],
  startHour: number,
  endHour: number,
): PositionedEvent<TMeta>[] {
  const rangeStart = startHour * 60;
  const rangeEnd = endHour * 60;
  const rangeLen = rangeEnd - rangeStart;
  if (rangeLen <= 0) return [];

  // Clamp each event to the rendered range and enforce a minimum height.
  const items = events
    .map((event) => {
      const rawStart = minutesFromMidnight(event.start);
      const rawEnd = minutesFromMidnight(event.end);
      // An end at/before the start means it runs to midnight.
      const normalizedEnd = rawEnd <= rawStart ? MINUTES_PER_DAY : rawEnd;
      const start = Math.min(Math.max(rawStart, rangeStart), rangeEnd);
      const end = Math.max(
        Math.min(normalizedEnd, rangeEnd),
        start + MIN_EVENT_MINUTES,
      );
      return { event, start, end };
    })
    .sort((a, b) => a.start - b.start || b.end - a.end);

  // Greedy lane assignment: reuse the first lane free at this event's start.
  const laneEnds: number[] = [];
  const laned = items.map((item) => {
    let lane = laneEnds.findIndex((laneEnd) => laneEnd <= item.start);
    if (lane === -1) {
      lane = laneEnds.length;
      laneEnds.push(item.end);
    } else {
      laneEnds[lane] = item.end;
    }
    return { ...item, lane };
  });

  // Group transitively-overlapping events so a cluster shares its column count.
  const positioned: PositionedEvent<TMeta>[] = [];
  let cluster: typeof laned = [];
  let clusterEnd = -Infinity;

  const flush = () => {
    if (cluster.length === 0) return;
    const columns = Math.max(...cluster.map((item) => item.lane)) + 1;
    for (const item of cluster) {
      positioned.push({
        event: item.event,
        topPct: ((item.start - rangeStart) / rangeLen) * 100,
        heightPct: ((item.end - item.start) / rangeLen) * 100,
        leftPct: (item.lane / columns) * 100,
        widthPct: (1 / columns) * 100,
      });
    }
    cluster = [];
  };

  for (const item of laned) {
    if (cluster.length > 0 && item.start >= clusterEnd) {
      flush();
      clusterEnd = -Infinity;
    }
    cluster.push(item);
    clusterEnd = Math.max(clusterEnd, item.end);
  }
  flush();

  return positioned;
}

/** Fraction (0–1) down the range where "now" sits, or null if outside it. */
export function getNowOffset(
  now: Date,
  startHour: number,
  endHour: number,
): number | null {
  const minutes = minutesFromMidnight(now);
  const rangeStart = startHour * 60;
  const rangeEnd = endHour * 60;
  if (minutes < rangeStart || minutes > rangeEnd) return null;
  return (minutes - rangeStart) / (rangeEnd - rangeStart);
}
