const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];

const cloneDate = (value = new Date()) => new Date(value instanceof Date ? value.getTime() : new Date(value).getTime());

const parseTime = (value = '09:00') => {
  const match = String(value).trim().match(/^(\d{1,2}):(\d{2})$/);
  if (!match) return { hour: 9, minute: 0 };
  return { hour: Math.min(23, Number(match[1])), minute: Math.min(59, Number(match[2])) };
};

const withTime = (date, { hour, minute }) => {
  const next = cloneDate(date);
  next.setHours(hour, minute, 0, 0);
  return next;
};

const addDays = (date, days) => {
  const next = cloneDate(date);
  next.setDate(next.getDate() + days);
  return next;
};

const addMonths = (date, months) => {
  const next = cloneDate(date);
  next.setMonth(next.getMonth() + months);
  return next;
};

const matchesCronPart = (value, part) => {
  if (part === '*') return true;
  if (/^\*\/\d+$/.test(part)) {
    const step = Number(part.split('/')[1] || 1);
    return value % step === 0;
  }
  if (/^\d+$/.test(part)) return Number(part) === value;
  if (part.includes(',')) return part.split(',').some((item) => matchesCronPart(value, item.trim()));
  if (part.includes('-')) {
    const [start, end] = part.split('-').map(Number);
    return value >= start && value <= end;
  }
  return false;
};

const nextCronDate = ({ expression, from = new Date() }) => {
  const parts = String(expression || '').trim().split(/\s+/);
  if (parts.length !== 5) return null;
  let cursor = cloneDate(from);
  cursor.setSeconds(0, 0);
  cursor = new Date(cursor.getTime() + 60000);
  const limit = new Date(cursor.getTime() + 370 * 24 * 60 * 60000);
  while (cursor <= limit) {
    const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
    if (
      matchesCronPart(cursor.getMinutes(), minute) &&
      matchesCronPart(cursor.getHours(), hour) &&
      matchesCronPart(cursor.getDate(), dayOfMonth) &&
      matchesCronPart(cursor.getMonth() + 1, month) &&
      matchesCronPart(cursor.getDay(), dayOfWeek)
    ) {
      return cursor;
    }
    cursor = new Date(cursor.getTime() + 60000);
  }
  return null;
};

const computeNextRunAt = ({ expression, from = new Date() }) => {
  const normalized = String(expression || '').trim();
  if (!normalized) return null;
  const lower = normalized.toLowerCase();

  if (/^\d{4}-\d{2}-\d{2}t/.test(lower) || /^\d{4}-\d{2}-\d{2}$/.test(lower)) {
    const date = new Date(normalized);
    return Number.isNaN(date.getTime()) ? null : date;
  }

  if (lower.startsWith('daily@')) {
    const time = parseTime(lower.split('@')[1]);
    let next = withTime(from, time);
    if (next <= from) next = addDays(next, 1);
    return next;
  }

  if (lower.startsWith('weekly:')) {
    const [dayToken, timeToken = '09:00'] = lower.replace('weekly:', '').split('@');
    const targetDay = WEEKDAYS.indexOf(dayToken.slice(0, 3));
    if (targetDay < 0) return null;
    const time = parseTime(timeToken);
    let next = withTime(from, time);
    while (next.getDay() !== targetDay || next <= from) next = addDays(next, 1);
    return next;
  }

  if (lower.startsWith('monthly:')) {
    const [dayValue, timeToken = '09:00'] = lower.replace('monthly:', '').split('@');
    const targetDay = Math.min(28, Math.max(1, Number(dayValue || 1)));
    const time = parseTime(timeToken);
    let next = withTime(from, time);
    next.setDate(targetDay);
    if (next <= from) {
      next = addMonths(next, 1);
      next.setDate(targetDay);
    }
    return next;
  }

  return nextCronDate({ expression: normalized, from });
};

const validateScheduleExpression = ({ expression }) => {
  const nextRunAt = computeNextRunAt({ expression, from: new Date() });
  return { valid: Boolean(nextRunAt), nextRunAt };
};

module.exports = {
  computeNextRunAt,
  validateScheduleExpression,
};
