import { addDays, differenceInCalendarDays, parseISO, startOfDay, subMilliseconds } from "date-fns";
import { parseDuration } from "@/components/calendar/event-card";
import type { CalendarEvent } from "@/lib/jmap/types";

export interface CalendarWeekSegment {
  event: CalendarEvent;
  startIndex: number;
  span: number;
  row: number;
  continuesBefore: boolean;
  continuesAfter: boolean;
}

export interface TimedEventLayout {
  event: CalendarEvent;
  column: number;
  totalColumns: number;
  startMinutes: number;
  endMinutes: number;
  continuesBefore: boolean;
  continuesAfter: boolean;
}

export function getEventStartDate(
  event: Pick<CalendarEvent, 'start' | 'utcStart' | 'showWithoutTime'>,
): Date {
  const source = !event.showWithoutTime && event.utcStart ? event.utcStart : event.start;
  return parseISO(source);
}

export function packWeekSegments(rawSegments: CalendarWeekSegment[]): CalendarWeekSegment[] {
  rawSegments.sort((left, right) => {
    if (left.startIndex !== right.startIndex) return left.startIndex - right.startIndex;
    if (left.span !== right.span) return right.span - left.span;
    if (left.event.showWithoutTime !== right.event.showWithoutTime) {
      return left.event.showWithoutTime ? -1 : 1;
    }
    const timeDiff = getEventStartDate(left.event).getTime() - getEventStartDate(right.event).getTime();
    if (timeDiff !== 0) return timeDiff;
    return (left.event.title || "").localeCompare(right.event.title || "");
  });

  const rowEndIndices: number[] = [];
  return rawSegments.map((segment) => {
    const segmentEndIndex = segment.startIndex + segment.span - 1;
    let row = rowEndIndices.findIndex((endIndex) => endIndex < segment.startIndex);
    if (row === -1) {
      row = rowEndIndices.length;
      rowEndIndices.push(segmentEndIndex);
    } else {
      rowEndIndices[row] = segmentEndIndex;
    }
    return { ...segment, row };
  });
}

export function getEventEndDate(event: CalendarEvent): Date {
  if (!event.showWithoutTime && event.utcEnd) {
    return parseISO(event.utcEnd);
  }

  const start = getEventStartDate(event);
  if (!event.duration) return start;
  return new Date(start.getTime() + parseDuration(event.duration) * 60000);
}

export function getEventDisplayEndDate(event: CalendarEvent): Date {
  const end = getEventEndDate(event);
  const start = getEventStartDate(event);
  if (!event.showWithoutTime || end.getTime() <= start.getTime()) {
    return end;
  }
  return subMilliseconds(end, 1);
}

export function getEventDayBounds(event: CalendarEvent): { startDay: Date; endDay: Date } {
  return {
    startDay: startOfDay(getEventStartDate(event)),
    endDay: startOfDay(getEventDisplayEndDate(event)),
  };
}

export function getTimedEventBoundsForDay(
  event: CalendarEvent,
  day: Date,
): { startMinutes: number; endMinutes: number; continuesBefore: boolean; continuesAfter: boolean } | null {
  if (event.showWithoutTime) return null;

  const eventStart = getEventStartDate(event);
  const eventEnd = getEventEndDate(event);
  const dayStart = startOfDay(day);
  const nextDayStart = addDays(dayStart, 1);

  if (eventEnd <= dayStart || eventStart >= nextDayStart) {
    return null;
  }

  const clippedStart = eventStart > dayStart ? eventStart : dayStart;
  const clippedEnd = eventEnd < nextDayStart ? eventEnd : nextDayStart;
  const startMinutes = Math.max(0, Math.floor((clippedStart.getTime() - dayStart.getTime()) / 60000));
  const endMinutes = Math.min(1440, Math.ceil((clippedEnd.getTime() - dayStart.getTime()) / 60000));

  return {
    startMinutes,
    endMinutes,
    continuesBefore: eventStart < dayStart,
    continuesAfter: eventEnd > nextDayStart,
  };
}

export function isTimedEventFullDayOnDate(event: CalendarEvent, day: Date): boolean {
  const bounds = getTimedEventBoundsForDay(event, day);
  return bounds?.startMinutes === 0 && bounds?.endMinutes === 1440;
}

export function normalizeAllDayDuration(duration: string | undefined): string | undefined {
  if (!duration) return undefined;
  const totalMinutes = parseDuration(duration);
  const totalDays = Math.max(1, Math.ceil(totalMinutes / (24 * 60)));
  return `P${totalDays}D`;
}

export function buildAllDayDuration(start: Date, inclusiveEnd: Date): string {
  const dayCount = Math.max(1, differenceInCalendarDays(startOfDay(inclusiveEnd), startOfDay(start)) + 1);
  return `P${dayCount}D`;
}

