import { zonedWeekdayAndMinutes } from './zoned-civil-time';

export const MINUTES_PER_DAY = 24 * 60;

export type WorkingHoursBlockInput = {
  dayOfWeek: number;
  startMinute: number;
  endMinute: number;
  sortOrder?: number;
};

export type WorkingHoursDayBlock = {
  startMinute: number;
  endMinute: number;
};

export function localDayOfWeekMondayZero(dayOfWeekJs: number): number {
  return dayOfWeekJs === 0 ? 6 : dayOfWeekJs - 1;
}

export function validateWorkingHoursBlocks(blocks: WorkingHoursBlockInput[]): string | null {
  if (!Array.isArray(blocks)) {
    return 'Working hours blocks must be an array';
  }

  const byDay = new Map<number, WorkingHoursBlockInput[]>();
  for (const block of blocks) {
    if (!Number.isInteger(block.dayOfWeek) || block.dayOfWeek < 0 || block.dayOfWeek > 6) {
      return 'dayOfWeek must be an integer from 0 (Monday) to 6 (Sunday)';
    }
    if (!Number.isInteger(block.startMinute) || block.startMinute < 0 || block.startMinute >= MINUTES_PER_DAY) {
      return 'startMinute must be between 0 and 1439';
    }
    if (!Number.isInteger(block.endMinute) || block.endMinute <= 0 || block.endMinute > MINUTES_PER_DAY) {
      return 'endMinute must be between 1 and 1440';
    }
    if (block.endMinute <= block.startMinute) {
      return 'Each shift end time must be after its start time';
    }
    const list = byDay.get(block.dayOfWeek) ?? [];
    list.push(block);
    byDay.set(block.dayOfWeek, list);
  }

  for (const [, dayBlocks] of byDay) {
    const sorted = [...dayBlocks].sort((a, b) => a.startMinute - b.startMinute);
    for (let i = 1; i < sorted.length; i += 1) {
      if (sorted[i].startMinute < sorted[i - 1].endMinute) {
        return 'Shifts on the same day cannot overlap';
      }
    }
  }

  return null;
}

export function blocksForDay(
  blocks: WorkingHoursBlockInput[],
  dayOfWeek: number,
): WorkingHoursDayBlock[] {
  return blocks
    .filter((b) => b.dayOfWeek === dayOfWeek)
    .sort((a, b) => a.startMinute - b.startMinute || a.endMinute - b.endMinute)
    .map((b) => ({ startMinute: b.startMinute, endMinute: b.endMinute }));
}

export function isMinuteWithinWorkingBlocks(minute: number, dayBlocks: WorkingHoursDayBlock[]): boolean {
  return dayBlocks.some((b) => minute >= b.startMinute && minute < b.endMinute);
}

export function isRangeWithinWorkingBlocks(
  startMinute: number,
  endMinute: number,
  dayBlocks: WorkingHoursDayBlock[],
): boolean {
  if (endMinute <= startMinute) {
    return false;
  }
  for (let m = startMinute; m < endMinute; m += 1) {
    if (!isMinuteWithinWorkingBlocks(m, dayBlocks)) {
      return false;
    }
  }
  return true;
}

export function dateToZonedMinutes(date: Date, timeZone: string): number {
  return zonedWeekdayAndMinutes(date, timeZone).minuteOfDay;
}

export function appointmentWithinWorkingHours(
  startAt: Date,
  endAt: Date,
  dayBlocks: WorkingHoursDayBlock[],
  timeZone: string,
): boolean {
  const startMinute = dateToZonedMinutes(startAt, timeZone);
  const endMinute = dateToZonedMinutes(endAt, timeZone);
  return isRangeWithinWorkingBlocks(startMinute, endMinute, dayBlocks);
}

export function unionDayBlockRange(dayBlocksList: WorkingHoursDayBlock[][]): {
  startMinute: number;
  endMinute: number;
} | null {
  let startMinute: number | null = null;
  let endMinute: number | null = null;

  for (const dayBlocks of dayBlocksList) {
    for (const block of dayBlocks) {
      startMinute = startMinute == null ? block.startMinute : Math.min(startMinute, block.startMinute);
      endMinute = endMinute == null ? block.endMinute : Math.max(endMinute, block.endMinute);
    }
  }

  if (startMinute == null || endMinute == null) {
    return null;
  }

  return { startMinute, endMinute };
}
