function calendarDay(value) {
  const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})(?:$|T)/);
  if (!match) return '';
  const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
  return date.getFullYear() === Number(match[1]) && date.getMonth() === Number(match[2]) - 1 &&
    date.getDate() === Number(match[3]) ? match.slice(1, 4).join('-') : '';
}

function localDay(value) {
  return value.getFullYear() + '-' + String(value.getMonth() + 1).padStart(2, '0') + '-' +
    String(value.getDate()).padStart(2, '0');
}

function issueDueState(dueDate, now) {
  const dueDay = calendarDay(dueDate);
  if (!dueDay) return null;
  const today = localDay(now);
  if (dueDay < today) return { label: 'Overdue', priority: 2 };
  if (dueDay === today) return { label: 'Due today', priority: 2.5 };
  return {
    label: 'Due ' + new Date(
      Number(dueDay.slice(0, 4)), Number(dueDay.slice(5, 7)) - 1, Number(dueDay.slice(8, 10))
    ).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
    priority: 4,
  };
}

function urgencyReason(item) {
  if (!item?.is_assigned || item.kind !== 'issue') return '';
  const priorityLabels = ['p0', 'priority-high', 'critical'];
  const priorityLabel = (item.labels || []).find(label =>
    priorityLabels.includes(String(label).toLowerCase())
  );
  if (priorityLabel) return priorityLabel + ' priority';
  return ['Overdue', 'Due today'].includes(item.due_label) ? item.due_label : '';
}

function attentionReason(item) {
  if (item?.has_update) return 'Unread update';
  return urgencyReason(item) || (item?.is_review ? 'Needs your review' : '');
}

function needsAttention(item) {
  return Boolean(item && (item.has_update || item.is_review || urgencyReason(item)));
}

function updateActionability(item) {
  const priorityLabels = ['p0', 'priority-high', 'critical'];
  if ((item?.labels || []).some(label => priorityLabels.includes(String(label).toLowerCase()))) {
    return { priority: 0, reason: 'Critical' };
  }
  if (item?.is_review) return { priority: 1, reason: 'Review requested' };
  if (item?.is_assigned && ['Overdue', 'Due today'].includes(item?.due_label)) {
    return { priority: 2, reason: item.due_label };
  }
  if (item?.is_assigned) return { priority: 3, reason: 'Assigned to you' };
  return { priority: 4, reason: '' };
}

function buildMyWork(data, now = new Date()) {
  const login = data.user?.login || '';
  const issues = (data.issues || []).map((item) => ({ ...item, kind: 'issue' }));
  const pulls = (data.pull_requests || []).map((item) => ({ ...item, kind: 'pull' }));
  const priorityLabels = ['p0', 'priority-high', 'critical'];

  const work = issues.concat(pulls).map((item) => {
    const labels = item.labels || [];
    const priorityLabel = labels.find((label) =>
      priorityLabels.includes(String(label).toLowerCase())
    );
    const assigned = (item.assignees || []).includes(login);
    const isReview = (item.work_reasons || []).includes('review_requested');
    const isFiled = (item.work_reasons || []).includes('created_by_me');
    const isCompleted = isFiled && item.state === 'closed';
    const due = item.kind === 'issue' ? issueDueState(item.due_date, now) : null;
    const normalized = {
      ...item,
      key: (item.repository || 'unknown') + '#' + item.number,
      is_review: isReview,
      is_filed: isFiled,
      is_completed: isCompleted,
      is_assigned: assigned,
      has_update: false,
      ...(due ? { due_label: due.label } : {}),
      reason: priorityLabel ? priorityLabel + ' priority' :
        (due && due.priority < 4 ? due.label :
          (isReview ? 'Needs your review' : (isCompleted ? 'Completed · review outcome' :
            (assigned ? 'Assigned to you' : (isFiled ? 'Filed by you' : 'Open work'))))),
      _priority: priorityLabel ? 0 :
        (due && due.priority < 4 ? due.priority : (isReview ? 3 : (isCompleted ? 3.5 : (assigned ? 4 : 5)))),
    };
    normalized.needs_attention = needsAttention(normalized);
    normalized.attention_reason = attentionReason(normalized);
    return normalized;
  });

  const byKey = new Map(work.map((item) => [item.kind + ':' + item.key, item]));
  (data.notifications || []).filter((item) => item && item.unread).forEach((update) => {
    const key = (update.repository || 'unknown') + '#' + update.number;
    const subjectKind = String(update.subject_type || '').toLowerCase().includes('pull') ? 'pull' : 'issue';
    const existing = byKey.get(subjectKind + ':' + key);
    if (existing) {
      const actionability = updateActionability(existing);
      existing.has_update = true;
      existing.needs_attention = true;
      existing.attention_reason = 'Unread update';
      existing.notification_id = update.id;
      existing.url = update.url || existing.url;
      existing.updated_at = update.updated_at || existing.updated_at;
      existing.update_reason = actionability.reason;
      existing._priority = actionability.priority;
      return;
    }
    if (!update.url) return;
    const item = {
      ...update,
      key,
      kind: 'update',
      is_review: false,
      is_assigned: false,
      has_update: true,
      needs_attention: true,
      attention_reason: 'Unread update',
      notification_id: update.id,
      reason: 'Unread update',
      update_reason: '',
      _priority: 4,
    };
    work.push(item);
    byKey.set(subjectKind + ':' + key, item);
  });

  return work.sort((left, right) =>
    left._priority - right._priority ||
    String(right.updated_at || '').localeCompare(String(left.updated_at || '')) ||
    left.key.localeCompare(right.key)
  ).map(({ _priority, ...item }) => item);
}

