// Schedule spec parser. Supports a small set of human strings.
//
//   "on demand" / "manual" / "live" / "event based" / ""
//     → null (never scheduled)
//
//   "daily HH:MM"
//     → next occurrence today/tomorrow at HH:MM local time
//
//   "weekly <day> HH:MM"
//     → next occurrence on that weekday at HH:MM local time.
//       <day> ∈ {sun,mon,tue,wed,thu,fri,sat} (full names also accepted).
//
//   "monthly <dom|last> HH:MM"
//     → next occurrence on that day-of-month at HH:MM.
//       Use "last" for the last day of each month. Numeric days above the
//       month length clamp to the last day (e.g. "monthly 31" → Feb 28).
//
//   "every Nm"   → from now + N minutes
//   "every Nh"   → from now + N hours
//   "every Nmo"  → from now + N months
//
//   "at <ISO>"
//     → one-time run at the given ISO datetime. Returns null once the time
//       has passed (the daemon fires it from the stored next_run_at, then
//       this parser returns null on the next tick so it never re-queues).
//
//   anything else → from now + 1 hour as a soft default.
//
// Returns ISO datetime string, or null if the spec means "no scheduled run".

const NON_SCHEDULED = new Set(['', 'on demand', 'live', 'event based', 'manual']);

const DAY_NAMES = {
  sun: 0, sunday: 0,
  mon: 1, monday: 1,
  tue: 2, tues: 2, tuesday: 2,
  wed: 3, wednesday: 3,
  thu: 4, thurs: 4, thursday: 4,
  fri: 5, friday: 5,
  sat: 6, saturday: 6,
};

const DAY_PATTERN = Object.keys(DAY_NAMES).join('|');

export function computeNextRunAt(spec, fromIso = new Date().toISOString()) {
  const s = String(spec || '').trim().toLowerCase();
  if (NON_SCHEDULED.has(s)) return null;
  const from = new Date(fromIso);
  if (Number.isNaN(from.getTime())) return null;

  // One-time: "at <ISO>"
  const once = s.match(/^at\s+(.+)$/);
  if (once) {
    const target = new Date(once[1]);
    if (Number.isNaN(target.getTime())) return null;
    if (target.getTime() <= from.getTime()) return null;
    return target.toISOString();
  }

  // Daily: "daily HH:MM"
  const daily = s.match(/^daily\s+(\d{1,2}):(\d{2})$/);
  if (daily) {
    const h = Math.min(23, Math.max(0, Number(daily[1])));
    const m = Math.min(59, Math.max(0, Number(daily[2])));
    const next = new Date(from);
    next.setSeconds(0, 0);
    next.setHours(h, m, 0, 0);
    if (next.getTime() <= from.getTime()) next.setDate(next.getDate() + 1);
    return next.toISOString();
  }

  // Weekly: "weekly <day> HH:MM"
  const weekly = s.match(new RegExp(`^weekly\\s+(${DAY_PATTERN})\\s+(\\d{1,2}):(\\d{2})$`));
  if (weekly) {
    const targetDay = DAY_NAMES[weekly[1]];
    const h = Math.min(23, Math.max(0, Number(weekly[2])));
    const m = Math.min(59, Math.max(0, Number(weekly[3])));
    const next = new Date(from);
    next.setSeconds(0, 0);
    next.setHours(h, m, 0, 0);
    let daysAhead = (targetDay - next.getDay() + 7) % 7;
    if (daysAhead === 0 && next.getTime() <= from.getTime()) daysAhead = 7;
    next.setDate(next.getDate() + daysAhead);
    return next.toISOString();
  }

  // Monthly: "monthly <dom|last> HH:MM"
  const monthly = s.match(/^monthly\s+(last|\d{1,2})\s+(\d{1,2}):(\d{2})$/);
  if (monthly) {
    const domSpec = monthly[1];
    const h = Math.min(23, Math.max(0, Number(monthly[2])));
    const m = Math.min(59, Math.max(0, Number(monthly[3])));
    const buildForMonth = (year, monthIdx) => {
      const lastDay = new Date(year, monthIdx + 1, 0).getDate();
      const dom = domSpec === 'last' ? lastDay : Math.min(Math.max(1, Number(domSpec)), lastDay);
      return new Date(year, monthIdx, dom, h, m, 0, 0);
    };
    let candidate = buildForMonth(from.getFullYear(), from.getMonth());
    if (candidate.getTime() <= from.getTime()) {
      candidate = buildForMonth(from.getFullYear(), from.getMonth() + 1);
    }
    return candidate.toISOString();
  }

  // Every N months: "every N mo" or "every Nmo"
  const everyMonths = s.match(/^every\s+(\d{1,3})\s*mo$/);
  if (everyMonths) {
    const n = Math.max(1, Number(everyMonths[1]));
    const next = new Date(from);
    next.setMonth(next.getMonth() + n);
    return next.toISOString();
  }

  const everyHours = s.match(/^every\s+(\d{1,3})\s*h$/);
  if (everyHours) {
    return new Date(from.getTime() + Number(everyHours[1]) * 3600_000).toISOString();
  }

  const everyMins = s.match(/^every\s+(\d{1,4})\s*m$/);
  if (everyMins) {
    return new Date(from.getTime() + Number(everyMins[1]) * 60_000).toISOString();
  }

  return new Date(from.getTime() + 3600_000).toISOString();
}

export function isDue(nextRunAtIso, nowIso = new Date().toISOString()) {
  if (!nextRunAtIso) return false;
  return new Date(nextRunAtIso).getTime() <= new Date(nowIso).getTime();
}
