/**
 * Romanian working-day arithmetic.
 *
 * Fixed public holidays + Orthodox Easter-derived moveable feasts.
 * Uses the Meeus algorithm for Orthodox Easter computation.
 */

// ── Fixed Romanian public holidays (month is 0-indexed) ──

interface FixedHoliday {
  month: number;
  day: number;
}

const FIXED_HOLIDAYS: FixedHoliday[] = [
  { month: 0, day: 1 },   // Anul Nou
  { month: 0, day: 2 },   // Anul Nou (zi 2)
  { month: 0, day: 24 },  // Ziua Unirii
  { month: 4, day: 1 },   // Ziua Muncii
  { month: 5, day: 1 },   // Ziua Copilului
  { month: 7, day: 15 },  // Adormirea Maicii Domnului
  { month: 10, day: 30 }, // Sfântul Andrei
  { month: 11, day: 1 },  // Ziua Națională
  { month: 11, day: 25 }, // Crăciunul
  { month: 11, day: 26 }, // Crăciunul (zi 2)
];

// ── Orthodox Easter via Meeus algorithm ──

function orthodoxEaster(year: number): Date {
  const a = year % 4;
  const b = year % 7;
  const c = year % 19;
  const d = (19 * c + 15) % 30;
  const e = (2 * a + 4 * b - d + 34) % 7;
  const month = Math.floor((d + e + 114) / 31); // 3 = March, 4 = April
  const day = ((d + e + 114) % 31) + 1;

  // Julian date — convert to Gregorian by adding 13 days (valid 1900–2099)
  const julian = new Date(year, month - 1, day);
  julian.setDate(julian.getDate() + 13);
  return julian;
}

function getMovableHolidays(year: number): Date[] {
  const easter = orthodoxEaster(year);

  const goodFriday = new Date(easter);
  goodFriday.setDate(easter.getDate() - 2);

  const easterMonday = new Date(easter);
  easterMonday.setDate(easter.getDate() + 1);

  const rusaliiSunday = new Date(easter);
  rusaliiSunday.setDate(easter.getDate() + 49);

  const rusaliiMonday = new Date(easter);
  rusaliiMonday.setDate(easter.getDate() + 50);

  return [goodFriday, easter, easterMonday, rusaliiSunday, rusaliiMonday];
}

// ── Holiday cache per year ──

const holidayCache = new Map<number, Set<string>>();

function toKey(d: Date): string {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

function getHolidaySet(year: number): Set<string> {
  const cached = holidayCache.get(year);
  if (cached) return cached;

  const set = new Set<string>();

  for (const h of FIXED_HOLIDAYS) {
    set.add(toKey(new Date(year, h.month, h.day)));
  }

  for (const d of getMovableHolidays(year)) {
    set.add(toKey(d));
  }

  holidayCache.set(year, set);
  return set;
}

// ── Public API ──

export function isPublicHoliday(date: Date): boolean {
  return getHolidaySet(date.getFullYear()).has(toKey(date));
}

export function isWeekend(date: Date): boolean {
  const day = date.getDay();
  return day === 0 || day === 6;
}

export function isWorkingDay(date: Date): boolean {
  return !isWeekend(date) && !isPublicHoliday(date);
}

/**
 * Add calendar days (simply skips no days).
 * Supports negative values.
 */
export function addCalendarDays(start: Date, days: number): Date {
  const result = new Date(start);
  result.setDate(result.getDate() + days);
  return result;
}

/**
 * Add working days, skipping weekends and Romanian public holidays.
 * Supports negative values (for backward deadlines).
 */
export function addWorkingDays(start: Date, days: number): Date {
  const result = new Date(start);
  const direction = days >= 0 ? 1 : -1;
  let remaining = Math.abs(days);

  while (remaining > 0) {
    result.setDate(result.getDate() + direction);
    if (isWorkingDay(result)) {
      remaining--;
    }
  }

  return result;
}

/**
 * Compute the due date for a deadline definition.
 */
export function computeDueDate(
  startDate: Date,
  days: number,
  dayType: 'calendar' | 'working',
  isBackward?: boolean,
): Date {
  const effectiveDays = isBackward ? -days : days;
  return dayType === 'working'
    ? addWorkingDays(startDate, effectiveDays)
    : addCalendarDays(startDate, effectiveDays);
}
