type HoursEntry = { open?: string; close?: string; closed?: boolean } | string | null | undefined;
type Hours = Record<string, HoursEntry>;

const DAY_KEYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const DAY_ALIASES: Record<string, string> = {
  sun: 'sun', sunday: 'sun',
  mon: 'mon', monday: 'mon',
  tue: 'tue', tues: 'tue', tuesday: 'tue',
  wed: 'wed', wednesday: 'wed',
  thu: 'thu', thur: 'thu', thurs: 'thu', thursday: 'thu',
  fri: 'fri', friday: 'fri',
  sat: 'sat', saturday: 'sat',
};

function parseTimeToMinutes(t: string | undefined): number | null {
  if (!t) return null;
  const m = /^(\d{1,2}):(\d{2})$/.exec(t.trim());
  if (!m) return null;
  const h = Number(m[1]); const mm = Number(m[2]);
  if (h < 0 || h > 24 || mm < 0 || mm > 59) return null;
  return h * 60 + mm;
}

function getEntry(hours: Hours, day: string): { open: number; close: number } | null {
  const e = hours?.[day];
  if (!e) return null;
  if (typeof e === 'string') {
    const [a, b] = e.split('-').map((s) => s.trim());
    const open = parseTimeToMinutes(a);
    const close = parseTimeToMinutes(b);
    if (open == null || close == null) return null;
    return { open, close };
  }
  if (typeof e === 'object') {
    if (e.closed) return null;
    const open = parseTimeToMinutes(e.open);
    const close = parseTimeToMinutes(e.close);
    if (open == null || close == null) return null;
    return { open, close };
  }
  return null;
}

function getZonedDowAndMinutes(now: Date, timezone?: string | null): { dow: number; minutes: number } {
  // Default to client local time if no timezone is provided
  if (!timezone) return { dow: now.getDay(), minutes: now.getHours() * 60 + now.getMinutes() };
  try {
    const fmt = new Intl.DateTimeFormat('en-US', {
      timeZone: timezone,
      weekday: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    });
    const parts = fmt.formatToParts(now);
    const wd = parts.find((p) => p.type === 'weekday')?.value ?? '';
    const hh = Number(parts.find((p) => p.type === 'hour')?.value ?? '0');
    // 'en-US' formats midnight as '24' in some runtimes — normalize.
    const hours = hh === 24 ? 0 : hh;
    const mm = Number(parts.find((p) => p.type === 'minute')?.value ?? '0');
    const wdMap: Record<string, number> = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
    const dow = wdMap[wd] ?? now.getDay();
    return { dow, minutes: hours * 60 + mm };
  } catch {
    return { dow: now.getDay(), minutes: now.getHours() * 60 + now.getMinutes() };
  }
}

export function isOpenNow(
  rawHours: Record<string, unknown> | null | undefined,
  timezone?: string | null,
  now: Date = new Date(),
): boolean | null {
  if (!rawHours || Object.keys(rawHours).length === 0) return null;
  // normalize keys
  const hours: Hours = {};
  for (const [k, v] of Object.entries(rawHours)) {
    const norm = DAY_ALIASES[k.toLowerCase()] ?? k.toLowerCase();
    hours[norm] = v as HoursEntry;
  }
  const { dow, minutes } = getZonedDowAndMinutes(now, timezone);
  const todayKey = DAY_KEYS[dow];

  const today = getEntry(hours, todayKey);
  if (today) {
    if (today.close > today.open) {
      if (minutes >= today.open && minutes < today.close) return true;
    } else {
      // wraps midnight
      if (minutes >= today.open || minutes < today.close) return true;
    }
  }
  // Check yesterday for late-night spillover
  const yKey = DAY_KEYS[(dow + 6) % 7];
  const yest = getEntry(hours, yKey);
  if (yest && yest.close <= yest.open && minutes < yest.close) return true;

  return false;
}
