/** First revision in the repo — week 1 anchor. */
export const REPO_FIRST_REVISION_DATE = '2026-06-18';

export const HOURS_PER_DAY = 24;
export const DAYS_PER_WEEK = 7;
export const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

const DAY_MS = 86400000;

function startOfDay(date) {
  const d = new Date(date);
  d.setHours(0, 0, 0, 0);
  return d;
}

function parseDateKey(key) {
  const [y, m, d] = key.split('-').map(Number);
  return new Date(y, m - 1, d);
}

/** Sunday of the week containing the first revision. */
export function getRepoWeekOneStart() {
  const first = startOfDay(parseDateKey(REPO_FIRST_REVISION_DATE));
  const sunday = new Date(first);
  sunday.setDate(first.getDate() - first.getDay());
  return sunday;
}

export function getWeekNumber(date = new Date()) {
  const d = startOfDay(date);
  const weekOne = getRepoWeekOneStart();
  const diff = Math.floor((d - weekOne) / DAY_MS);
  return Math.floor(diff / 7) + 1;
}

export function getWeekStartDate(weekNumber) {
  const start = new Date(getRepoWeekOneStart());
  start.setDate(start.getDate() + (weekNumber - 1) * 7);
  return start;
}

export function getWeekDateRange(weekNumber) {
  const start = getWeekStartDate(weekNumber);
  const end = new Date(start);
  end.setDate(start.getDate() + 6);
  return { start, end };
}

export function formatWeekHeader(weekNumber) {
  const { start, end } = getWeekDateRange(weekNumber);
  const fmt = (d) => d.toLocaleDateString(undefined, { month: 'long', day: 'numeric' });
  return {
    weekNumber,
    label: `Week ${weekNumber}`,
    // Always include the month on both ends (even within the same month) so
    // in-month weeks are the same visual length as weeks spanning two months.
    range: `${fmt(start)} — ${fmt(end)}`,
  };
}

export function slotKey(dayIndex, hour) {
  return `${dayIndex}-${hour}`;
}

export function parseSlotKey(key) {
  const [dayIndex, hour] = key.split('-').map(Number);
  return { dayIndex, hour };
}

export function detectUserTimezone() {
  try {
    return Intl.DateTimeFormat().resolvedOptions().timeZone;
  } catch {
    return 'America/Los_Angeles';
  }
}

export function formatHourLabel(hour) {
  if (hour === 0) return '12 AM';
  if (hour < 12) return `${hour} AM`;
  if (hour === 12) return '12 PM';
  return `${hour - 12} PM`;
}

/** Sidebar hover label e.g. "2:00 PM". */
export function formatHoverTime(hour) {
  const h = hour % 12 || 12;
  const suffix = hour < 12 ? 'AM' : 'PM';
  return `${h}:00 ${suffix}`;
}

export function slotToLinear(dayIndex, hour) {
  return dayIndex * HOURS_PER_DAY + hour;
}

const WEEK_HOURS = DAYS_PER_WEEK * HOURS_PER_DAY;

/** Wall-clock offset from UTC, in minutes, for a timezone at a given instant (DST-aware). */
function tzOffsetMinutes(timeZone, date) {
  const dtf = new Intl.DateTimeFormat('en-US', {
    timeZone,
    hourCycle: 'h23',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  });
  const parts = {};
  for (const { type, value } of dtf.formatToParts(date)) {
    if (type !== 'literal') parts[type] = value;
  }
  const asUTC = Date.UTC(
    Number(parts.year),
    Number(parts.month) - 1,
    Number(parts.day),
    Number(parts.hour),
    Number(parts.minute),
    Number(parts.second)
  );
  return (asUTC - date.getTime()) / 60000;
}

/** Whole-hour shift to go from `fromTz` wall-clock to `toTz` wall-clock at `date` (DST-aware). */
export function getTimezoneShiftHours(fromTz, toTz, date = new Date()) {
  if (!fromTz || !toTz || fromTz === toTz) return 0;
  try {
    return Math.round((tzOffsetMinutes(toTz, date) - tzOffsetMinutes(fromTz, date)) / 60);
  } catch {
    return 0;
  }
}

/** Shift a (dayIndex, hour) slot by a whole number of hours, wrapping cyclically across the week. */
export function shiftSlot(dayIndex, hour, shiftHours) {
  const linear = (((slotToLinear(dayIndex, hour) + shiftHours) % WEEK_HOURS) + WEEK_HOURS) % WEEK_HOURS;
  return { dayIndex: Math.floor(linear / HOURS_PER_DAY), hour: linear % HOURS_PER_DAY };
}

/** Shift a meeting's (exclusive-end) day/hour range for display in another timezone. */
export function shiftMeetingRange(meeting, shiftHours) {
  if (!shiftHours) return meeting;
  const start = shiftSlot(meeting.startDay, meeting.startHour, shiftHours);
  const lastInclusive = shiftSlot(meeting.endDay, meeting.endHour - 1, shiftHours);
  return {
    ...meeting,
    startDay: start.dayIndex,
    startHour: start.hour,
    endDay: lastInclusive.dayIndex,
    endHour: lastInclusive.hour + 1,
  };
}