function acknowledgeNotification(items, notificationId) {
  return items.flatMap((item) => {
    if (item.notification_id !== notificationId) return [item];
    if (item.kind === 'update') return [];
    const { notification_id, ...acknowledged } = item;
    const next = { ...acknowledged, has_update: false };
    if ('needs_attention' in item) next.needs_attention = needsAttention(next);
    if ('attention_reason' in item) next.attention_reason = attentionReason(next);
    return [next];
  });
}

function createNotificationAcknowledger({ markRead, onItems, onStatus }) {
  const pending = new Set();
  return {
    async acknowledge(items, notificationId) {
      if (pending.has(notificationId)) return false;
      pending.add(notificationId);
      onItems(acknowledgeNotification(items, notificationId));
      onStatus('Marking update read…');
      try {
        await markRead(notificationId);
        onStatus('Update marked read.');
        return true;
      } catch (_error) {
        onItems(items);
        onStatus('Could not mark update read. Retry.');
        return false;
      } finally {
        pending.delete(notificationId);
      }
    },
  };
}

function notificationIds(items) {
  return Array.from(new Set((items || [])
    .filter((item) => item && item.has_update && Number.isInteger(item.notification_id))
    .map((item) => item.notification_id)));
}

function createBulkNotificationAcknowledger({ markRead, onItems, onStatus }) {
  let pending = false;
  return {
    async acknowledge(items, requestedIds) {
      if (pending) return false;
      const ids = Array.from(new Set((requestedIds || []).filter(Number.isInteger)));
      if (!ids.length) return false;
      pending = true;
      onStatus('Marking ' + ids.length + ' updates read…');
      try {
        const result = await markRead(ids);
        const marked = (result.marked || []).filter(Number.isInteger);
        const failed = (result.failed || []).filter(Number.isInteger);
        const updated = marked.reduce(
          (current, notificationId) => acknowledgeNotification(current, notificationId),
          items
        );
        onItems(updated);
        onStatus(failed.length ?
          marked.length + ' marked read · ' + failed.length + ' could not be updated — retry.' :
          marked.length + ' updates marked read.');
        return { marked, failed };
      } catch (_error) {
        onStatus('Could not mark updates read. Retry.');
        return false;
      } finally {
        pending = false;
      }
    },
  };
}

function createNotificationSelection({ limit = 50, onChange = () => {} } = {}) {
  const maximum = Number.isInteger(limit) && limit > 0 ? limit : 50;
  let active = false;
  const selected = new Set();
  const snapshot = () => ({
    active,
    ids: Array.from(selected),
    count: selected.size,
    limit: maximum,
    at_limit: selected.size >= maximum,
  });
  const publish = () => onChange(snapshot());
  const select = notificationId => {
    if (!active || !Number.isInteger(notificationId)) return 'inactive';
    if (selected.has(notificationId)) return 'already-selected';
    if (selected.size >= maximum) return 'limit';
    selected.add(notificationId);
    publish();
    return 'selected';
  };
  return {
    start() {
      if (active) return snapshot();
      active = true;
      publish();
      return snapshot();
    },
    select,
    toggle(notificationId) {
      if (!selected.has(notificationId)) return select(notificationId);
      selected.delete(notificationId);
      publish();
      return 'deselected';
    },
    retain(notificationIds) {
      const allowed = new Set((notificationIds || []).filter(Number.isInteger));
      let changed = false;
      selected.forEach(id => {
        if (!allowed.has(id)) {
          selected.delete(id);
          changed = true;
        }
      });
      if (changed) publish();
      return snapshot();
    },
    cancel() {
      active = false;
      selected.clear();
      publish();
      return snapshot();
    },
    snapshot,
  };
}

