import type { Tables } from "@xtablo/shared-types";
import { DateTime } from "luxon";

// Types for availability calculation
type TimeRange = {
  start: string;
  end: string;
};

type DayAvailability = {
  enabled: boolean;
  timeRanges: TimeRange[];
};

export type WeeklyAvailability = {
  [key: number]: DayAvailability;
};

export type Exception = {
  date: string;
} & (
  | {
      type: "hours";
      hours: TimeRange[];
    }
  | {
      type: "day";
    }
);

export type TimeSlot = {
  date: string;
  time: string;
  available: boolean;
};

export type EventTypeConfig = {
  name: string;
  description: string;
  duration: number; // in minutes
  bufferTime?: number; // buffer time before/after in minutes
  maxBookingsPerDay?: number;
  requiresApproval: boolean;
  price?: number;
  location?: string;
  minAdvanceBooking?: {
    value: number;
    unit: "minutes" | "hours" | "days";
  };
};

// Helper functions for slot calculation
function parseTime(timeStr: string): { hours: number; minutes: number } {
  const [hours, minutes] = timeStr.split(":").map(Number);
  return { hours, minutes };
}

function formatTime(hours: number, minutes: number): string {
  return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
}

function addMinutes(timeStr: string, minutesToAdd: number): string {
  const { hours, minutes } = parseTime(timeStr);
  const totalMinutes = hours * 60 + minutes + minutesToAdd;

  // Handle negative time (before 00:00)
  if (totalMinutes < 0) {
    return "00:00"; // Return start of day for negative times
  }

  // Handle time beyond 24 hours
  const dayMinutes = totalMinutes % (24 * 60);
  const newHours = Math.floor(dayMinutes / 60);
  const newMinutes = dayMinutes % 60;
  return formatTime(newHours, newMinutes);
}

function mergeOverlappingTimeRanges(ranges: TimeRange[]): TimeRange[] {
  if (ranges.length <= 1) return ranges;

  // Sort ranges by start time
  const sortedRanges = [...ranges].sort((a, b) => a.start.localeCompare(b.start));
  const merged: TimeRange[] = [sortedRanges[0]];

  for (let i = 1; i < sortedRanges.length; i++) {
    const current = sortedRanges[i];
    const lastMerged = merged[merged.length - 1];

    // Check if current range overlaps with the last merged range
    if (current.start <= lastMerged.end) {
      // Merge by extending the end time if current range extends further
      lastMerged.end = current.end > lastMerged.end ? current.end : lastMerged.end;
    } else {
      // No overlap, add current range to merged array
      merged.push(current);
    }
  }

  return merged;
}

export function getDayOfWeek(date: Date): number {
  // Convert JS day (0=Sunday) to our format (0=Monday)
  const jsDay = date.getDay();
  return jsDay === 0 ? 6 : jsDay - 1;
}

function getMinAdvanceBookingDate(
  config: EventTypeConfig,
  currentDate: Date
): { date: string; time: string } {
  if (!config.minAdvanceBooking) {
    return {
      date: getDateStringCET(currentDate),
      time: formatTime(currentDate.getHours(), currentDate.getMinutes()),
    };
  }

  const { value, unit } = config.minAdvanceBooking;
  const advanceDate = new Date(currentDate);

  switch (unit) {
    case "minutes":
      advanceDate.setMinutes(currentDate.getMinutes() + value);
      break;
    case "hours":
      advanceDate.setHours(currentDate.getHours() + value);
      break;
    case "days":
      advanceDate.setDate(currentDate.getDate() + value);
      break;
  }

  return {
    date: getDateStringCET(advanceDate),
    time: formatTime(advanceDate.getHours(), advanceDate.getMinutes()),
  };
}

export function getDateStringCET(date: Date): string {
  return DateTime.fromJSDate(date).setZone("Europe/Paris").toFormat("yyyy-MM-dd");
}

export function generateTimeSlots(
  currentTime: Date, // in CET
  date: Date, // in CET
  availability: DayAvailability,
  eventTypeConfig: EventTypeConfig,
  exceptions: Exception[],
  existingEvents: Tables<"events">[]
): TimeSlot[] {
  const dateStr = getDateStringCET(date);
  const slots: TimeSlot[] = [];

  // Check if this date has an exception
  const exception = exceptions.find((e) => {
    const exceptionDate = new Date(e.date);
    const exceptionDateStr = getDateStringCET(exceptionDate);
    return exceptionDateStr === dateStr;
  });

  // If there's a "day" exception, no slots available
  if (exception?.type === "day") {
    return slots;
  }

  // Get time ranges for this day
  let timeRanges: TimeRange[];
  if (exception?.type === "hours") {
    timeRanges = mergeOverlappingTimeRanges(exception.hours);
  } else if (availability.enabled) {
    timeRanges = mergeOverlappingTimeRanges(availability.timeRanges);
  } else {
    return slots; // Day not available
  }

  // Check minimum advance booking
  const minAdvanceBooking = getMinAdvanceBookingDate(eventTypeConfig, currentTime);

  // Generate slots for each time range
  for (const range of timeRanges) {
    const startTime = parseTime(range.start);
    const endTime = parseTime(range.end);

    let currentMinutes = startTime.hours * 60 + startTime.minutes;
    const endMinutes = endTime.hours * 60 + endTime.minutes;

    while (currentMinutes + eventTypeConfig.duration <= endMinutes) {
      const slotTime = formatTime(Math.floor(currentMinutes / 60), currentMinutes % 60);

      // Check if slot is in the future (considering minimum advance booking)
      // Compare dates first, then times if on the same date
      const isInFuture =
        dateStr > minAdvanceBooking.date ||
        (dateStr === minAdvanceBooking.date && slotTime >= minAdvanceBooking.time);

      slots.push({
        date: dateStr,
        time: slotTime,
        available: isInFuture, // Will be updated later based on conflicts and buffer time
      });

      // Move to next slot (without buffer time - generate all possible slots)
      currentMinutes += eventTypeConfig.duration;
    }
  }

  // Apply buffer time and conflict detection to all generated slots
  const bufferTime = eventTypeConfig.bufferTime || 0;

  for (const slot of slots) {
    if (!slot.available) continue; // Skip slots already marked as unavailable

    const slotEndTime = addMinutes(slot.time, eventTypeConfig.duration);

    // Check if this slot conflicts with existing events (including buffer time)
    const hasConflict = existingEvents.some((event) => {
      if (event.start_date !== dateStr || event.deleted_at) return false;

      const eventStart = event.start_time;
      const eventEnd = event.end_time || addMinutes(eventStart, eventTypeConfig.duration);

      // Apply buffer time around the existing event
      const bufferedEventStart = addMinutes(eventStart, -bufferTime);
      const bufferedEventEnd = addMinutes(eventEnd, bufferTime);

      // Check for overlap with buffered event time
      return (
        (slot.time >= bufferedEventStart && slot.time < bufferedEventEnd) ||
        (slotEndTime > bufferedEventStart && slotEndTime <= bufferedEventEnd) ||
        (slot.time <= bufferedEventStart && slotEndTime >= bufferedEventEnd)
      );
    });

    if (hasConflict) {
      slot.available = false;
    }
  }

  // Apply max bookings per day limit
  if (eventTypeConfig.maxBookingsPerDay) {
    const existingBookingsCount = existingEvents.filter(
      (event) => event.start_date === dateStr && !event.deleted_at
    ).length;

    if (existingBookingsCount >= eventTypeConfig.maxBookingsPerDay) {
      return slots.map((slot) => ({ ...slot, available: false }));
    }
  }

  return slots;
}