export function buildWeekSegmentsRaw(events: CalendarEvent[], weekDays: Date[]): CalendarWeekSegment[] {
  if (weekDays.length === 0) return [];

  const weekStart = startOfDay(weekDays[0]);
  const weekEnd = startOfDay(weekDays[weekDays.length - 1]);

  const rawSegments = events.flatMap((event) => {
    const { startDay, endDay } = getEventDayBounds(event);
    if (endDay < weekStart || startDay > weekEnd) {
      return [];
    }

    const segmentStart = startDay < weekStart ? weekStart : startDay;
    const segmentEnd = endDay > weekEnd ? weekEnd : endDay;
    const startIndex = differenceInCalendarDays(segmentStart, weekStart);
    const span = differenceInCalendarDays(segmentEnd, segmentStart) + 1;

    return [{
      event,
      startIndex,
      span,
      row: -1,
      continuesBefore: startDay < weekStart,
      continuesAfter: endDay > weekEnd,
    } satisfies CalendarWeekSegment];
  });

  return rawSegments;
}

export function buildWeekSegments(events: CalendarEvent[], weekDays: Date[]): CalendarWeekSegment[] {
  return packWeekSegments(buildWeekSegmentsRaw(events, weekDays));
}

export function buildTimedFullDayWeekSegments(events: CalendarEvent[], weekDays: Date[]): CalendarWeekSegment[] {
  if (weekDays.length === 0) return [];

  const rawSegments = events.flatMap((event) => {
    const fullDayIndices = weekDays
      .map((day, index) => (isTimedEventFullDayOnDate(event, day) ? index : -1))
      .filter((index) => index >= 0);

    if (fullDayIndices.length === 0) {
      return [];
    }

    const segments: CalendarWeekSegment[] = [];
    let rangeStart = fullDayIndices[0];
    let previousIndex = fullDayIndices[0];

    const pushSegment = (startIndex: number, endIndex: number) => {
      const startDay = weekDays[startIndex];
      const endDay = weekDays[endIndex];
      segments.push({
        event,
        startIndex,
        span: endIndex - startIndex + 1,
        row: -1,
        continuesBefore: isTimedEventFullDayOnDate(event, addDays(startDay, -1)),
        continuesAfter: isTimedEventFullDayOnDate(event, addDays(endDay, 1)),
      });
    };

    for (let index = 1; index < fullDayIndices.length; index++) {
      const currentIndex = fullDayIndices[index];
      if (currentIndex !== previousIndex + 1) {
        pushSegment(rangeStart, previousIndex);
        rangeStart = currentIndex;
      }
      previousIndex = currentIndex;
    }

    pushSegment(rangeStart, previousIndex);
    return segments;
  });

  return packWeekSegments(rawSegments);
}

export function layoutOverlappingEvents(
  events: CalendarEvent[],
  day: Date,
): TimedEventLayout[] {
  const layoutInputs = events.flatMap((event) => {
    const bounds = getTimedEventBoundsForDay(event, day);
    return bounds ? [{ event, ...bounds }] : [];
  });

  const sorted = layoutInputs.sort((a, b) => {
    const diff = a.startMinutes - b.startMinutes;
    if (diff !== 0) return diff;
    return (b.endMinutes - b.startMinutes) - (a.endMinutes - a.startMinutes);
  });

  const result: TimedEventLayout[] = [];
  let columns: { event: CalendarEvent; end: number }[][] = [];
  let clusterStart = 0;
  let clusterMaxEnd = 0;

  const flushCluster = () => {
    const total = columns.length;
    for (let i = clusterStart; i < result.length; i++) {
      result[i].totalColumns = total;
    }
  };

  for (const event of sorted) {
    if (columns.length > 0 && event.startMinutes >= clusterMaxEnd) {
      flushCluster();
      clusterStart = result.length;
      columns = [];
      clusterMaxEnd = 0;
    }

    let placed = false;
    for (let col = 0; col < columns.length; col++) {
      if (columns[col].every(e => e.end <= event.startMinutes)) {
        columns[col].push({ event: event.event, end: event.endMinutes });
        result.push({ ...event, column: col, totalColumns: 0 });
        placed = true;
        break;
      }
    }
    if (!placed) {
      columns.push([{ event: event.event, end: event.endMinutes }]);
      result.push({ ...event, column: columns.length - 1, totalColumns: 0 });
    }
    clusterMaxEnd = Math.max(clusterMaxEnd, event.endMinutes);
  }

  flushCluster();
  return result;
}

export function formatSnapTime(minutes: number, timeFormat: "12h" | "24h"): string {
  const clamped = Math.max(0, Math.min(1440, minutes));
  const h = Math.floor(clamped / 60) % 24;
  const m = clamped % 60;
  if (timeFormat === "12h") {
    return `${h % 12 || 12}:${String(m).padStart(2, "0")} ${h < 12 ? "AM" : "PM"}`;
  }
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

export function getPrimaryCalendarId(event: Pick<CalendarEvent, 'calendarIds'>): string | undefined {
  return Object.keys(event.calendarIds || {})[0];
}

export function formatIsoInTimeZone(date: Date, timeZone: string): string {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  }).formatToParts(date);
  const map: Record<string, string> = {};
  for (const part of parts) {
    if (part.type !== "literal") map[part.type] = part.value;
  }
  const hour = map.hour === "24" ? "00" : map.hour;
  return `${map.year}-${map.month}-${map.day}T${hour}:${map.minute}:${map.second}`;
}