function createNotificationPager({ load, onNotifications, onPagination, onStatus }) {
  let pagination = { page: 1, total: 0, has_more: false };
  let pending = false;
  let completing = null;
  const pager = {
    reset(next) {
      pagination = { ...pagination, ...(next || {}) };
      onPagination(pagination);
    },
    async loadMore(existing) {
      if (pending || !pagination.has_more) return false;
      pending = true;
      onStatus('Loading older updates…');
      try {
        const result = await load(pagination.page + 1);
        const byId = new Map((existing || [])
          .filter(item => item && Number.isInteger(item.id))
          .map(item => [item.id, item]));
        (result.items || []).forEach(item => {
          if (item && Number.isInteger(item.id) && !byId.has(item.id)) byId.set(item.id, item);
        });
        pagination = {
          page: result.page,
          total: result.total,
          has_more: result.has_more === true,
        };
        const loaded = Math.min(pagination.total, pagination.page * 50);
        onNotifications(Array.from(byId.values()));
        onPagination(pagination);
        onStatus(loaded + ' of ' + pagination.total + ' unread updates loaded.');
        return true;
      } catch (_error) {
        onStatus('Could not load older updates. Retry.');
        return false;
      } finally {
        pending = false;
      }
    },
    loadAll(getExisting) {
      if (completing) return completing;
      completing = (async () => {
        while (pagination.has_more) {
          const loaded = await pager.loadMore(getExisting());
          if (!loaded) return false;
        }
        return true;
      })().finally(() => { completing = null; });
      return completing;
    },
  };
  return pager;
}

function createWorkPager({ load, onItems, onPagination, onStatus }) {
  let pagination = {};
  const pending = new Set();
  const completing = new Map();
  const labels = {
    issue: 'issues',
    pull: 'pull requests',
    review: 'review requests',
  };
  const pager = {
    reset(next) {
      Object.entries(next || {}).forEach(([stream, value]) => {
        const current = pagination[stream];
        pagination[stream] = current && current.page > 1 ?
          { ...value, page: current.page, has_more: current.page * 50 < value.total } :
          { ...value };
      });
      onPagination({ ...pagination });
    },
    async loadMore(stream, existing) {
      const page = pagination[stream];
      if (pending.has(stream) || !page?.has_more) return false;
      pending.add(stream);
      const label = labels[stream] || 'work';
      onStatus('Loading older ' + label + '…');
      try {
        const result = await load(stream, page.page + 1);
        const merged = new Map((existing || [])
          .filter(item => item && Number.isInteger(item.id))
          .map(item => [item.id, { ...item }]));
        (result.items || []).forEach(item => {
          if (!item || !Number.isInteger(item.id)) return;
          const current = merged.get(item.id);
          if (!current) {
            merged.set(item.id, { ...item });
            return;
          }
          const reasons = Array.from(new Set(
            (current.work_reasons || []).concat(item.work_reasons || [])
          ));
          merged.set(item.id, {
            ...current, ...item, ...(reasons.length ? { work_reasons: reasons } : {}),
          });
        });
        pagination[stream] = {
          page: result.page, total: result.total, has_more: result.has_more === true,
        };
        onItems(stream, Array.from(merged.values()));
        onPagination({ ...pagination });
        onStatus(Math.min(result.total, result.page * 50) + ' of ' + result.total + ' ' + label + ' loaded.');
        return true;
      } catch (_error) {
        onStatus('Could not load older ' + label + '. Retry.');
        return false;
      } finally {
        pending.delete(stream);
      }
    },
    loadAll(stream, getExisting) {
      if (completing.has(stream)) return completing.get(stream);
      const completion = (async () => {
        while (pagination[stream]?.has_more) {
          const loaded = await pager.loadMore(stream, getExisting());
          if (!loaded) return false;
        }
        return true;
      })().finally(() => completing.delete(stream));
      completing.set(stream, completion);
      return completion;
    },
  };
  return pager;
}

