import { BATCH_TIMEZONE } from './booking-batch.constants';

/** EAT intake boundaries — cron runs at these hours; each window spans to the next. */
export const BATCH_WINDOW_START_HOURS = [7, 10, 13, 16, 19, 22] as const;

export interface BatchWindow {
  key: string;
  label: string;
  start: Date;
  end: Date;
}

type EatDateParts = {
  year: number;
  month: number;
  day: number;
  hour: number;
  minute: number;
};

const dateFmt = new Intl.DateTimeFormat('en-GB', {
  day: '2-digit',
  month: 'short',
  year: 'numeric',
  timeZone: BATCH_TIMEZONE,
});

const timeFmt = new Intl.DateTimeFormat('en-GB', {
  hour: '2-digit',
  minute: '2-digit',
  hour12: false,
  timeZone: BATCH_TIMEZONE,
});

function eatParts(date: Date): EatDateParts {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: BATCH_TIMEZONE,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  }).formatToParts(date);

  const get = (type: Intl.DateTimeFormatPartTypes) =>
    Number(parts.find((p) => p.type === type)?.value ?? 0);

  return {
    year: get('year'),
    month: get('month'),
    day: get('day'),
    hour: get('hour'),
    minute: get('minute'),
  };
}

/**
 * The EAT calendar day a timestamp falls on, as `yyyy-MM-dd`. This is the day
 * key for day-level booking pools — it must match the day the portal calendar
 * renders, so always derive day keys through this (never `toISOString().slice`).
 */
export function eatDay(date: Date): string {
  const { year, month, day } = eatParts(date);
  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}

/** Build a UTC Date for a given EAT local wall-clock time on a calendar day. */
function eatToUtc(
  year: number,
  month: number,
  day: number,
  hour: number,
  minute = 0,
): Date {
  // EAT is UTC+3 year-round (no DST). Binary search would be safer across DST zones;
  // for Africa/Addis_Ababa the offset is fixed.
  const utcMs = Date.UTC(year, month - 1, day, hour - 3, minute, 0, 0);
  return new Date(utcMs);
}

function formatWindowLabel(start: Date, end: Date, endHourLabel?: string): string {
  const endTime = endHourLabel ?? timeFmt.format(new Date(end.getTime() - 60_000));
  return `${dateFmt.format(start)} · ${timeFmt.format(start)} – ${endTime} EAT`;
}

function windowFromEatStart(
  year: number,
  month: number,
  day: number,
  startHour: number,
): BatchWindow {
  const start = eatToUtc(year, month, day, startHour);
  let endYear = year;
  let endMonth = month;
  let endDay = day;
  let endHour: number;
  let endHourLabel: string;

  const idx = BATCH_WINDOW_START_HOURS.indexOf(startHour as (typeof BATCH_WINDOW_START_HOURS)[number]);
  if (idx === BATCH_WINDOW_START_HOURS.length - 1) {
    endHour = 7;
    endHourLabel = '07:00';
    const next = new Date(eatToUtc(year, month, day, 0));
    next.setUTCDate(next.getUTCDate() + 1);
    const nextParts = eatParts(next);
    endYear = nextParts.year;
    endMonth = nextParts.month;
    endDay = nextParts.day;
  } else {
    endHour = BATCH_WINDOW_START_HOURS[idx + 1];
    endHourLabel = `${String(endHour).padStart(2, '0')}:00`;
  }

  const end = eatToUtc(endYear, endMonth, endDay, endHour);
  return {
    key: start.toISOString(),
    start,
    end,
    label: formatWindowLabel(start, end, endHourLabel),
  };
}

