(function (root, factory) {
  const createSearchBatchPlan = factory();
  if (typeof module === 'object' && module.exports) module.exports = createSearchBatchPlan;
  if (root) {
    root.createSearchBatchPlan = createSearchBatchPlan;
    root.mountSearchBatchPlanning = createSearchBatchPlan.mount;
  }
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  function createSearchBatchPlan({
    limit = 50,
    resolve = item => Promise.resolve(item),
    claim = item => Promise.resolve(item),
    onChange = () => {},
  } = {}) {
    const maximum = Number.isInteger(limit) && limit > 0 ? limit : 50;
    const selected = new Map();
    let active = false;

    const identity = item => [item?.kind || '', item?.repository || '', item?.number || ''].join(':');
    const eligible = item => item?.kind === 'issue' && item?.state === 'open' &&
      Boolean(item.repository) && Number.isInteger(item.number);
    function snapshot() {
      return { active, count:selected.size, items:Array.from(selected.values()) };
    }
    function changed() {
      const state = snapshot();
      onChange(state);
      return state;
    }
    function start() {
      active = true;
      selected.clear();
      return changed();
    }
    function cancel() {
      active = false;
      selected.clear();
      return changed();
    }
    function toggle(item) {
      if (!active) return 'inactive';
      if (!eligible(item)) return 'ineligible';
      const key = identity(item);
      if (selected.has(key)) {
        selected.delete(key);
        changed();
        return 'removed';
      }
      if (selected.size >= maximum) return 'limit';
      selected.set(key, { ...item });
      changed();
      return 'selected';
    }
    async function prepare(item) {
      const detail = await resolve(item);
      if (detail?.assigned_to_me) return detail;
      if (detail?.claimable) return claim(detail);
      throw new Error('This issue is no longer available to assign.');
    }
    function releaseRepository() {
      const repositories = new Set(Array.from(selected.values()).map(item => item.repository));
      return repositories.size === 1 ? repositories.values().next().value : null;
    }
    return { identity, eligible, start, cancel, toggle, snapshot, prepare, releaseRepository, limit:maximum };
  }

  createSearchBatchPlan.mount = function mountSearchBatchPlanning(
    document, batchFactory, todayWork, getOwner, fetchJson, previewPath, queueToday,
    todaySync, acceptIssue, lookup, render, escapeHtml, escapeAttribute,
    laterWork = null, laterPicker = null
  ) {
    const get = selector => document.querySelector(selector);
    let processor;
    let laterProcessor;
    let releaseProcessor;
    let releaseContext = null;
    const plan = createSearchBatchPlan({
      resolve:item => fetchJson(previewPath(item), {headers:{Accept:'application/json'}}),
      claim:detail => fetchJson(
        'api/v1/repos/' + detail.repository.split('/').map(encodeURIComponent).join('/') +
        '/issues/' + encodeURIComponent(detail.number) + '/claim',
        {method:'PATCH', headers:{Accept:'application/json'}}
      ),
      onChange:state => {
        get('#search-batch-actions').hidden = !state.active;
        get('#select-search-results').hidden = state.active;
        get('#queue-selected-search-results').disabled = state.count === 0;
        get('#defer-selected-search-results').disabled = state.count === 0;
        get('#plan-selected-search-results').disabled = state.count === 0;
        get('#search-selection-status').textContent = state.count ?
          state.count + ' issue' + (state.count === 1 ? '' : 's') + ' selected.' : 'No issues selected.';
        render();
      },
    });
    processor = batchFactory({
      capacity:() => Math.max(0, todayWork.limit - todayWork.read().length),
      owner:getOwner,
      timeBudget:() => {
        const planning = todayWork.planning();
        return {
          capacity_minutes:planning.capacity_minutes,
          planned_minutes:Object.values(planning.estimates || {}).reduce((sum, minutes) => sum + minutes, 0),
        };
      },
      journalName:'search-today-batch',
      autoMount:false,
      claim:item => plan.prepare(item),
      queue:confirmed => queueToday(acceptIssue(confirmed)),
      persistEstimate:(confirmed, minutes) => {
        const planning = todayWork.planning();
        planning.estimates[todayWork.identity(confirmed)] = minutes;
        todayWork.replacePlanning(planning);
        todaySync.enqueueConfiguration(planning.capacity_minutes, planning.estimates);
        todaySync.flush();
      },
      onProgress:progress => {
        get('#queue-selected-search-results').disabled = progress.status === 'running';
        if (progress.status === 'running') {
          get('#search-selection-status').textContent = 'Planning ' + progress.processed + ' of ' + progress.selected + '…';
        } else if (progress.status === 'full') {
          get('#search-selection-status').textContent = 'Today has ' + progress.available + ' remaining slot' +
            (progress.available === 1 ? '.' : 's.');
        } else if (progress.status === 'estimates-required') {
          get('#search-batch-estimate-summary').textContent = 'Estimate every selected issue.';
        } else if (progress.status === 'over-budget') {
          get('#search-batch-estimate-summary').textContent = progress.requested_minutes +
            ' min selected · ' + progress.remaining_minutes + ' min remain.';
        } else if (progress.status === 'complete') {
          get('#cmd-search-action-status').textContent = progress.failed.length ?
            progress.queued.length + ' queued · ' + progress.failed.length + ' need retry.' :
            progress.queued.length + ' added to Today.';
          get('#resume-search-batch').hidden = processor.pending() === 0;
          closeEstimateReview();
          if (!progress.failed.length) plan.cancel();
        }
      },
    });
    laterProcessor = batchFactory({
      capacity:() => plan.limit,
      owner:getOwner,
      journalName:'search-later-batch',
      queueFailureReason:'assigned but Later could not be saved',
      autoMount:false,
      claim:item => plan.prepare(item),
      queue:(confirmed, context) => {
        const outcome = laterWork?.defer(acceptIssue(confirmed), context?.until);
        return outcome === 'deferred' ? 'queued' : outcome;
      },
      onProgress:progress => {
        get('#defer-selected-search-results').disabled = progress.status === 'running';
        if (progress.status === 'running') {
          get('#search-selection-status').textContent = 'Deferring ' + progress.processed + ' of ' + progress.selected + '…';
        } else if (progress.status === 'complete') {
          get('#cmd-search-action-status').textContent = progress.failed.length ?
            progress.queued.length + ' deferred · ' + progress.failed.length + ' need retry.' :
            progress.queued.length + ' added to Later.';
          restore();
          if (!progress.failed.length) plan.cancel();
        }
      },
    });
    releaseProcessor = batchFactory({
      capacity:() => plan.limit,
      owner:getOwner,
      journalName:'search-release-batch',
      queueFailureReason:'assigned but release planning could not be confirmed',
      autoMount:false,
      claim:item => plan.prepare(item),
      queue:async (confirmed, context) => {
        const issuePath = 'api/v1/repos/' + confirmed.repository.split('/').map(encodeURIComponent).join('/') +
          '/issues/' + encodeURIComponent(confirmed.number);
        const releasePlan = await fetchJson(issuePath + '/release-plan', {
          method:'PATCH', headers:{Accept:'application/json','Content-Type':'application/json'},
          body:JSON.stringify({milestone_id:context.milestone_id, due_date:context.due_date || null}),
        });
        if (releasePlan?.number !== confirmed.number ||
            releasePlan?.milestone?.id !== context.milestone_id ||
            (context.due_date && releasePlan?.due_date !== context.due_date)) {
          throw new Error('Issue release plan was not confirmed.');
        }
        return 'queued';
      },
      onProgress:progress => {
        get('#confirm-search-release').disabled = progress.status === 'running';
        if (progress.status === 'running') {
          get('#search-release-summary').textContent = 'Planning ' + progress.processed + ' of ' + progress.selected + '…';
        } else if (progress.status === 'complete') {
          get('#cmd-search-action-status').textContent = progress.failed.length ?
            progress.queued.length + ' planned · ' + progress.failed.length + ' need retry.' :
            progress.queued.length + ' planned for ' + (releaseContext?.milestone_title || 'release') + '.';
          get('#resume-search-batch').hidden = releaseProcessor.pending() === 0;
          closeReleaseReview();
          if (!progress.failed.length) plan.cancel();
        }
      },
    });
    function estimateValues() {
      return Object.fromEntries(Array.from(document.querySelectorAll('[data-search-batch-estimate]')).map(input =>
        [input.dataset.searchBatchEstimate, Number(input.value)]
      ));
    }
    function closeEstimateReview() {
      get('#search-batch-estimate-review').hidden = true;
      get('#search-batch-actions').hidden = !plan.snapshot().active;
    }
    function openEstimateReview(items) {
      const planning = todayWork.planning();
      const planned = Object.values(planning.estimates || {}).reduce((sum, minutes) => sum + minutes, 0);
      const remaining = Math.max(0, planning.capacity_minutes - planned);
      get('#search-batch-estimate-summary').textContent = remaining +
        ' min remain. Estimate before assigning.';
      get('#search-batch-estimate-list').innerHTML = items.map(item => {
        const id = String(item.repository || '') + '#' + String(item.number || '');
        const estimate = planning.estimates?.[id] || '';
        return '<div class="search-batch-estimate-row"><div><span class="small">' + escapeHtml(id) +
          '</span><strong>' + escapeHtml(item.title || 'Untitled work') + '</strong></div><label class="small">' +
          '<input type="number" inputmode="numeric" min="5" max="1440" step="5" value="' +
          escapeAttribute(estimate) + '" data-search-batch-estimate="' + escapeAttribute(id) +
          '" aria-label="Estimate for ' + escapeAttribute(item.title || id) + ' in minutes" /> min</label></div>';
      }).join('');
      get('#search-batch-actions').hidden = true;
      get('#search-batch-estimate-review').hidden = false;
      document.querySelector('[data-search-batch-estimate]')?.focus();
    }
    function closeReleaseReview() {
      get('#search-release-review').hidden = true;
      get('#search-batch-actions').hidden = !plan.snapshot().active;
    }
    async function openReleaseReview() {
      const items = plan.snapshot().items;
      const repository = plan.releaseRepository();
      if (!items.length) return;
      if (!repository) {
        get('#search-selection-status').textContent = 'Select issues from one repository to plan a release.';
        return;
      }
      get('#plan-selected-search-results').disabled = true;
      get('#search-selection-status').textContent = 'Loading release milestones…';
      try {
        const milestones = await fetchJson(
          'api/v1/repos/' + repository.split('/').map(encodeURIComponent).join('/') + '/milestones',
          {headers:{Accept:'application/json'}}
        );
        const open = (Array.isArray(milestones) ? milestones : []).filter(item => item?.state !== 'closed');
        get('#search-release-milestone').innerHTML = '<option value="">Choose milestone</option>' + open.map(item =>
          '<option value="' + Number(item.id) + '">' + escapeHtml(item.title) + '</option>'
        ).join('');
        get('#search-release-list').innerHTML = items.map(item => '<div class="search-release-row"><span>' +
          escapeHtml(item.repository + ' #' + item.number) + '</span><strong>' +
          escapeHtml(item.title || 'Untitled work') + '</strong></div>').join('');
        get('#search-release-summary').textContent = items.length + ' issues in ' + repository + '. Assignment starts only after confirmation.';
        get('#search-batch-actions').hidden = true;
        get('#search-release-review').hidden = false;
        get('#search-release-milestone').focus();
      } catch (error) {
        get('#search-selection-status').textContent = error?.message || 'Milestones could not be loaded. Try again.';
      } finally {
        get('#plan-selected-search-results').disabled = false;
      }
    }
    get('#select-search-results').addEventListener('click', () => plan.start());
    get('#cancel-search-selection').addEventListener('click', () => plan.cancel());
    get('#queue-selected-search-results').addEventListener('click', () => {
      const items = plan.snapshot().items;
      const capacity = todayWork.planning().capacity_minutes;
      if (Number.isInteger(capacity) && capacity > 0) openEstimateReview(items);
      else processor.run(items);
    });
    get('#defer-selected-search-results').addEventListener('click', event => {
      const items = plan.snapshot().items;
      if (!items.length) return;
      laterPicker?.open({ items, confirm:until => () => laterProcessor.run(
        items, {}, { until:new Date(until).toISOString() }
      ) }, event.currentTarget, 'search-batch');
    });
    get('#plan-selected-search-results').addEventListener('click', openReleaseReview);
    get('#cancel-search-release').addEventListener('click', closeReleaseReview);
    get('#confirm-search-release').addEventListener('click', () => {
      const milestoneSelect = get('#search-release-milestone');
      const milestoneId = Number(milestoneSelect.value);
      if (!Number.isInteger(milestoneId) || milestoneId < 1) {
        get('#search-release-summary').textContent = 'Choose an open milestone.';
        return;
      }
      const selectedOption = milestoneSelect.options?.[milestoneSelect.selectedIndex];
      const milestoneTitle = selectedOption?.textContent || selectedOption?.text || 'release';
      const dueDay = get('#search-release-due-date').value;
      releaseContext = {
        milestone_id:milestoneId, milestone_title:milestoneTitle,
        due_date:dueDay ? dueDay + 'T23:59:59Z' : null,
      };
      releaseProcessor.run(plan.snapshot().items, {}, releaseContext);
    });
    get('#cancel-search-batch-estimates').addEventListener('click', closeEstimateReview);
    get('#confirm-search-batch-estimates').addEventListener('click', () =>
      processor.run(plan.snapshot().items, estimateValues())
    );
    get('#resume-search-batch').addEventListener('click', () => {
      if (processor.pending()) return processor.resume();
      if (laterProcessor.pending()) return laterProcessor.resume();
      return releaseProcessor.resume();
    });
    get('#open-palette').addEventListener('click', () => restore());
    get('#cmd-results').addEventListener('change', event => {
      const index = event.target?.dataset?.searchSelect;
      if (index === undefined) return;
      const result = lookup(Number(index));
      if (result) plan.toggle(result);
    });
    function restore() {
      const pending = processor.pending() + laterProcessor.pending() + releaseProcessor.pending();
      get('#resume-search-batch').hidden = pending === 0;
      get('#resume-search-batch').textContent = 'Resume ' + pending + ' interrupted';
    }
    function resultHtml(result, index) {
      const allowed = result.kind === 'issue' && result.state === 'open';
      const selected = plan.snapshot().items.some(item => plan.identity(item) === plan.identity(result));
      return '<label class="cmd-select-result"' + (allowed ? '' : ' aria-disabled="true"') + '>' +
        '<input type="checkbox" data-search-select="' + index + '" ' + (selected ? 'checked ' : '') +
        (allowed ? '' : 'disabled ') + 'aria-label="Select ' + escapeAttribute(result.title) + '" />' +
        '<span>' + escapeHtml(result.title) + '</span><span class="cmd-meta">' +
        escapeHtml(result.repository) + ' #' + escapeHtml(result.number) + ' · ' +
        (allowed ? 'Open issue' : 'Not eligible') + '</span></label>';
    }
    return { plan, processor, laterProcessor, releaseProcessor, restore, resultHtml };
  };

  return createSearchBatchPlan;
});
