function createBatchFindWork({
  capacity,
  claim,
  queue,
  timeBudget = () => ({ capacity_minutes: null, planned_minutes: 0 }),
  persistEstimate = () => {},
  onProgress = () => {},
  storage = typeof localStorage === 'undefined' ? null : localStorage,
  owner = () => '',
  journalName = 'find-work-batch',
  queueFailureReason = 'assigned but Today sync is unavailable',
  autoMount = true,
}) {
  let request = null;
  const journalKey = () => 'stackchain.' + journalName + '.v1.' +
    encodeURIComponent(String(owner() || ''));

  function readJournal() {
    if (!storage || !String(owner() || '')) return null;
    try {
      const value = JSON.parse(storage.getItem(journalKey()) || 'null');
      return value?.owner === String(owner()) && Array.isArray(value.items) ? value : null;
    } catch (_error) { return null; }
  }

  function writeJournal(value) {
    if (!storage || !String(owner() || '')) return;
    storage.setItem(journalKey(), JSON.stringify(value));
  }

  function clearJournal() {
    if (storage && String(owner() || '')) storage.removeItem(journalKey());
  }

  function result(status, selected, available, queued = [], failed = []) {
    return { status, selected, available, queued, failed };
  }

  function key(item) {
    return String(item.repository || '') + '#' + String(item.number || '');
  }

  async function processJournal(journal) {
    const selected = journal.items;
    const queued = [];
    const failed = [];
    for (let index = 0; index < selected.length; index += 1) {
      const entry = selected[index];
      const item = entry.item;
      if (entry.state === 'queued') {
        queued.push(key(item));
        onProgress({ status: 'running', processed: index + 1, selected: selected.length });
        continue;
      }
      try {
        const confirmed = entry.confirmed || await claim(item);
        if (!entry.confirmed) {
          entry.confirmed = confirmed;
          entry.state = 'assigned';
          writeJournal(journal);
        }
        const queueResult = await queue(confirmed, journal.context);
        if (queueResult === 'queued' || queueResult === 'exists') {
          queued.push(key(item));
          entry.state = 'queued';
          if (Number.isInteger(entry.estimate) && entry.estimate > 0) {
            persistEstimate(confirmed, entry.estimate);
          }
          writeJournal(journal);
        } else {
          failed.push({ key: key(item), reason: queueFailureReason, assigned: true });
        }
      } catch (error) {
        failed.push({ key: key(item), reason: error?.message || 'assignment failed' });
      }
      onProgress({ status: 'running', processed: index + 1, selected: selected.length });
    }
    const outcome = result('complete', selected.length, journal.available, queued, failed);
    if (!failed.length) clearJournal();
    else writeJournal(journal);
    onProgress({ ...outcome, processed: selected.length });
    return outcome;
  }

  function run(items, estimates = {}, context = null) {
    if (request) return request;
    const selected = Array.isArray(items) ? items.slice() : [];
    const available = Math.max(0, Number(capacity()) || 0);
    if (selected.length > available) {
      const outcome = result('full', selected.length, available);
      onProgress(outcome);
      return Promise.resolve(outcome);
    }
    const budget = timeBudget() || {};
    if (Number.isInteger(budget.capacity_minutes) && budget.capacity_minutes > 0) {
      const remaining = Math.max(0, budget.capacity_minutes - (Number(budget.planned_minutes) || 0));
      const invalid = selected.map(key).filter(id =>
        !Number.isInteger(estimates[id]) || estimates[id] <= 0
      );
      const requested = selected.reduce((sum, item) => sum +
        (Number.isInteger(estimates[key(item)]) && estimates[key(item)] > 0 ? estimates[key(item)] : 0), 0);
      if (invalid.length) {
        const outcome = { ...result('estimates-required', selected.length, available),
          remaining_minutes: remaining, requested_minutes: requested, invalid };
        onProgress(outcome);
        return Promise.resolve(outcome);
      }
      if (requested > remaining) {
        const outcome = { ...result('over-budget', selected.length, available),
          remaining_minutes: remaining, requested_minutes: requested, over_minutes: requested - remaining };
        onProgress(outcome);
        return Promise.resolve(outcome);
      }
    }
    const journal = {
      owner: String(owner() || ''), available, context,
      items: selected.map(item => ({ item, estimate: estimates[key(item)] || null, state: 'pending' })),
    };
    writeJournal(journal);
    request = processJournal(journal).finally(() => { request = null; });
    return request;
  }

  function resume() {
    if (request) return request;
    const journal = readJournal();
    if (!journal) return Promise.resolve(null);
    request = processJournal(journal).finally(() => { request = null; });
    return request;
  }

  function pending() {
    const journal = readJournal();
    return journal ? journal.items.filter(item => item.state !== 'queued').length : 0;
  }

  function mountRecovery(button, opener) {
    const show = () => {
      const journal = readJournal();
      button.hidden = !journal;
      if (journal) button.textContent = 'Resume ' + journal.items.filter(item => item.state !== 'queued').length + ' interrupted';
    };
    button.addEventListener('click', async () => {
      button.disabled = true;
      const outcome = await resume();
      button.disabled = false;
      show();
      if (outcome) {
        button.previousElementSibling.textContent = outcome.failed.length ?
          outcome.failed.length + ' still need recovery.' : outcome.queued.length + ' queued · batch recovered.';
      }
    });
    opener.addEventListener('click', show);
  }

  if (autoMount && typeof document !== 'undefined') mountRecovery(
    document.getElementById('resume-find-work-batch'), document.getElementById('find-work')
  );
  return { run, resume, pending, mountRecovery };
}

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