/** Which 3h EAT intake window a timestamp (e.g. fullyExecutedAt) belongs to. */
export function getBatchWindowForTimestamp(date: Date): BatchWindow {
  const { year, month, day, hour } = eatParts(date);

  if (hour < 7) {
    const prev = new Date(eatToUtc(year, month, day, 0));
    prev.setUTCDate(prev.getUTCDate() - 1);
    const prevParts = eatParts(prev);
    return windowFromEatStart(prevParts.year, prevParts.month, prevParts.day, 22);
  }

  let startHour: (typeof BATCH_WINDOW_START_HOURS)[number] = 7;
  for (const h of BATCH_WINDOW_START_HOURS) {
    if (hour >= h) startHour = h;
  }

  return windowFromEatStart(year, month, day, startHour);
}

/** All six intake windows for an EAT calendar day (includes overnight 22:00–07:00). */
export function listBatchWindowsForDate(reference: Date): BatchWindow[] {
  const { year, month, day } = eatParts(reference);
  return BATCH_WINDOW_START_HOURS.map((startHour) =>
    windowFromEatStart(year, month, day, startHour),
  );
}

export function compareBatchWindows(a: BatchWindow, b: BatchWindow): number {
  return a.start.getTime() - b.start.getTime();
}

/** Schedule-day windows plus any extra windows that contain booking timestamps (cross-day). */
export function listBatchWindowsForBookings(
  timestamps: Array<Date | null | undefined>,
  referenceDate: Date,
): BatchWindow[] {
  const byKey = new Map<string, BatchWindow>();
  for (const w of listBatchWindowsForDate(referenceDate)) {
    byKey.set(w.key, w);
  }
  for (const ts of timestamps) {
    if (!ts) continue;
    const w = getBatchWindowForTimestamp(ts);
    byKey.set(w.key, w);
  }
  return [...byKey.values()].sort(compareBatchWindows);
}

// ---------------------------------------------------------------------------
// Board-display windows: full-day, midnight-based 3h slots over a date range.
// These are used ONLY for the batch-board UI grouping (not persisted, and
// independent of the cron intake hours above).
// ---------------------------------------------------------------------------

/** Midnight-based 3-hour slot starts (00–03, 03–06, … 21–24). */
export const BOARD_WINDOW_HOURS = [0, 3, 6, 9, 12, 15, 18, 21] as const;

/** A board window carries an EAT calendar date in addition to the slot times. */
export interface BoardWindow extends BatchWindow {
  /** EAT calendar day as ISO `YYYY-MM-DD`. */
  date: string;
  /** Human label for the day, e.g. `Thu, 05 Jun`. */
  dateLabel: string;
}

const dayLabelFmt = new Intl.DateTimeFormat('en-GB', {
  weekday: 'short',
  day: '2-digit',
  month: 'short',
  timeZone: BATCH_TIMEZONE,
});

function pad2(n: number): string {
  return String(n).padStart(2, '0');
}

/** Build a midnight-based 3h board window for an EAT calendar day + slot start hour. */
function boardWindowFromEatStart(
  year: number,
  month: number,
  day: number,
  startHour: number,
): BoardWindow {
  const start = eatToUtc(year, month, day, startHour);
  const endHour = startHour + 3; // 21 -> 24 (handled by Date.UTC roll-over)
  const end = eatToUtc(year, month, day, endHour);
  const endLabel = endHour >= 24 ? '24:00' : `${pad2(endHour)}:00`;
  return {
    key: start.toISOString(),
    start,
    end,
    label: formatWindowLabel(start, end, endLabel),
    date: `${year}-${pad2(month)}-${pad2(day)}`,
    dateLabel: dayLabelFmt.format(start),
  };
}

/** Which midnight-based 3h EAT slot a timestamp falls in. */
export function boardWindowForTimestamp(date: Date): BoardWindow {
  const { year, month, day, hour } = eatParts(date);
  let startHour: (typeof BOARD_WINDOW_HOURS)[number] = 0;
  for (const h of BOARD_WINDOW_HOURS) {
    if (hour >= h) startHour = h;
  }
  return boardWindowFromEatStart(year, month, day, startHour);
}

/**
 * Continuous list of board windows from `openDate` to `departureDate` (inclusive),
 * clamped to the slot containing `openDate` on the first day and the slot
 * containing `departureDate` on the last day. Returned in chronological order.
 */
