// @-mention autocomplete. Attaches to a <textarea>/<input> or a
// contenteditable element: typing "@" + letters shows a user picker; choosing
// one inserts "@Name ". While the menu is open the host element has
// data-mention-active="1" so the host's own Enter handler can defer to us.
import { api } from '/static/js/api.js';

const TOKEN_RE = /(?:^|\s)@([\w.\-]{0,30})$/;

export function attachMentions(el) {
  const isCE = el.isContentEditable;
  // drop any stale menu left over from a previous attach on the same field
  // (task/comment views recreate their composer on every re-render)
  if (el.id) {
    document.querySelectorAll(`.mention-menu[data-mention-for="${el.id}"]`).forEach((m) => m.remove());
  }
  const menu = document.createElement('div');
  menu.className = 'mention-menu';
  menu.hidden = true;
  if (el.id) menu.dataset.mentionFor = el.id;
  document.body.appendChild(menu);

  let items = [];
  let active = 0;
  let seq = 0;
  el._mentions = el._mentions || new Map(); // display name -> user id

  function close() {
    menu.hidden = true;
    items = [];
    delete el.dataset.mentionActive;
  }

  function context() {
    if (isCE) {
      const sel = window.getSelection();
      if (!sel || !sel.rangeCount) return null;
      const range = sel.getRangeAt(0);
      const node = range.startContainer;
      if (node.nodeType !== 3) return null;
      const before = node.textContent.slice(0, range.startOffset);
      const m = before.match(TOKEN_RE);
      if (!m) return null;
      return { query: m[1], node, end: range.startOffset, start: range.startOffset - m[1].length - 1 };
    }
    const pos = el.selectionStart;
    const m = el.value.slice(0, pos).match(TOKEN_RE);
    if (!m) return null;
    return { query: m[1], end: pos, start: pos - m[1].length - 1 };
  }

  async function update() {
    const ctx = context();
    if (!ctx) { close(); return; }
    const mySeq = ++seq;
    let users;
    try { users = (await api('GET', `/api/v1/users?q=${encodeURIComponent(ctx.query)}`)).users; }
    catch (e) { return; }
    if (mySeq !== seq) return; // a newer keystroke superseded this fetch
    const ctx2 = context();
    if (!ctx2) { close(); return; }
    items = users.slice(0, 6);
    if (!items.length) { close(); return; }
    active = 0;
    render(ctx2);
  }

  function render(ctx) {
    menu.replaceChildren(...items.map((u, i) => {
      const row = document.createElement('button');
      row.type = 'button';
      row.className = 'mention-row' + (i === active ? ' active' : '');
      row.innerHTML = `<strong>${escapeHtml(u.name)}</strong> <span class="muted">${escapeHtml(u.email)}</span>`;
      row.addEventListener('mousedown', (e) => { e.preventDefault(); pick(u, ctx); });
      return row;
    }));
    position();
    menu.hidden = false;
    el.dataset.mentionActive = '1';
  }

  function position() {
    const rect = el.getBoundingClientRect();
    menu.style.left = `${rect.left + window.scrollX}px`;
    menu.style.top = `${rect.bottom + window.scrollY + 2}px`;
    menu.style.minWidth = `${Math.min(Math.max(rect.width, 220), 360)}px`;
  }

  function highlight() {
    [...menu.children].forEach((c, i) => c.classList.toggle('active', i === active));
  }

  function pick(u, ctx) {
    el._mentions.set(u.name, u.id);
    if (isCE) {
      // insert an atomic mention chip + a trailing space in the text node
      const node = ctx.node;
      const full = node.textContent;
      const after = full.slice(ctx.end);
      node.textContent = full.slice(0, ctx.start);
      const span = document.createElement('span');
      span.className = 'mention';
      span.dataset.userCard = u.id;
      span.contentEditable = 'false';
      span.textContent = '@' + u.name;
      // the chip is an atomic, non-editable element, so the trailing space in
      // this separate text node stays put as the user keeps typing
      const tail = document.createTextNode(' ' + after);
      const parent = node.parentNode;
      const ref = node.nextSibling;
      parent.insertBefore(span, ref);
      parent.insertBefore(tail, ref);
      const sel = window.getSelection();
      const range = document.createRange();
      range.setStart(tail, 1);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    } else {
      const text = '@' + u.name + ' ';
      const v = el.value;
      el.value = v.slice(0, ctx.start) + text + v.slice(ctx.end);
      const caret = ctx.start + text.length;
      el.setSelectionRange(caret, caret);
    }
    close();
    el.focus();
    el.dispatchEvent(new Event('input', { bubbles: true }));
  }

  el.addEventListener('input', update);
  el.addEventListener('keydown', (e) => {
    if (menu.hidden) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); active = (active + 1) % items.length; highlight(); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); active = (active - 1 + items.length) % items.length; highlight(); }
    else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); pick(items[active], context()); }
    else if (e.key === 'Escape') { e.preventDefault(); close(); }
  });
  el.addEventListener('blur', () => setTimeout(close, 150));
}

// mentionHTML turns the plain text of a textarea/input into safe HTML, wrapping
// any recorded "@Name" run in a mention span carrying the user id. Names with
// spaces are matched exactly (longest first) against what the picker inserted.
export function mentionHTML(text, el) {
  const mentions = (el && el._mentions) || new Map();
  const names = [...mentions.keys()].sort((a, b) => b.length - a.length);
  let out = '';
  let i = 0;
  while (i < text.length) {
    if (text[i] === '@') {
      const name = names.find((n) => text.startsWith('@' + n, i));
      if (name) {
        out += `<span class="mention" data-user-card="${escapeHtml(mentions.get(name))}">@${escapeHtml(name)}</span>`;
        i += 1 + name.length;
        continue;
      }
    }
    out += escapeHtml(text[i]);
    i += 1;
  }
  return out;
}

function escapeHtml(s) {
  return String(s ?? '').replace(/[&<>"']/g, (c) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  }[c]));
}
