/**
 * EventLayoutEngine - Simplified stacking/grouping algorithm
 *
 * Supports two layout modes:
 * - GRID: Events starting at same time rendered side-by-side
 * - STACKING: Overlapping events with margin-left offset (15px per level)
 *
 * No prev/next chains, single-pass greedy algorithm
 */

import { ICalendarEvent } from '../../types/CalendarTypes';
import { IGridConfig } from '../../core/IGridConfig';
import { calculateEventPosition } from '../../utils/PositionUtils';
import { IColumnLayout, IGridGroupLayout, IStackedEventLayout } from './EventLayoutTypes';

/**
 * Check if two events overlap (strict - touching at boundary = NOT overlapping)
 * This matches Scenario 8: end===start is NOT overlap
 */
export function eventsOverlap(a: ICalendarEvent, b: ICalendarEvent): boolean {
  return a.start < b.end && a.end > b.start;
}

/**
 * Check if two events are within threshold for grid grouping.
 * This includes:
 * 1. Start-to-start: Events start within threshold of each other
 * 2. End-to-start: One event starts within threshold before another ends
 */
function eventsWithinThreshold(a: ICalendarEvent, b: ICalendarEvent, thresholdMinutes: number): boolean {
  const thresholdMs = thresholdMinutes * 60 * 1000;

  // Start-to-start: both events start within threshold
  const startToStartDiff = Math.abs(a.start.getTime() - b.start.getTime());
  if (startToStartDiff <= thresholdMs) return true;

  // End-to-start: one event starts within threshold before the other ends
  // B starts within threshold before A ends
  const bStartsBeforeAEnds = a.end.getTime() - b.start.getTime();
  if (bStartsBeforeAEnds > 0 && bStartsBeforeAEnds <= thresholdMs) return true;

  // A starts within threshold before B ends
  const aStartsBeforeBEnds = b.end.getTime() - a.start.getTime();
  if (aStartsBeforeBEnds > 0 && aStartsBeforeBEnds <= thresholdMs) return true;

  return false;
}

/**
 * Check if all events in a group start within threshold of each other
 */
function allStartWithinThreshold(events: ICalendarEvent[], thresholdMinutes: number): boolean {
  if (events.length <= 1) return true;

  // Find earliest and latest start times
  let earliest = events[0].start.getTime();
  let latest = events[0].start.getTime();

  for (const event of events) {
    const time = event.start.getTime();
    if (time < earliest) earliest = time;
    if (time > latest) latest = time;
  }

  const diffMinutes = (latest - earliest) / (1000 * 60);
  return diffMinutes <= thresholdMinutes;
}

/**
 * Find groups of overlapping events (connected by overlap chain)
 * Events are grouped if they overlap with any event in the group
 */
function findOverlapGroups(events: ICalendarEvent[]): ICalendarEvent[][] {
  if (events.length === 0) return [];

  const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
  const used = new Set<string>();
  const groups: ICalendarEvent[][] = [];

  for (const event of sorted) {
    if (used.has(event.id)) continue;

    // Start a new group with this event
    const group: ICalendarEvent[] = [event];
    used.add(event.id);

    // Expand group by finding all connected events (via overlap)
    let expanded = true;
    while (expanded) {
      expanded = false;
      for (const candidate of sorted) {
        if (used.has(candidate.id)) continue;

        // Check if candidate overlaps with any event in group
        const connects = group.some(member => eventsOverlap(member, candidate));

        if (connects) {
          group.push(candidate);
          used.add(candidate.id);
          expanded = true;
        }
      }
    }

    groups.push(group);
  }

  return groups;
}

/**
 * Find grid candidates within a group - events connected via threshold chain
 * Uses V1 logic: events are connected if within threshold (no overlap requirement)
 */