function createNotificationReader({
  load, markRead, onOpen, onDetail, onItems, onStatus, onClose,
  acknowledge = null,
  queueRead = null,
  loadSaved = () => null,
  loadConversation = null,
  onConversation = () => {},
  onConversationStatus = () => {},
  createPager = typeof createConversationPager === 'function' ? createConversationPager : null,
  getScope = () => '',
}) {
  let selected = null;
  let loadVersion = 0;
  let marking = false;
  let conversationPager = null;
  let offlineHydrated = false;
  let prefetched = null;
  let prefetchKey = '';

  async function hydrateConversation(item, version) {
    if (!loadConversation || !createPager || !item || offlineHydrated) return false;
    onConversationStatus('Loading conversation…');
    try {
      const initial = await loadConversation(item.notification_id, null);
      if (selected !== item || version !== loadVersion) return false;
      conversationPager = createPager({
        loadPage: page => loadConversation(item.notification_id, page),
      });
      const state = conversationPager.reset(initial);
      onConversation(state);
      onConversationStatus(state.comments.length + ' of ' +
        Math.max(state.total || 0, state.comments.length) + ' messages loaded.');
      return true;
    } catch (_error) {
      if (selected === item && version === loadVersion) {
        conversationPager = null;
        onConversationStatus('Conversation temporarily unavailable. Retry.');
      }
      return false;
    }
  }

  function prefetch(item) {
    if (!item) { prefetched = null; prefetchKey = ''; return false; }
    const notificationId = item?.notification_id;
    if (!Number.isInteger(notificationId) || offlineHydrated) return false;
    const key = getScope() + notificationId;
    if (prefetchKey === key) return prefetched;
    if (prefetched) return false;
    prefetchKey = key;
    prefetched = load(notificationId);
    return prefetched;
  }

  async function advanceAfterRead(items, current, queueing = false) {
    const updated = acknowledgeNotification(items, current.notification_id);
    onItems(updated);
    const currentIndex = items.indexOf(current);
    const remaining = items.slice(currentIndex + 1).concat(items.slice(0, currentIndex));
    const next = remaining.find(item => item && item.has_update &&
      Number.isInteger(item.notification_id) && (!queueing || loadSaved(item)));
    if (next) await open(next, queueing ? loadSaved(next) : null);
    else {
      selected = null;
      onClose();
      onStatus('Inbox cleared.');
    }
    return { items: updated, item: current, next: next || null };
  }

  async function open(item, savedDetail = null) {
    selected = item;
    offlineHydrated = Boolean(savedDetail && !Array.isArray(savedDetail));
    const version = ++loadVersion;
    const preload = !offlineHydrated && prefetchKey === getScope() + item.notification_id ? prefetched : null;
    prefetched = null;
    prefetchKey = '';
    onOpen(item);
    if (!preload) onStatus('Loading update…');
    try {
      let detail;
      if (offlineHydrated) detail = savedDetail;
      else if (preload) detail = await preload.catch(() => {
        onStatus('Loading update…');
        return load(item.notification_id);
      });
      else detail = await load(item.notification_id);
      if (selected !== item || version !== loadVersion) return false;
      onDetail(detail);
      if (createPager && loadConversation && detail.conversation) {
        conversationPager = createPager({
          loadPage: page => loadConversation(item.notification_id, page),
        });
        onConversation(conversationPager.reset(detail.conversation));
      } else {
        conversationPager = null;
      }
      onStatus('Update ready.');
      if (!offlineHydrated && detail.conversation_available) {
        void hydrateConversation(item, version);
      }
      return true;
    } catch (_error) {
      if (selected === item && version === loadVersion) {
        onStatus('Could not load update. Retry or open it in Gitea.');
      }
      return false;
    }
  }

  return {
    open,
    prefetch,
    retryConversation() {
      if (!selected || offlineHydrated) return Promise.resolve(false);
      return hydrateConversation(selected, loadVersion);
    },
    commentPager() {
      return conversationPager;
    },
    appendReply(comment) {
      if (!conversationPager) return false;
      onConversation(conversationPager.append(comment));
      return true;
    },
    acceptReadAndNext(items, item = selected) {
      if (!item || (selected && selected !== item)) return false;
      selected = item;
      return advanceAfterRead(items, item);
    },
    async loadOlder() {
      if (!conversationPager || !selected || offlineHydrated) return false;
      const pager = conversationPager;
      const version = loadVersion;
      onStatus('Loading older messages…');
      try {
        const state = await pager.loadOlder();
        if (pager !== conversationPager || version !== loadVersion) return false;
        onConversation(state);
        onStatus(state.comments.length + ' of ' + state.total + ' messages loaded.');
        return true;
      } catch (_error) {
        if (pager === conversationPager && version === loadVersion) {
          onStatus('Could not load older messages. Retry.');
        }
        return false;
      }
    },
    async markReadAndNext(items) {
      if (!selected || marking || (offlineHydrated && !queueRead)) return false;
      const current = selected;
      const queueing = offlineHydrated;
      marking = true;
      onStatus(queueing ? 'Queueing update read…' : 'Marking update read…');
      try {
        if (queueing) await queueRead(current.notification_id);
        else await markRead(current.notification_id);
        return await advanceAfterRead(items, current, queueing);
      } catch (_error) {
        onStatus(queueing ? 'Could not queue update read. Retry.' : 'Could not mark update read. Retry.');
        return false;
      } finally {
        marking = false;
      }
    },
    async acknowledgeAndNext(items) {
      if (!selected || marking || offlineHydrated || !acknowledge) return false;
      const current = selected;
      marking = true;
      onStatus('Adding reaction and marking read…');
      try {
        await acknowledge(current.notification_id);
        return await advanceAfterRead(items, current);
      } catch (_error) {
        onStatus('Could not acknowledge update. Retry.');
        return false;
      } finally {
        marking = false;
      }
    },
  };
}

