function createLaterWork({ storage, getLogin, now = () => new Date(), setTimer = setTimeout, clearTimer = clearTimeout, onWake = () => {}, onChange = () => {}, onExpire = () => {} }) {
  const prefix = 'stackchain.later-work.v1.';
  let timer = null;

  function identity(item) {
    if (!item) return '';
    const kind = item.is_review ? 'review' : (item.kind || 'work');
    const number = Number.isInteger(item.number) ? item.number : '';
    const notification = Number.isInteger(item.notification_id) ? item.notification_id : '';
    return [kind, item.repository || '', number, notification].join(':');
  }

  function storageKey() {
    const login = String(getLogin?.() || '').trim().toLowerCase();
    return login ? prefix + encodeURIComponent(login) : '';
  }

  function read() {
    const key = storageKey();
    if (!key || !storage) return {};
    try {
      const value = JSON.parse(storage.getItem(key) || '{}');
      return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
    } catch (_error) {
      return {};
    }
  }

  function write(records) {
    const key = storageKey();
    if (!key || !storage) return false;
    try {
      if (Object.keys(records).length) storage.setItem(key, JSON.stringify(records));
      else storage.removeItem(key);
      return true;
    } catch (_error) {
      return false;
    }
  }

  function defer(item, until, options = {}) {
    const key = storageKey();
    const id = identity(item);
    const wake = new Date(until);
    if (!key) return 'unavailable';
    if (!id || Number.isNaN(wake.getTime()) || wake <= now()) return 'invalid';
    const records = read();
    const wakeAt = wake.toISOString();
    const handoff = options.handoff === 'today' ? 'today' : null;
    records[id] = handoff ? { wake_at:wakeAt, handoff } : wakeAt;
    if (!write(records)) return 'unavailable';
    onChange('defer', id, wakeAt, handoff);
    return 'deferred';
  }

  function deferMany(items, until) {
    const key = storageKey();
    const wake = new Date(until);
    const entries = (items || []).map(item => [identity(item), item]);
    if (!key) return 'unavailable';
    if (!entries.length || entries.some(([id]) => !id) || Number.isNaN(wake.getTime()) || wake <= now()) {
      return 'invalid';
    }
    const records = read();
    const wakeAt = wake.toISOString();
    entries.forEach(([id]) => { records[id] = wakeAt; });
    if (!write(records)) return 'unavailable';
    entries.forEach(([id]) => onChange('defer', id, wakeAt));
    return 'deferred';
  }

  function presetUntil(preset) {
    const current = new Date(now());
    if (preset === 'today') return new Date(current.getTime() + 4 * 60 * 60 * 1000);
    if (preset === 'tomorrow') {
      const wake = new Date(current);
      wake.setDate(wake.getDate() + 1);
      wake.setHours(9, 0, 0, 0);
      return wake;
    }
    return new Date(NaN);
  }

  function restore(item) {
    const id = identity(item);
    const records = read();
    if (!id || !Object.prototype.hasOwnProperty.call(records, id)) return false;
    delete records[id];
    if (!write(records)) return false;
    onChange('restore', id, null);
    return true;
  }

  function adopt(records) {
    if (!records || typeof records !== 'object' || Array.isArray(records)) return false;
    const normalized = {};
    Object.entries(records).forEach(([id, record]) => {
      const wake = record && typeof record === 'object' ? record.wake_at : record;
      const wakeTime = new Date(wake).getTime();
      if (id && Number.isFinite(wakeTime)) {
        const wakeAt = new Date(wakeTime).toISOString();
        normalized[id] = record?.handoff === 'today' ? { wake_at:wakeAt, handoff:'today' } : wakeAt;
      }
    });
    return write(normalized);
  }

  function schedule(wakeTimes, current) {
    if (timer !== null) clearTimer(timer);
    timer = null;
    if (!wakeTimes.length) return;
    const delay = Math.max(0, Math.min(...wakeTimes) - current);
    timer = setTimer(() => {
      timer = null;
      onWake();
    }, Math.min(delay, 2147483647));
  }

  function partition(items, { pruneMissing = true } = {}) {
    const current = now().getTime();
    const records = read();
    const available = new Map((items || []).map(item => [identity(item), item]));
    const retained = {};
    const wakeTimes = [];
    const expired = [];

    const handoffs = [];
    Object.entries(records).forEach(([id, record]) => {
      const wake = record && typeof record === 'object' ? record.wake_at : record;
      const wakeTime = new Date(wake).getTime();
      if (!Number.isFinite(wakeTime) || wakeTime <= current) {
        if (Number.isFinite(wakeTime)) {
          expired.push(id);
          if (record?.handoff === 'today') handoffs.push(id);
        }
        return;
      }
      if (pruneMissing && !available.has(id)) return;
      const wakeAt = new Date(wakeTime).toISOString();
      retained[id] = record?.handoff === 'today' ? { wake_at:wakeAt, handoff:'today' } : wakeAt;
      wakeTimes.push(wakeTime);
    });

    if (JSON.stringify(retained) !== JSON.stringify(records) && write(retained) && expired.length) {
      onExpire(expired, handoffs);
    }
    schedule(wakeTimes, current);
    const deferredIds = new Set(Object.keys(retained));
    const active = (items || []).filter(item => !deferredIds.has(identity(item)));
    const later = (items || []).flatMap(item => {
      const record = retained[identity(item)];
      const wake = record && typeof record === 'object' ? record.wake_at : record;
      return wake ? [{ ...item, deferred_until: wake }] : [];
    });
    return { active, later };
  }

  return { identity, read, adopt, defer, deferMany, restore, presetUntil, partition };
}

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