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

  function activeMention(value, caret) {
    const before = String(value || '').slice(0, Math.max(0, caret));
    const match = before.match(/(^|\s)@([A-Za-z0-9_.-]{2,39})$/);
    if (!match) return null;
    const start = before.length - match[2].length - 1;
    return { start, end:before.length, query:match[2] };
  }

  function insertMention(value, mention, login) {
    const text = String(value || '');
    const suffixStart = mention.end < text.length && /\s/.test(text[mention.end]) ? mention.end + 1 : mention.end;
    const inserted = '@' + login + ' ';
    return {
      value:text.slice(0, mention.start) + inserted + text.slice(suffixStart),
      caret:mention.start + inserted.length,
    };
  }

  function create(options) {
    const textarea = options.textarea;
    const listbox = options.listbox;
    const status = options.status;
    const setTimer = options.setTimer || setTimeout;
    const clearTimer = options.clearTimer || clearTimeout;
    const createOption = options.createOption || (() => document.createElement('button'));
    let timer = null;
    let requestId = 0;
    let mention = null;
    let candidates = [];
    let activeIndex = -1;

    function dismiss() {
      requestId += 1;
      candidates = [];
      activeIndex = -1;
      listbox.replaceChildren();
      listbox.hidden = true;
      textarea.setAttribute('aria-expanded', 'false');
      textarea.removeAttribute('aria-activedescendant');
      status.textContent = '';
    }

    function activate(index) {
      if (!candidates.length) return;
      activeIndex = (index + candidates.length) % candidates.length;
      listbox.children.forEach((option, optionIndex) => {
        option.setAttribute('aria-selected', optionIndex === activeIndex ? 'true' : 'false');
      });
      textarea.setAttribute('aria-activedescendant', listbox.children[activeIndex].id);
    }

    function select(index) {
      if (!mention || !candidates[index]) return;
      const result = insertMention(textarea.value, mention, candidates[index].login);
      textarea.value = result.value;
      textarea.setSelectionRange(result.caret, result.caret);
      dismiss();
      textarea.focus();
      textarea.dispatchEvent?.(new Event('input', { bubbles:true }));
    }

    function render(items) {
      candidates = Array.isArray(items) ? items : [];
      activeIndex = -1;
      listbox.replaceChildren();
      candidates.forEach((candidate, index) => {
        const option = createOption();
        option.type = 'button';
        option.id = listbox.id + '-option-' + index;
        option.className = 'mention-option';
        option.dataset.login = candidate.login;
        option.setAttribute('role', 'option');
        option.setAttribute('aria-selected', 'false');
        option.textContent = '@' + candidate.login + (candidate.name !== candidate.login ? ' · ' + candidate.name : '');
        option.addEventListener('pointerdown', event => {
          event.preventDefault();
          select(index);
        });
        listbox.appendChild(option);
      });
      listbox.hidden = !candidates.length;
      textarea.setAttribute('aria-expanded', candidates.length ? 'true' : 'false');
      status.textContent = candidates.length ? candidates.length + ' teammates found.' : 'No matching teammates.';
    }

    function schedule() {
      if (timer !== null) clearTimer(timer);
      mention = activeMention(textarea.value, textarea.selectionStart);
      const repository = options.getRepository();
      if (!mention || !repository) {
        dismiss();
        return;
      }
      const currentRequest = ++requestId;
      timer = setTimer(async () => {
        try {
          const items = await options.loadCandidates(repository, mention.query);
          if (currentRequest === requestId && repository === options.getRepository()) render(items);
        } catch (_error) {
          if (currentRequest === requestId) {
            listbox.hidden = true;
            textarea.setAttribute('aria-expanded', 'false');
            status.textContent = 'Teammate suggestions unavailable; you can keep typing.';
          }
        }
      }, options.delayMs ?? 180);
    }

    function keydown(event) {
      if (event.key === 'Escape' && !listbox.hidden) {
        event.preventDefault();
        dismiss();
      } else if (event.key === 'ArrowDown' && candidates.length) {
        event.preventDefault();
        activate(activeIndex + 1);
      } else if (event.key === 'ArrowUp' && candidates.length) {
        event.preventDefault();
        activate(activeIndex < 0 ? candidates.length - 1 : activeIndex - 1);
      } else if (event.key === 'Enter' && activeIndex >= 0) {
        event.preventDefault();
        select(activeIndex);
      }
    }

    return {
      start() {
        textarea.setAttribute('autocomplete', 'off');
        textarea.setAttribute('aria-autocomplete', 'list');
        textarea.setAttribute('aria-controls', listbox.id);
        textarea.setAttribute('aria-expanded', 'false');
        textarea.addEventListener('input', schedule);
        textarea.addEventListener('click', schedule);
        textarea.addEventListener('keydown', keydown);
      },
      dismiss,
    };
  }

  return { activeMention, insertMention, create };
});