function createNotificationReplier({
  post, storage, onStatus, authoredOutbox,
  createOperationId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random(),
}) {
  let pending = false;
  const keyFor = item => 'stackchain.update-reply.v1.' + item.notification_id;
  const operationKeyFor = item => keyFor(item) + ':operation';
  return {
    loadDraft(item) {
      try { return storage.getItem(keyFor(item)) || ''; }
      catch (_error) { return ''; }
    },
    saveDraft(item, body) {
      try {
        if ((storage.getItem(keyFor(item)) || '') !== body) storage.removeItem(operationKeyFor(item));
        storage.setItem(keyFor(item), body);
      }
      catch (_error) {}
    },
    async submit(item, body, attachment = null) {
      if (pending) return false;
      pending = true;
      this.saveDraft(item, body);
      let operationId;
      try {
        operationId = storage.getItem(operationKeyFor(item)) || String(createOperationId()).slice(0, 128);
        storage.setItem(operationKeyFor(item), operationId);
      } catch (_error) { operationId = String(createOperationId()).slice(0, 128); }
      onStatus('Sending reply…');
      try {
        if (attachment) {
          onStatus('Saving screenshot for durable delivery…');
          const admission = await authoredOutbox.enqueueDurably({
            kind:'update-reply', notificationId:item.notification_id, body, operationId, attachment,
          });
          const delivery = await authoredOutbox.retry(admission.item.id, admission.item.ownerLogin);
          if (!delivery.confirmed?.length) {
            const remaining = delivery.remaining?.find(candidate => candidate.id === admission.item.id);
            if (remaining?.status === 'attention') return false;
            onStatus('Queued for sync when the connection returns.');
            return { queued:true };
          }
          try { storage.removeItem(keyFor(item)); storage.removeItem(operationKeyFor(item)); }
          catch (_error) {}
          onStatus('Reply posted. You can mark this update read when ready.');
          return delivery.confirmed[0];
        }
        const result = await post(item.notification_id, body, operationId);
        try { storage.removeItem(keyFor(item)); }
        catch (_error) {}
        try { storage.removeItem(operationKeyFor(item)); }
        catch (_error) {}
        onStatus('Reply posted. You can mark this update read when ready.');
        return result;
      } catch (error) {
        const status = Number(error?.status || 0);
        if (authoredOutbox && (!status || status >= 500)) {
          onStatus('Saving for background delivery…');
          const admission = await authoredOutbox.enqueueDurably({
            kind: 'update-reply', notificationId: item.notification_id, body, operationId,
          });
          if (admission.background) {
            onStatus('Queued for sync when the connection returns.');
            return { queued: true };
          }
          onStatus('Saved for next launch; background delivery unavailable.');
          return { queued: false, degraded: true };
        }
        onStatus('Could not send reply. Your draft is safe; retry.');
        return false;
      } finally {
        pending = false;
      }
    },
  };
}

function filterMyWork(items, selectedFilter, selectedMilestone = 'all') {
  let filtered = items;
  if (selectedFilter === 'attention') filtered = items.filter(needsAttention);
  else if (selectedFilter === 'filed') filtered = items.filter((item) => item.is_filed);
  else if (selectedFilter === 'review') filtered = items.filter((item) => item.is_review);
  else if (selectedFilter === 'update') filtered = items.filter((item) => item.has_update);
  else if (selectedFilter !== 'all') filtered = items.filter((item) => item.kind === selectedFilter);
  if (selectedMilestone === 'all') return filtered;
  if (selectedMilestone === 'unplanned') {
    return filtered.filter(item => item.kind === 'issue' && !item.milestone);
  }
  return filtered.filter(item =>
    item.kind === 'issue' && String(item.milestone?.id || '') === String(selectedMilestone)
  );
}

function createCompletedFiledReview({
  storage,
  getLogin,
  key = 'stackchain.completed-filed-review.v1',
}) {
  const login = () => String(getLogin?.() || '').trim();
  const ownerKey = () => key + ':' + login();
  const identity = item => String(item?.repository || '') + '#' + String(item?.number || '');
  const read = () => {
    if (!login()) return { items:{}, pending:{} };
    try {
      const value = JSON.parse(storage?.getItem(ownerKey()) || '{}');
      return value && value.version === 1 && value.items && typeof value.items === 'object' ? {
        items:value.items,
        pending:value.pending && typeof value.pending === 'object' ? value.pending : {},
      } : { items:{}, pending:{} };
    } catch (_error) { return { items:{}, pending:{} }; }
  };
  const receipt = (item, stamp = String(item?.updated_at || '')) => ({
    repository:String(item?.repository || ''), number:item?.number, updated_at:stamp,
  });
  const save = value => storage?.setItem(ownerKey(), JSON.stringify({ version:1, ...value }));
  const partition = items => {
    const acknowledged = read().items;
    const needsReview = [];
    const reviewed = [];
    (items || []).forEach(item => {
      const isReviewed = item?.is_completed &&
        acknowledged[identity(item)] === String(item.updated_at || '');
      (isReviewed ? reviewed : needsReview).push(item);
    });
    reviewed.sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')));
    return { needsReview, reviewed };
  };
  return {
    partition,
    visible(items) {
      return partition(items).needsReview;
    },
    pending() {
      const state = read();
      return Object.entries(state.pending).map(([itemIdentity, stamp]) => {
        const boundary = itemIdentity.lastIndexOf('#');
        return receipt({
          repository:itemIdentity.slice(0, boundary),
          number:Number(itemIdentity.slice(boundary + 1)),
        }, stamp);
      });
    },
    adopt(snapshot) {
      const state = read();
      let changed = false;
      (snapshot?.receipts || []).forEach(remote => {
        const itemIdentity = identity(remote);
        const stamp = String(remote?.updated_at || '');
        if (!remote?.repository || !Number.isInteger(remote?.number) || !stamp) return;
        if (!state.items[itemIdentity] || state.items[itemIdentity] < stamp) {
          state.items[itemIdentity] = stamp;
          changed = true;
        }
        if (state.pending[itemIdentity] && state.pending[itemIdentity] <= stamp) {
          delete state.pending[itemIdentity];
        }
      });
      try { save(state); } catch (_error) { return false; }
      return changed;
    },
    acknowledge(item) {
      const stamp = String(item?.updated_at || '');
      if (!login() || !item?.is_completed || !item?.repository || !Number.isInteger(item?.number) || !stamp) {
        return false;
      }
      const state = read();
      const entries = Object.entries({ ...state.items, [identity(item)]: stamp }).slice(-200);
      const pending = Object.entries({ ...state.pending, [identity(item)]: stamp }).slice(-200);
      try {
        save({ items:Object.fromEntries(entries), pending:Object.fromEntries(pending) });
        return true;
      } catch (_error) { return false; }
    },
  };
}