export function normalizeRectangle(a, b) {
  return {
    start: {
      dayIndex: Math.min(a.dayIndex, b.dayIndex),
      hour: Math.min(a.hour, b.hour),
    },
    end: {
      dayIndex: Math.max(a.dayIndex, b.dayIndex),
      hour: Math.max(a.hour, b.hour),
    },
  };
}

export function rectangleContains(rect, dayIndex, hour) {
  const { start, end } = normalizeRectangle(rect.start, rect.end);
  if (dayIndex < start.dayIndex || dayIndex > end.dayIndex) return false;
  if (hour < start.hour || hour > end.hour) return false;
  return true;
}

export function normalizeLinearSelection(a, b) {
  const idxA = slotToLinear(a.dayIndex, a.hour);
  const idxB = slotToLinear(b.dayIndex, b.hour);
  if (idxA <= idxB) return { start: a, end: b };
  return { start: b, end: a };
}

export function linearSelectionContains(selection, dayIndex, hour) {
  if (!selection) return false;
  const { start, end } = normalizeLinearSelection(selection.start, selection.end);
  const idx = slotToLinear(dayIndex, hour);
  return idx >= slotToLinear(start.dayIndex, start.hour) && idx <= slotToLinear(end.dayIndex, end.hour);
}

/** Per-day segments for overlay rendering. endHour is exclusive. */
export function getMeetingSegments(meeting) {
  const segments = [];
  const endIdx = slotToLinear(meeting.endDay, meeting.endHour - 1);
  let day = meeting.startDay;
  let hour = meeting.startHour;

  while (slotToLinear(day, hour) <= endIdx) {
    const dayEndHour = day === meeting.endDay ? meeting.endHour : HOURS_PER_DAY;
    segments.push({ dayIndex: day, startHour: hour, endHour: dayEndHour });
    day += 1;
    hour = 0;
  }
  return segments;
}

export function meetingCoversSlot(meeting, dayIndex, hour) {
  const idx = slotToLinear(dayIndex, hour);
  const startIdx = slotToLinear(meeting.startDay, meeting.startHour);
  const endIdx = slotToLinear(meeting.endDay, meeting.endHour - 1);
  return idx >= startIdx && idx <= endIdx;
}

export function segmentsOverlap(a, b) {
  if (a.dayIndex !== b.dayIndex) return false;
  return a.startHour < b.endHour && b.startHour < a.endHour;
}

export function meetingStartLinear(meeting) {
  return slotToLinear(meeting.startDay, meeting.startHour);
}

export function meetingOrderIndex(meeting, weekMeetings) {
  return weekMeetings.findIndex((m) => m.id === meeting.id);
}

/** Any other meeting shares time with this segment. */
export function segmentHasAnyOverlap(segment, meeting, weekMeetings) {
  return weekMeetings.some((other) => {
    if (other.id === meeting.id) return false;
    return getMeetingSegments(other).some((os) => segmentsOverlap(segment, os));
  });
}

/**
 * Yellow styling: overlaps another meeting AND this one starts later
 * than at least one meeting it overlaps.
 */
export function isLaterOverlappingMeeting(segment, meeting, weekMeetings) {
  if (!segmentHasAnyOverlap(segment, meeting, weekMeetings)) return false;
  const myStart = meetingStartLinear(meeting);
  return weekMeetings.some((other) => {
    if (other.id === meeting.id) return false;
    if (meetingStartLinear(other) >= myStart) return false;
    return getMeetingSegments(other).some((os) => segmentsOverlap(segment, os));
  });
}

export function formatSlotTime(dayIndex, hour, weekNumber) {
  const weekStart = getWeekStartDate(weekNumber);
  const date = new Date(weekStart);
  date.setDate(weekStart.getDate() + dayIndex);
  const day = DAY_LABELS[dayIndex];
  const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
  return `${day} ${dateStr}, ${formatHourLabel(hour)}`;
}

export function formatMeetingTime(dayIndex, hour, weekNumber) {
  const weekStart = getWeekStartDate(weekNumber);
  const date = new Date(weekStart);
  date.setDate(weekStart.getDate() + dayIndex);
  return `${date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })} ${formatHourLabel(hour)}`;
}

export function formatMeetingRange(meeting, weekNumber) {
  const start = formatSlotTime(meeting.startDay, meeting.startHour, weekNumber);
  const endSlot = meeting.endHour > 0 ? meeting.endHour - 1 : 0;
  const end = formatSlotTime(meeting.endDay, endSlot, weekNumber);
  if (meeting.startDay === meeting.endDay && meeting.endHour - meeting.startHour === 1) {
    return start;
  }
  return `${start} – ${end}`;
}

/** Discrete overlap level 0–4 for team heatmap. */
export function getOverlapLevel(freeCount, teamSize) {
  if (!freeCount) return 0;
  if (teamSize <= 1) return 4;
  const ratio = freeCount / teamSize;
  if (ratio <= 0.25) return 1;
  if (ratio <= 0.5) return 2;
  if (ratio <= 0.75) return 3;
  return 4;
}
