(function (root, factory) {
  const createSearchPreview = factory();
  if (typeof module === 'object' && module.exports) module.exports = createSearchPreview;
  if (root) {
    root.createSearchPreview = createSearchPreview;
    root.safeSearchUrl = value => {
      try {
        const url = new URL(value);
        return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
      } catch (_) { return ''; }
    };
    root.searchPreviewUrl = (state, defaultScope, location) => {
      const { query, preview, scope = defaultScope } = state;
      const params = new URLSearchParams({
        search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
        search_kind:scope.kind, search_state:scope.state,
      });
      if (scope.repository) params.set('search_repository', scope.repository);
      return new URL('?' + params, location.origin + location.pathname).href;
    };
    root.searchPreviewPath = item => {
      const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
      return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
        '/preview?kind=' + encodeURIComponent(item.kind);
    };
    root.searchPreviewConversationPath = (item, page) => {
      const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
      const query = new URLSearchParams({ kind:item.kind, limit:'20' });
      if (page) query.set('page', String(page));
      return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
        '/preview/conversation?' + query.toString();
    };
    root.searchPreviewReplyPath = item => root.searchPreviewPath(item).replace(/\?.*$/, '') +
      '/comments?kind=' + encodeURIComponent(item.kind);
    root.searchPreviewReplyOptions = (fetchJson, storage, crypto) => ({
      storage,
      createOperationId:() => crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random(),
      postReply:(item,body,operationId) => fetchJson(root.searchPreviewReplyPath(item), {
        method:'POST', headers:{Accept:'application/json','Content-Type':'application/json','Idempotency-Key':operationId},
        body:JSON.stringify({body}),
      }),
    });
    root.renderSearchPreviewConversation = (conversation, document, escapeHtml, formatTime, renderMarkdown) => {
      const comments = document.querySelector('#search-preview-comments');
      const status = document.querySelector('#search-preview-conversation-status');
      const retry = document.querySelector('#retry-search-preview-conversation');
      const older = document.querySelector('#load-older-search-preview-comments');
      if (!conversation) {
        comments.textContent = status.textContent = '';
        retry.hidden = older.hidden = true;
        return;
      }
      comments.innerHTML = (conversation.comments || []).map(comment =>
        '<article class="search-preview-comment" data-comment-id="' + Number(comment.id || 0) + '">' +
        '<div class="small">' + escapeHtml(comment.author || 'Unknown author') +
        (comment.created_at ? ' · ' + escapeHtml(formatTime(comment.created_at)) : '') + '</div>' +
        '<div class="markdown-content">' + renderMarkdown(comment.body || '') + '</div></article>'
      ).join('');
      retry.hidden = conversation.status !== 'error';
      older.hidden = conversation.status !== 'ready' || !conversation.olderPage;
      older.disabled = conversation.status === 'loading';
      if (conversation.status === 'loading') status.textContent = conversation.comments?.length
        ? 'Loading older messages…' : 'Loading current conversation…';
      else if (conversation.status === 'error') status.textContent =
        'Conversation unavailable. Preview and planning actions still work.';
      else if (!conversation.comments?.length) status.textContent = 'No conversation yet.';
      else status.textContent = conversation.comments.length +
        (conversation.comments.length === 1 ? ' message.' : ' messages.');
    };
    root.renderSearchPreviewReply = (state, detail, preview, document) => {
      const section = document.querySelector('.search-preview-reply');
      const input = document.querySelector('#search-preview-reply');
      const status = document.querySelector('#search-preview-reply-status');
      const buttons = ['#send-search-preview-reply', '#send-search-preview-reply-next']
        .map(selector => document.querySelector(selector));
      section.hidden = detail?.commentable !== true;
      status.textContent = '';
      if (section.hidden) {
        buttons.forEach(button => { button.disabled = true; });
        return;
      }
      input.value = preview.replyDraft();
      const replying = state.status === 'replying';
      buttons.forEach(button => {
        button.disabled = replying || (!input.value.trim() && !preview.hasReplyAttachments?.());
      });
      if (replying) status.textContent = 'Saving reply for delivery…';
      else if (state.status === 'queued') status.textContent = 'Reply queued. It will send when connected.';
      else if (state.status === 'replied') status.textContent = 'Reply posted.';
      else if (state.status === 'reply-error') status.textContent =
        state.error?.message || 'Reply failed. Your draft is safe; retry when ready.';
    };
  }
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  return function createSearchPreview({ fetchJson, fetchConversation, mutate, share, postReply, queueReply, prepareReply, afterReply, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
    if (Array.isArray(session)) {
      getSession = session[0];
      loadMore = () => session[1].loadMore();
      onNavigate = session[2];
      navigationRoot = document;
    }
    let generation = 0;
    let current = null;
    let mutationRequest = null;
    let shareRequest = null;
    let moveRequest = null;
    let replyRequest = null;
    let conversation = null;

    function sameItem(left, right) {
      return left && right && left.kind === right.kind && left.repository === right.repository &&
        Number(left.number) === Number(right.number);
    }

    function replyKey(item, suffix) {
      return 'stackchain.search-reply.' + [item?.kind, item?.repository, item?.number]
        .map(value => encodeURIComponent(String(value || ''))).join('.') + '.' + suffix;
    }

    function stored(key) {
      try { return storage?.getItem(key) || ''; }
      catch (_) { return ''; }
    }

    function save(key, value) {
      try {
        if (value) storage?.setItem(key, value);
        else storage?.removeItem(key);
      } catch (_) { /* Volatile drafts still remain in the textarea. */ }
    }

    function navigation(item) {
      const session = typeof getSession === 'function' ? getSession() : null;
      const items = Array.isArray(session?.items) ? session.items : [];
      const index = items.findIndex(candidate => sameItem(candidate, item));
      if (index < 0) return null;
      return {
        position:index + 1,
        total:items.length,
        hasPrevious:index > 0,
        hasNext:index < items.length - 1 || session.more === true,
      };
    }

    function publish(state) {
      const position = navigation(state.item || current);
      if (navigationRoot) {
        const bar = navigationRoot.querySelector('.search-preview-navigation');
        bar.hidden = !position;
        if (position) {
          navigationRoot.querySelector('#search-preview-position').textContent = position.position + ' of ' + position.total;
          navigationRoot.querySelector('#previous-search-result').disabled = !position.hasPrevious;
          navigationRoot.querySelector('#next-search-result').disabled = !position.hasNext;
        }
      }
      onState(position ? { ...state, navigation:position } : state);
      if (state.complete && navigationRoot) {
        navigationRoot.querySelector('#search-preview-status').textContent = 'Search pass complete.';
      }
    }

    function run(action, pending, success, detail) {
      if (mutationRequest) return mutationRequest;
      publish({ status: pending, item: current, detail });
      mutationRequest = mutate(detail, action).then(result => {
        publish({ status: success, item: current, detail, result });
        return result;
      }).catch(error => {
        publish({ status: 'ready', item: current, detail, error });
        throw error;
      }).finally(() => { mutationRequest = null; });
      return mutationRequest;
    }

    function loadConversation(detail, requestGeneration, page) {
      if (typeof fetchConversation !== 'function') return Promise.resolve(null);
      const previousComments = page && Array.isArray(conversation?.comments)
        ? conversation.comments : [];
      conversation = { status:'loading', comments:previousComments, olderPage:page ?? null };
      publish({ status:'ready', item:current, detail, conversation });
      return fetchConversation(detail, page).then(result => {
        if (requestGeneration !== generation) return result;
        const incoming = Array.isArray(result?.comments) ? result.comments : [];
        const liveComments = Array.isArray(conversation?.comments) ? conversation.comments : previousComments;
        const comments = [...incoming, ...liveComments].filter((comment, index, all) =>
          all.findIndex(candidate => candidate?.id === comment?.id) === index);
        conversation = {
          status:'ready',
          comments,
          olderPage:result?.older_page ?? null,
        };
        publish({ status:'ready', item:current, detail:current, conversation });
        return result;
      }).catch(error => {
        if (requestGeneration === generation) {
          conversation = {
            status:'error', comments:previousComments,
            olderPage:page ?? conversation?.olderPage ?? null, error,
          };
          publish({ status:'ready', item:current, detail:current, conversation });
        }
        return null;
      });
    }

    const api = {
      hasReplyAttachments() {
        return Boolean(hasAttachments?.());
      },
      open(item) {
        if (current && !sameItem(current, item) && hasAttachments?.()) clearAttachments?.();
        generation += 1;
        const requestGeneration = generation;
        current = { ...item };
        conversation = null;
        publish({ status: 'loading', item: current });
        return fetchJson(current).then(detail => {
          if (requestGeneration === generation) {
            current = { ...current, ...detail };
            if (typeof fetchConversation === 'function') {
              loadConversation(current, requestGeneration);
            } else {
              publish({ status: 'ready', item: current, detail });
            }
          }
          return detail;
        }).catch(error => {
          if (requestGeneration === generation) {
            publish({ status: 'error', item: current, error });
          }
          throw error;
        });
      },
      close() {
        clearAttachments?.();
        generation += 1;
        current = null;
        onState({ status: 'closed' });
      },
      previous() {
        const session = typeof getSession === 'function' ? getSession() : null;
        const items = Array.isArray(session?.items) ? session.items : [];
        const index = items.findIndex(item => sameItem(item, current));
        if (index <= 0) return Promise.resolve(current);
        const item = items[index - 1];
        if (typeof onNavigate === 'function') onNavigate(item);
        return api.open(item);
      },
      next() {
        if (moveRequest) return moveRequest;
        moveRequest = (async () => {
          let session = typeof getSession === 'function' ? getSession() : null;
          let items = Array.isArray(session?.items) ? session.items : [];
          let index = items.findIndex(item => sameItem(item, current));
          if (index < 0) return current;
          if (index === items.length - 1 && session?.more === true && typeof loadMore === 'function') {
            await loadMore();
            session = getSession();
            items = Array.isArray(session?.items) ? session.items : [];
            index = items.findIndex(item => sameItem(item, current));
          }
          if (index >= 0 && index < items.length - 1) {
            const item = items[index + 1];
            if (typeof onNavigate === 'function') onNavigate(item);
            return api.open(item);
          }
          publish({ status:'ready', item:current, detail:current, complete:true });
          return current;
        })().finally(() => { moveRequest = null; });
        return moveRequest;
      },
      retryConversation() {
        if (!current) return Promise.resolve(null);
        return loadConversation(current, generation);
      },
      loadOlderConversation() {
        if (!current || !conversation?.olderPage) return Promise.resolve(null);
        return loadConversation(current, generation, conversation.olderPage);
      },
      saveReplyDraft(body) {
        if (!current) return '';
        const key = replyKey(current, 'draft');
        const next = String(body || '');
        if (stored(key) !== next) save(replyKey(current, 'operation'), '');
        save(key, next);
        return next;
      },
      replyDraft() {
        return current ? stored(replyKey(current, 'draft')) : '';
      },
      reply({ advance = false } = {}) {
        if (replyRequest) return replyRequest;
        if (!current || (typeof postReply !== 'function' && typeof queueReply !== 'function')) {
          return Promise.reject(new Error('Replying is unavailable.'));
        }
        const body = api.replyDraft().trim();
        if (!body && !hasAttachments?.()) return Promise.reject(new Error('Write a reply or add a photo first.'));
        const item = { ...current };
        const operationKey = replyKey(item, 'operation');
        let operationId = stored(operationKey);
        if (!operationId) {
          operationId = String(createOperationId?.() || Date.now()).slice(0, 128);
          save(operationKey, operationId);
        }
        publish({ status:'replying', item:current, detail:current, conversation });
        replyRequest = (typeof queueReply === 'function' ?
          Promise.resolve(queueReply(item, body, operationId)) :
          Promise.resolve(typeof prepareReply === 'function' ? prepareReply(item, body) : body)
            .then(preparedBody => {
              if (!String(preparedBody || '').trim()) throw new Error('Write a reply or add a photo first.');
              return postReply(item, preparedBody, operationId);
            })
        ).then(async comment => {
          await afterReply?.(item);
          if (!comment?.queued) {
            const comments = [...(conversation?.comments || [])];
            if (!comments.some(candidate => candidate?.id === comment?.id)) comments.push(comment);
            conversation = { status:'ready', comments, olderPage:conversation?.olderPage ?? null };
          }
          save(replyKey(item, 'draft'), '');
          save(operationKey, '');
          clearAttachments?.();
          publish({ status:comment?.queued ? 'queued' : 'replied', item:current, detail:current, conversation, result:comment });
          return advance ? api.next().then(() => comment) : comment;
        }).catch(error => {
          publish({ status:'reply-error', item:current, detail:current, conversation, error });
          throw error;
        }).finally(() => { replyRequest = null; });
        return replyRequest;
      },
      share(url) {
        if (shareRequest) return shareRequest;
        if (!current || typeof share !== 'function') return Promise.reject(new Error('Sharing is unavailable.'));
        const detail = current;
        publish({ status:'sharing', item:current, detail });
        shareRequest = share(url).then(result => {
          publish({ status:result, item:current, detail });
          return result;
        }).catch(error => {
          publish({ status:error?.name === 'AbortError' ? 'share-canceled' : 'share-error', item:current, detail, error });
          throw error;
        }).finally(() => { shareRequest = null; });
        return shareRequest;
      },
      claim(detail) {
        return run('claim', 'claiming', 'claimed', detail);
      },
      reopen(detail) {
        return run('reopen', 'reopening', 'reopened', detail);
      },
    };
    if (navigationRoot) {
      navigationRoot.querySelector('#previous-search-result').addEventListener('click', () => api.previous().catch(() => {}));
      navigationRoot.querySelector('#next-search-result').addEventListener('click', () => api.next().catch(() => {}));
      navigationRoot.querySelector('#retry-search-preview-conversation').addEventListener('click', () => api.retryConversation());
      navigationRoot.querySelector('#load-older-search-preview-comments').addEventListener('click', () => api.loadOlderConversation());
      const reply = navigationRoot.querySelector('#search-preview-reply');
      reply?.addEventListener('input', event => {
        api.saveReplyDraft(event.target.value);
        const disabled = !event.target.value.trim();
        navigationRoot.querySelector('#send-search-preview-reply').disabled = disabled;
        navigationRoot.querySelector('#send-search-preview-reply-next').disabled = disabled;
      });
      reply?.addEventListener('focus', event => event.target.scrollIntoView({ block:'center', behavior:'smooth' }));
      navigationRoot.querySelector('#send-search-preview-reply')?.addEventListener('click', () => api.reply().catch(() => {}));
      navigationRoot.querySelector('#send-search-preview-reply-next')?.addEventListener('click', () =>
        api.reply({ advance:true }).catch(() => {}));
    }
    return api;
  };
});
