const FIXED_REFERENCE_EVENTS = {
  new_years_day: { label: 'Neujahr', month: 0, day: 1 },
  epiphany: { label: 'Heilige Drei Könige', month: 0, day: 6 },
  valentines_day: { label: 'Valentinstag', month: 1, day: 14 },
  womens_day: { label: 'Internationaler Frauentag', month: 2, day: 8 },
  may_day: { label: 'Tag der Arbeit', month: 4, day: 1 },
  german_unity_day: { label: 'Tag der Deutschen Einheit', month: 9, day: 3 },
  halloween: { label: 'Halloween', month: 9, day: 31 },
  all_saints_day: { label: 'Allerheiligen', month: 10, day: 1 },
  st_nicholas_day: { label: 'Nikolaus', month: 11, day: 6 },
  christmas_eve: { label: 'Heiligabend', month: 11, day: 24 },
  christmas_day: { label: '1. Weihnachtstag', month: 11, day: 25 },
  boxing_day: { label: '2. Weihnachtstag', month: 11, day: 26 },
  new_years_eve: { label: 'Silvester', month: 11, day: 31 }
};

const EASTER_REFERENCE_EVENTS = {
  carnival_monday: { label: 'Rosenmontag', offsetDays: -48 },
  ash_wednesday: { label: 'Aschermittwoch', offsetDays: -46 },
  palm_sunday: { label: 'Palmsonntag', offsetDays: -7 },
  maundy_thursday: { label: 'Gründonnerstag', offsetDays: -3 },
  good_friday: { label: 'Karfreitag', offsetDays: -2 },
  easter_sunday: { label: 'Ostersonntag', offsetDays: 0 },
  easter_monday: { label: 'Ostermontag', offsetDays: 1 },
  ascension_day: { label: 'Christi Himmelfahrt', offsetDays: 39 },
  pentecost_sunday: { label: 'Pfingstsonntag', offsetDays: 49 },
  pentecost_monday: { label: 'Pfingstmontag', offsetDays: 50 },
  corpus_christi: { label: 'Fronleichnam', offsetDays: 60 }
};

const REFERENCE_EVENT_DEFINITIONS = {
  ...FIXED_REFERENCE_EVENTS,
  ...EASTER_REFERENCE_EVENTS
};

const REFERENCE_EVENTS = Object.entries(REFERENCE_EVENT_DEFINITIONS).map(([key, value]) => ({
  key,
  label: value.label
}));