function findGridCandidates(
  events: ICalendarEvent[],
  thresholdMinutes: number
): ICalendarEvent[][] {
  if (events.length === 0) return [];

  const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
  const used = new Set<string>();
  const groups: ICalendarEvent[][] = [];

  for (const event of sorted) {
    if (used.has(event.id)) continue;

    const group: ICalendarEvent[] = [event];
    used.add(event.id);

    // Expand by threshold chain (V1 logic: no overlap requirement, just threshold)
    let expanded = true;
    while (expanded) {
      expanded = false;
      for (const candidate of sorted) {
        if (used.has(candidate.id)) continue;

        const connects = group.some(member =>
          eventsWithinThreshold(member, candidate, thresholdMinutes)
        );

        if (connects) {
          group.push(candidate);
          used.add(candidate.id);
          expanded = true;
        }
      }
    }

    groups.push(group);
  }

  return groups;
}

/**
 * Calculate stack levels for overlapping events using greedy algorithm
 * For each event: level = max(overlapping already-processed events) + 1
 */
function calculateStackLevels(events: ICalendarEvent[]): Map<string, number> {
  const levels = new Map<string, number>();
  const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());

  for (const event of sorted) {
    let maxOverlappingLevel = -1;

    // Find max level among overlapping events already processed
    for (const [id, level] of levels) {
      const other = events.find(e => e.id === id);
      if (other && eventsOverlap(event, other)) {
        maxOverlappingLevel = Math.max(maxOverlappingLevel, level);
      }
    }

    levels.set(event.id, maxOverlappingLevel + 1);
  }

  return levels;
}

/**
 * Allocate events to columns for GRID layout using greedy algorithm
 * Non-overlapping events can share a column to minimize total columns
 */
function allocateColumns(events: ICalendarEvent[]): ICalendarEvent[][] {
  const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
  const columns: ICalendarEvent[][] = [];

  for (const event of sorted) {
    // Find first column where event doesn't overlap with existing events
    let placed = false;
    for (const column of columns) {
      const canFit = !column.some(e => eventsOverlap(event, e));
      if (canFit) {
        column.push(event);
        placed = true;
        break;
      }
    }

    // No suitable column found, create new one
    if (!placed) {
      columns.push([event]);
    }
  }

  return columns;
}

/**
 * Main entry point: Calculate complete layout for a column's events
 *
 * Algorithm:
 * 1. Find overlap groups (events connected by overlap chain)
 * 2. For each overlap group, find grid candidates (events within threshold chain)
 * 3. If all events in overlap group form a single grid candidate → GRID mode
 * 4. Otherwise → STACKING mode with calculated levels
 */
export function calculateColumnLayout(
  events: ICalendarEvent[],
  config: IGridConfig
): IColumnLayout {
  const thresholdMinutes = config.gridStartThresholdMinutes ?? 10;

  const result: IColumnLayout = {
    grids: [],
    stacked: []
  };

  if (events.length === 0) return result;

  // Find all overlapping event groups
  const overlapGroups = findOverlapGroups(events);

  for (const overlapGroup of overlapGroups) {
    if (overlapGroup.length === 1) {
      // Single event - no grouping needed
      result.stacked.push({
        event: overlapGroup[0],
        stackLevel: 0
      });
      continue;
    }

    // Within this overlap group, find grid candidates (threshold-connected subgroups)
    const gridSubgroups = findGridCandidates(overlapGroup, thresholdMinutes);

    // Check if the ENTIRE overlap group forms a single grid candidate
    // This happens when all events are connected via threshold chain
    const largestGridCandidate = gridSubgroups.reduce((max, g) =>
      g.length > max.length ? g : max, gridSubgroups[0]);

    if (largestGridCandidate.length === overlapGroup.length) {
      // All events in overlap group are connected via threshold chain → GRID mode
      const columns = allocateColumns(overlapGroup);
      const earliest = overlapGroup.reduce((min, e) =>
        e.start < min.start ? e : min, overlapGroup[0]);
      const position = calculateEventPosition(earliest.start, earliest.end, config);

      result.grids.push({
        events: overlapGroup,
        columns,
        stackLevel: 0,
        position: { top: position.top }
      });
    } else {
      // Not all events connected via threshold → STACKING mode
      const levels = calculateStackLevels(overlapGroup);
      for (const event of overlapGroup) {
        result.stacked.push({
          event,
          stackLevel: levels.get(event.id) ?? 0
        });
      }
    }
  }

  return result;
}
