(function (root, factory) {
  const api = factory();
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
  else root.createOfflineToday = api;
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const itemKey = item => [item?.kind, item?.repository, Number(item?.number || 0)].join(':');

  function createOfflineToday({
    loadDetail, loadSavedDetail, saveDetail, onStatus = () => {}, concurrency = 2, maxItems = 5,
  }) {
    let failed = new Set();
    let generation = 0;

    function bounded(items) {
      return (Array.isArray(items) ? items : []).filter(item =>
        ['issue', 'pull'].includes(item?.kind) && item?.repository && Number(item?.number) > 0
      ).slice(0, Math.max(1, maxItems));
    }

    async function run(login, sourceItems, onlyFailed) {
      login = String(login || '').trim();
      const items = bounded(sourceItems);
      const runGeneration = ++generation;
      if (!login || !items.length) {
        failed = new Set();
        const empty = { total: items.length, ready: 0, failed: 0, pending: 0 };
        onStatus(empty);
        return empty;
      }

      const previousFailed = failed;
      const savedDetails = new Map();
      await Promise.all(items.map(async item => {
        savedDetails.set(itemKey(item), await loadSavedDetail(login, item));
      }));
      const readyKeys = new Set(items.filter(item => savedDetails.get(itemKey(item))).map(itemKey));
      const snapshot = pending => ({
        total: items.length, ready: readyKeys.size, failed: failed.size, pending,
      });
      const candidates = items.filter(item => {
        const key = itemKey(item);
        if (onlyFailed && !previousFailed.has(key)) return false;
        const saved = savedDetails.get(key);
        return onlyFailed || !saved || saved.source_updated_at !== item.updated_at;
      });
      failed = new Set();
      onStatus(snapshot(candidates.length));
      let cursor = 0;

      async function worker() {
        while (cursor < candidates.length && runGeneration === generation) {
          const item = candidates[cursor++];
          try {
            const detail = await loadDetail(item);
            if (runGeneration !== generation) return;
            const saved = await saveDetail(login, item, { ...detail, source_updated_at: item.updated_at });
            if (saved === false) throw new Error('Offline detail was not durably admitted.');
            readyKeys.add(itemKey(item));
          } catch (_error) {
            if (runGeneration === generation) failed.add(itemKey(item));
          }
          if (runGeneration === generation) {
            onStatus(snapshot(Math.max(0, candidates.length - cursor)));
          }
        }
      }

      await Promise.all(Array.from(
        { length: Math.min(Math.max(1, concurrency), candidates.length) }, worker
      ));
      if (runGeneration !== generation) return snapshot(0);
      const status = snapshot(0);
      onStatus(status);
      return status;
    }

    return {
      warm: (login, items) => run(login, items, false),
      retry: (login, items) => run(login, items, true),
      cancel() { generation += 1; failed = new Set(); },
      failedKeys: () => Array.from(failed),
    };
  }

  return createOfflineToday;
});