function createFiledHistoryTabs({ root, review, onSelect }) {
  const buttons = Array.from(root.querySelectorAll('[data-filed-view]'));
  let partition = { needsReview:[], reviewed:[] };
  buttons.forEach(button => button.addEventListener('click', () => onSelect(button.dataset.filedView)));
  return {
    prepare(items) {
      partition = review.partition(items);
      root.querySelector('#filed-needs-review-count').textContent =
        partition.needsReview.filter(item => item.is_filed).length;
      root.querySelector('#filed-reviewed-count').textContent = partition.reviewed.length;
      return partition.needsReview;
    },
    render(selected, visible) {
      root.hidden = !visible;
      buttons.forEach(button =>
        button.setAttribute('aria-pressed', String(button.dataset.filedView === selected))
      );
      return selected === 'reviewed' ? partition.reviewed : partition.needsReview;
    },
  };
}

function agendaMyWork(items, now = new Date()) {
  const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const today = localDay(start);
  const tomorrowDate = new Date(start); tomorrowDate.setDate(tomorrowDate.getDate() + 1);
  const tomorrow = localDay(tomorrowDate);
  const afterTomorrowDate = new Date(start); afterTomorrowDate.setDate(afterTomorrowDate.getDate() + 2);
  const afterTomorrow = localDay(afterTomorrowDate);
  const horizonDate = new Date(start); horizonDate.setDate(horizonDate.getDate() + 7);
  const horizon = localDay(horizonDate);
  return (items || []).flatMap(item => {
    if (item?.kind !== 'issue' || !item.is_assigned || !item.due_date) return [];
    const due = calendarDay(item.due_date);
    if (!due || due >= horizon) return [];
    const group = due < today ? 'Overdue' : due < tomorrow ? 'Today' :
      due < afterTomorrow ? 'Tomorrow' : 'Next 7 days';
    return [{ ...item, agenda_group: group, _agenda_due: due }];
  }).sort((left, right) =>
    left._agenda_due.localeCompare(right._agenda_due) ||
    String(left.repository || '').localeCompare(String(right.repository || '')) ||
    Number(left.number || 0) - Number(right.number || 0)
  ).map(({ _agenda_due, ...item }) => item);
}

function milestoneLanes(items) {
  const lanes = new Map();
  (items || []).forEach(item => {
    if (item?.kind === 'issue' && Number.isInteger(item.milestone?.id) &&
        typeof item.milestone?.title === 'string') {
      lanes.set(item.milestone.id, { id: item.milestone.id, title: item.milestone.title });
    }
  });
  return Array.from(lanes.values()).sort((left, right) => left.title.localeCompare(right.title));
}

function workIdentity(item) {
  if (!item) return '';
  const kind = item.is_review ? 'review' : (item.kind || 'work');
  const repository = item.repository || '';
  const number = Number.isInteger(item.number) ? item.number : '';
  const notification = Number.isInteger(item.notification_id) ? item.notification_id : '';
  return [kind, repository, number, notification].join(':');
}

function createWorkSessionCheckpoint({
  storage,
  getLogin,
  onError = () => {},
  key = 'stackchain.today-session.v1',
}) {
  const login = () => String(getLogin() || '').trim();
  let errorReported = false;
  const reportError = error => {
    if (errorReported) return;
    errorReported = true;
    onError(error);
  };
  const read = () => {
    const owner = login();
    if (!owner) return null;
    try {
      const value = JSON.parse(storage.getItem(key) || 'null');
      if (value?.version !== 1 || value.login !== owner || typeof value.identity !== 'string' ||
          !Number.isInteger(value.index) || value.index < 0) return null;
      return value;
    } catch (error) {
      reportError(error);
      return null;
    }
  };
  return {
    read,
    save(identity, index) {
      const owner = login();
      if (!owner) return false;
      try {
        storage.setItem(key, JSON.stringify({ version:1, login:owner, identity, index }));
        return true;
      } catch (error) {
        reportError(error);
        return false;
      }
    },
    clear() {
      if (!read()) return false;
      try {
        storage.removeItem(key);
        return true;
      } catch (error) {
        reportError(error);
        return false;
      }
    },
  };
}