function startOfDay(date) {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

function addDays(date, days) {
  const copy = new Date(date.getTime());
  copy.setDate(copy.getDate() + days);
  return copy;
}

function addMonths(date, months) {
  const copy = new Date(date.getTime());
  copy.setMonth(copy.getMonth() + months);
  return copy;
}

function applyOffset(date, unit, value = 0) {
  const copy = new Date(date.getTime());
  const numericValue = Number(value) || 0;
  if (unit === 'weeks') {
    copy.setDate(copy.getDate() + numericValue * 7);
    return copy;
  }
  if (unit === 'months') {
    copy.setMonth(copy.getMonth() + numericValue);
    return copy;
  }
  copy.setDate(copy.getDate() + numericValue);
  return copy;
}

function subtractOffset(date, unit, value = 0) {
  return applyOffset(date, unit, -(Number(value) || 0));
}

function getIntervalMonths(interval) {
  if (interval === 'monthly') {
    return 1;
  }
  if (interval === 'quarterly') {
    return 3;
  }
  return 12;
}

function getEasterSunday(year) {
  const a = year % 19;
  const b = Math.floor(year / 100);
  const c = year % 100;
  const d = Math.floor(b / 4);
  const e = b % 4;
  const f = Math.floor((b + 8) / 25);
  const g = Math.floor((b - f + 1) / 3);
  const h = (19 * a + b - d - g + 15) % 30;
  const i = Math.floor(c / 4);
  const k = c % 4;
  const l = (32 + 2 * e + 2 * i - h - k) % 7;
  const m = Math.floor((a + 11 * h + 22 * l) / 451);
  const month = Math.floor((h + l - 7 * m + 114) / 31);
  const day = ((h + l - 7 * m + 114) % 31) + 1;
  return new Date(year, month - 1, day);
}

function getReferenceEventDate(eventKey, year) {
  if (!eventKey || !Number.isFinite(year)) {
    return null;
  }
  const fixedEvent = FIXED_REFERENCE_EVENTS[eventKey];
  if (fixedEvent) {
    return new Date(year, fixedEvent.month, fixedEvent.day);
  }
  const easterEvent = EASTER_REFERENCE_EVENTS[eventKey];
  if (easterEvent) {
    return addDays(getEasterSunday(year), easterEvent.offsetDays);
  }
  return null;
}

function getNextOccurrence(baseDate, intervalMonths, todayStart) {
  if (!baseDate || Number.isNaN(baseDate.getTime())) {
    return null;
  }
  let candidate = startOfDay(baseDate);
  const guardYear = todayStart.getFullYear() + 200;
  while (candidate < todayStart && candidate.getFullYear() < guardYear) {
    candidate = startOfDay(addMonths(candidate, intervalMonths));
  }
  return candidate;
}

function getReminderReference(reminder = {}) {
  const reference = reminder?.reference;
  if (!reference || reference.mode !== 'event') {
    return { mode: 'pickupDate' };
  }
  const unit = ['days', 'weeks', 'months'].includes(reference.occurrenceOffsetUnit)
    ? reference.occurrenceOffsetUnit
    : 'days';
  const value = Number.isFinite(Number(reference.occurrenceOffsetValue))
    ? Number(reference.occurrenceOffsetValue)
    : 0;
  if (!REFERENCE_EVENT_DEFINITIONS[reference.eventKey]) {
    return { mode: 'pickupDate' };
  }
  return {
    mode: 'event',
    eventKey: reference.eventKey,
    occurrenceOffsetUnit: unit,
    occurrenceOffsetValue: value
  };
}

function normalizeReminderReference(reference = {}) {
  return getReminderReference({ reference });
}

function computeReminderSchedule(entry, today = new Date()) {
  if (!entry?.reminder?.enabled) {
    return null;
  }
  const todayStart = startOfDay(today);
  const reminder = entry.reminder || {};
  const beforeUnit = ['days', 'weeks', 'months'].includes(reminder.beforeUnit) ? reminder.beforeUnit : 'days';
  const beforeValue = Number.isFinite(Number(reminder.beforeValue))
    ? Math.max(0, Number(reminder.beforeValue))
    : Number.isFinite(Number(reminder.daysBefore))
      ? Math.max(0, Number(reminder.daysBefore))
      : 6;

  const reference = getReminderReference(reminder);
  if (reference.mode === 'event') {
    for (let year = todayStart.getFullYear(); year <= todayStart.getFullYear() + 2; year += 1) {
      const eventDate = getReferenceEventDate(reference.eventKey, year);
      if (!eventDate) {
        return null;
      }
      const occurrence = startOfDay(
        applyOffset(eventDate, reference.occurrenceOffsetUnit, reference.occurrenceOffsetValue)
      );
      const reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue));
      if (reminderDate >= todayStart) {
        return { occurrence, reminderDate };
      }
    }
    return null;
  }

  if (!entry.pickupDate) {
    return null;
  }
  const baseDate = new Date(`${entry.pickupDate}T00:00:00`);
  if (Number.isNaN(baseDate.getTime())) {
    return null;
  }
  const intervalMonths = getIntervalMonths(reminder.interval);
  let occurrence = getNextOccurrence(baseDate, intervalMonths, todayStart);
  if (!occurrence) {
    return null;
  }
  let reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue));
  if (reminderDate < todayStart) {
    occurrence = startOfDay(addMonths(occurrence, intervalMonths));
    reminderDate = startOfDay(subtractOffset(occurrence, beforeUnit, beforeValue));
  }
  return { occurrence, reminderDate };
}

module.exports = {
  REFERENCE_EVENTS,
  REFERENCE_EVENT_DEFINITIONS,
  getReminderReference,
  getReminderSchedule: computeReminderSchedule,
  computeReminderSchedule,
  normalizeReminderReference
};