export function listBoardWindowsForRange(
  openDate: Date,
  departureDate: Date,
): BoardWindow[] {
  const startWin = boardWindowForTimestamp(openDate);
  const endWin = boardWindowForTimestamp(departureDate);
  // Guard against an inverted range (departure before open).
  if (endWin.start.getTime() < startWin.start.getTime()) {
    return [startWin];
  }

  const windows: BoardWindow[] = [];
  const seen = new Set<string>();
  // Walk day-by-day in EAT, emitting each day's slots, stepping via UTC noon to
  // avoid any boundary ambiguity, then filter to [startWin.start, endWin.start].
  let cursor = new Date(eatToUtc(
    Number(startWin.date.slice(0, 4)),
    Number(startWin.date.slice(5, 7)),
    Number(startWin.date.slice(8, 10)),
    12,
  ));
  const lastDayMs = eatToUtc(
    Number(endWin.date.slice(0, 4)),
    Number(endWin.date.slice(5, 7)),
    Number(endWin.date.slice(8, 10)),
    12,
  ).getTime();

  while (cursor.getTime() <= lastDayMs) {
    const { year, month, day } = eatParts(cursor);
    for (const h of BOARD_WINDOW_HOURS) {
      const w = boardWindowFromEatStart(year, month, day, h);
      if (
        w.start.getTime() >= startWin.start.getTime() &&
        w.start.getTime() <= endWin.start.getTime() &&
        !seen.has(w.key)
      ) {
        seen.add(w.key);
        windows.push(w);
      }
    }
    cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
  }

  windows.sort(compareBatchWindows);
  return windows;
}

/**
 * Group items into board windows spanning [openDate, departureDate]. Empty
 * windows are kept so the UI shows every slot. Items whose timestamp falls
 * outside the range still get their own window (nothing hidden). Items without
 * a timestamp go to `pendingKey`.
 */
export function groupBookingsIntoBoardWindows<T>(
  items: T[],
  getTimestamp: (item: T) => Date | null | undefined,
  openDate: Date,
  departureDate: Date,
  pendingKey = 'pending-contract',
): Map<string, { window: BoardWindow | null; items: T[] }> {
  const map = new Map<string, { window: BoardWindow | null; items: T[] }>();

  for (const w of listBoardWindowsForRange(openDate, departureDate)) {
    map.set(w.key, { window: w, items: [] });
  }
  map.set(pendingKey, { window: null, items: [] });

  for (const item of items) {
    const ts = getTimestamp(item);
    if (!ts) {
      map.get(pendingKey)!.items.push(item);
      continue;
    }
    const w = boardWindowForTimestamp(ts);
    if (!map.has(w.key)) {
      map.set(w.key, { window: w, items: [] });
    }
    map.get(w.key)!.items.push(item);
  }

  return map;
}

/** Group items by batch window key; items without a timestamp go to `pendingKey`. */
export function groupByBatchWindow<T>(
  items: T[],
  getTimestamp: (item: T) => Date | null | undefined,
  referenceDate: Date,
  pendingKey = 'pending-contract',
): Map<string, { window: BatchWindow | null; items: T[] }> {
  const timestamps = items.map(getTimestamp);
  const windows = listBatchWindowsForBookings(timestamps, referenceDate);
  const map = new Map<string, { window: BatchWindow | null; items: T[] }>();

  for (const w of windows) {
    map.set(w.key, { window: w, items: [] });
  }
  map.set(pendingKey, { window: null, items: [] });

  for (const item of items) {
    const ts = getTimestamp(item);
    if (!ts) {
      map.get(pendingKey)!.items.push(item);
      continue;
    }
    const w = getBatchWindowForTimestamp(ts);
    if (!map.has(w.key)) {
      map.set(w.key, { window: w, items: [] });
    }
    map.get(w.key)!.items.push(item);
  }

  return map;
}