function createWorkSession({
  getItems, getFilter, getMilestone = () => 'all', checkpoint = null,
  checkpointEnabled = () => true, checkpointedEnabled = checkpointEnabled,
  onOpen, onProgress, onFinish,
}) {
  let currentIdentity = '';
  let currentIndex = -1;
  let running = false;
  let durable = false;
  const activeCheckpoint = () => typeof checkpoint === 'function' ? checkpoint() : checkpoint;

  const queue = () => filterMyWork(getItems() || [], getFilter(), getMilestone());
  const report = (items, index) => onProgress({
    index: index + 1,
    total: items.length,
    can_previous: index > 0,
    can_next: index < items.length - 1,
  });
  const finish = () => {
    const clearCheckpoint = durable;
    running = false;
    durable = false;
    currentIdentity = '';
    currentIndex = -1;
    if (clearCheckpoint) activeCheckpoint()?.clear();
    onFinish();
    return false;
  };
  const openAt = (items, index) => {
    if (!items.length || index < 0 || index >= items.length) return finish();
    currentIndex = index;
    currentIdentity = workIdentity(items[index]);
    if (durable) activeCheckpoint()?.save(currentIdentity, currentIndex);
    report(items, index);
    onOpen(items[index]);
    return true;
  };

  return {
    active: () => running,
    checkpointed: item => running && durable && checkpointedEnabled() && (!item || workIdentity(item) === currentIdentity),
    end: () => finish(),
    resumable: () => Boolean(activeCheckpoint()?.read()),
    reopen(requested = null) {
      if (!running) return false;
      const items = queue();
      const requestedIdentity = requested ? workIdentity(requested) : currentIdentity;
      const index = items.findIndex(item => workIdentity(item) === requestedIdentity);
      if (index < 0) return false;
      currentIdentity = requestedIdentity;
      currentIndex = index;
      if (durable && requested) activeCheckpoint()?.save(currentIdentity, currentIndex);
      report(items, index);
      onOpen(items[index]);
      return true;
    },
    resume(requested = null) {
      const saved = activeCheckpoint()?.read();
      if (!saved) return false;
      durable = true;
      const items = queue();
      if (!items.length) return finish();
      running = true;
      const exact = items.findIndex(item => workIdentity(item) ===
        (requested ? workIdentity(requested) : saved.identity));
      return openAt(items, exact >= 0 ? exact : Math.min(saved.index, items.length - 1));
    },
    start(item = null) {
      const items = queue();
      if (!items.length) return finish();
      running = true;
      durable = Boolean(activeCheckpoint() && checkpointEnabled());
      const requested = item ? items.findIndex(candidate => workIdentity(candidate) === workIdentity(item)) : 0;
      return openAt(items, requested >= 0 ? requested : 0);
    },
    reconcile() {
      if (!running) return false;
      const items = queue();
      const index = items.findIndex(item => workIdentity(item) === currentIdentity);
      if (index < 0) return items.length ? openAt(items, Math.min(currentIndex, items.length - 1)) : finish();
      currentIndex = index;
      report(items, index);
      return true;
    },
    previous() {
      if (!running) return false;
      const items = queue();
      const index = items.findIndex(item => workIdentity(item) === currentIdentity);
      return index > 0 ? openAt(items, index - 1) : false;
    },
    next(requested = null) {
      if (!running) return false;
      const items = queue();
      const index = items.findIndex(item => workIdentity(item) === currentIdentity);
      if (requested) {
        const requestedIndex = items.findIndex(item => workIdentity(item) === workIdentity(requested));
        return requestedIndex >= 0 ? openAt(items, requestedIndex) : false;
      }
      return index >= 0 && index < items.length - 1 ? openAt(items, index + 1) : finish();
    },
    complete(requested = null) {
      if (!running) return false;
      const items = queue();
      if (requested) {
        const requestedIndex = items.findIndex(item => workIdentity(item) === workIdentity(requested));
        return requestedIndex >= 0 ? openAt(items, requestedIndex) : false;
      }
      const stillPresent = items.findIndex(item => workIdentity(item) === currentIdentity);
      if (stillPresent >= 0) {
        return stillPresent < items.length - 1 ? openAt(items, stillPresent + 1) : finish();
      }
      return items.length ? openAt(items, Math.min(currentIndex, items.length - 1)) : finish();
    },
    items: () => queue().slice(),
    target(action) {
      const items = queue();
      if (!items.length) return null;
      if (action === 'start') return items[0];
      if (action === 'resume') {
        const saved = activeCheckpoint()?.read();
        if (!saved) return null;
        const exact = items.findIndex(item => workIdentity(item) === saved.identity);
        return items[exact >= 0 ? exact : Math.min(saved.index, items.length - 1)];
      }
      const current = items.findIndex(item => workIdentity(item) === currentIdentity);
      if (action === 'continue') return current >= 0 ? items[current] : null;
      if (action === 'next') return current >= 0 && current < items.length - 1 ? items[current + 1] : null;
      if (action === 'complete') {
        return items[current >= 0 ? Math.min(current + 1, items.length - 1) : Math.min(currentIndex, items.length - 1)];
      }
      return null;
    },
  };
}

