function createTodayRollover(options = {}) {
  const now = options.now || (() => new Date());
  const resolvedTimeZone = options.resolvedTimeZone || (() =>
    Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');

  function timeZone() {
    const candidate = options.timeZone?.() || resolvedTimeZone();
    try {
      new Intl.DateTimeFormat('en', { timeZone: candidate }).format(now());
      return candidate;
    } catch (_error) {
      return 'UTC';
    }
  }

  function localDate() {
    if (options.localDate) return options.localDate();
    const parts = new Intl.DateTimeFormat('en-CA', {
      timeZone: timeZone(), year: 'numeric', month: '2-digit', day: '2-digit',
    }).formatToParts(now());
    const value = Object.fromEntries(parts.map(part => [part.type, part.value]));
    return `${value.year}-${value.month}-${value.day}`;
  }

  function reviewState(plan) {
    if (!Array.isArray(plan?.ids) || !plan.ids.length) return 'empty';
    if (!plan.plan_date) return 'legacy';
    return plan.plan_date === localDate() ? 'current' : 'stale';
  }

  function operation({ operation_id, base_revision, selected_ids, capacity_minutes, estimates = {} }) {
    const ids = [...new Set((selected_ids || []).filter(id => typeof id === 'string' && id))];
    const selected = new Set(ids);
    return {
      operation_id, action: 'rollover', item_id: 'plan', base_revision,
      plan_date: localDate(), timezone: timeZone(), ids, capacity_minutes,
      estimates: Object.fromEntries(Object.entries(estimates).filter(([id]) => selected.has(id))),
    };
  }

  return { localDate, timeZone, reviewState, operation };
}

if (typeof module !== 'undefined' && module.exports) module.exports = createTodayRollover;