function replaceIssueLabels(data, repository, number, labels) {
  return {
    ...data,
    issues: (data.issues || []).map(item =>
      item.repository === repository && item.number === number ? { ...item, labels: [...labels] } : item
    ),
  };
}

function replaceIssueContent(data, repository, number, content) {
  return {
    ...data,
    issues: (data.issues || []).map(item =>
      item.repository === repository && item.number === number ? { ...item, ...content } : item
    ),
  };
}

function replaceIssueDueDate(data, repository, number, dueDate) {
  return {
    ...data,
    issues: (data.issues || []).map(item =>
      item.repository === repository && item.number === number ? { ...item, due_date: dueDate } : item
    ),
  };
}

function replaceIssueMilestone(data, repository, number, milestone) {
  return {
    ...data,
    issues: (data.issues || []).map(item =>
      item.repository === repository && item.number === number ? { ...item, milestone } : item
    ),
  };
}

function removeIssue(data, repository, number) {
  return {
    ...data,
    issues: (data.issues || []).filter(item =>
      item.repository !== repository || item.number !== number
    ),
  };
}

function summarizeMyWork(items) {
  const updates = items.filter((item) => item.has_update).length;
  const reviews = items.filter((item) => item.is_review).length;
  const assigned = items.filter((item) => item.is_assigned).length;
  const updateLabel = updates + ' unread update' + (updates === 1 ? '' : 's');
  const reviewLabel = reviews + ' review' + (reviews === 1 ? '' : 's');
  const assignedLabel = assigned + ' assigned';
  return (updates ? updateLabel + ' · ' : '') + reviewLabel + ' · ' + assignedLabel;
}

function findQueueItems(items, query) {
  const needle = String(query || '').trim().toLowerCase();
  if (!needle) return (items || []).slice();
  return (items || []).filter(item => {
    const number = Number.isInteger(item?.number) ? '#' + item.number : '';
    return [item?.repository, item?.key, number, item?.title]
      .some(value => String(value || '').toLowerCase().includes(needle));
  });
}

function filedFollowUpTarget(items) {
  const item = (items || []).find(candidate => candidate?.is_filed);
  if (!item) return null;
  return { kind:item.has_update ? 'update' : 'issue', item };
}

function countMyWork(items) {
  return {
    all: items.length,
    attention: items.filter(needsAttention).length,
    filed: items.filter((item) => item.is_filed).length,
    issue: items.filter((item) => item.kind === 'issue').length,
    pull: items.filter((item) => item.kind === 'pull' && !item.is_review).length,
    review: items.filter((item) => item.is_review).length,
    update: items.filter((item) => item.has_update).length,
  };
}

if (typeof module !== 'undefined' && module.exports) {
  buildMyWork.filterMyWork = filterMyWork;
  buildMyWork.createCompletedFiledReview = createCompletedFiledReview;
  buildMyWork.createFiledHistoryTabs = createFiledHistoryTabs;
  buildMyWork.agendaMyWork = agendaMyWork;
  buildMyWork.milestoneLanes = milestoneLanes;
  buildMyWork.createWorkSession = createWorkSession;
  buildMyWork.createWorkSessionCheckpoint = createWorkSessionCheckpoint;
  buildMyWork.workIdentity = workIdentity;
  buildMyWork.replaceIssueLabels = replaceIssueLabels;
  buildMyWork.replaceIssueContent = replaceIssueContent;
  buildMyWork.replaceIssueDueDate = replaceIssueDueDate;
  buildMyWork.replaceIssueMilestone = replaceIssueMilestone;
  buildMyWork.removeIssue = removeIssue;
  buildMyWork.summarizeMyWork = summarizeMyWork;
  buildMyWork.findQueueItems = findQueueItems;
  buildMyWork.filedFollowUpTarget = filedFollowUpTarget;
  buildMyWork.countMyWork = countMyWork;
  buildMyWork.acknowledgeNotification = acknowledgeNotification;
  buildMyWork.createNotificationAcknowledger = createNotificationAcknowledger;
  buildMyWork.notificationIds = notificationIds;
  buildMyWork.createBulkNotificationAcknowledger = createBulkNotificationAcknowledger;
  buildMyWork.createNotificationSelection = createNotificationSelection;
  buildMyWork.createNotificationPager = createNotificationPager;
  buildMyWork.createWorkPager = createWorkPager;
  buildMyWork.createNotificationReader = createNotificationReader;
  buildMyWork.createNotificationReplier = createNotificationReplier;
  module.exports = buildMyWork;
}
