---
import {
  resolvePublicApiBaseUrl,
  resolvePublicCommentTurnstileSiteKey,
} from '../lib/api/client';
import { getI18n } from '../lib/i18n';
import type { SiteSettings } from '../lib/types';

interface Props {
  postSlug: string;
  class?: string;
  siteSettings: SiteSettings;
}

const { postSlug, class: className = '', siteSettings } = Astro.props as Props;
const { t } = getI18n(Astro);
const publicApiBaseUrl = resolvePublicApiBaseUrl(Astro.url);
const commentVerificationMode = siteSettings.comments.verificationMode;
const turnstileSiteKey = commentVerificationMode === 'turnstile'
  ? siteSettings.comments.turnstileSiteKey || resolvePublicCommentTurnstileSiteKey()
  : '';
---

<div
  class={`paragraph-comments-shell ${className}`}
  data-post-slug={postSlug}
  data-api-base={publicApiBaseUrl}
  data-storage-key={`termi:paragraph-comments:${postSlug}`}
  data-verification-mode={commentVerificationMode}
  data-turnstile-site-key={turnstileSiteKey || undefined}
>
  <div class="paragraph-comments-toolbar terminal-panel-muted">
    <div class="paragraph-comments-toolbar-copy">
      <span class="terminal-kicker">
        <i class="fas fa-paragraph"></i>
        {t('paragraphComments.kicker')}
      </span>
      <p class="text-sm leading-6 text-[var(--text-secondary)]" data-summary-text>
        {t('paragraphComments.scanning')}
      </p>
    </div>

    <button
      type="button"
      class="terminal-action-button paragraph-comments-visibility"
      data-display-toggle
      aria-pressed="true"
    >
      <i class="fas fa-comment-dots"></i>
      <span data-toggle-label>{t('paragraphComments.hideMarkers')}</span>
    </button>
  </div>
</div>

<script>
  const t = window.__termiTranslate;
  const locale = document.documentElement.lang || 'zh-CN';
  import { buildParagraphDescriptors } from '../lib/utils/paragraph-comments';
  import { mountTurnstile, type MountedTurnstile } from '../lib/utils/turnstile';

  interface BrowserComment {
    id: number;
    author: string | null;
    content: string | null;
    created_at: string;
    reply_to_comment_id: number | null;
    paragraph_excerpt: string | null;
  }

  interface PendingComment {
    id: string;
    author: string;
    content: string;
    created_at: string;
    reply_to_comment_id: number | null;
  }

  interface SummaryItem {
    paragraph_key: string;
    count: number;
  }

  const wrappers = document.querySelectorAll('.paragraph-comments-shell');
  const wrapper = wrappers.item(wrappers.length - 1) as HTMLElement | null;
  const postSlug = wrapper?.dataset.postSlug || '';
  const apiBase = wrapper?.dataset.apiBase || '/api';
  const storageKey = wrapper?.dataset.storageKey || 'termi:paragraph-comments';
  const verificationMode = wrapper?.dataset.verificationMode || 'captcha';
  const useTurnstile = verificationMode === 'turnstile';
  const useCaptcha = verificationMode === 'captcha';
  const turnstileSiteKey = wrapper?.dataset.turnstileSiteKey || '';
  const articleRoot = wrapper?.closest('[data-article-slug]') as HTMLElement | null;
  const articleContent = articleRoot?.querySelector('.article-content') as HTMLElement | null;
  const summaryText = wrapper?.querySelector('[data-summary-text]') as HTMLElement | null;
  const toggleButton = wrapper?.querySelector('[data-display-toggle]') as HTMLButtonElement | null;
  const toggleLabel = wrapper?.querySelector('[data-toggle-label]') as HTMLElement | null;

  if (wrapper && articleRoot && articleContent && postSlug) {
  const paragraphCounts = new Map<string, number>();
  const paragraphMarkers = new Map<string, HTMLButtonElement>();
  const paragraphDescriptors = new Map<
    string,
    ReturnType<typeof buildParagraphDescriptors>[number]
  >();
  const threadCache = new Map<string, BrowserComment[]>();
  const pendingComments = new Map<string, PendingComment[]>();

  let activeParagraphKey: string | null = null;
  let activeReplyToCommentId: number | null = null;
  let pendingCounter = 0;
  let markersVisible = true;
  let turnstileWidget: MountedTurnstile | null = null;

  function escapeHtml(value: string): string {
    return value
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;');
  }

  function formatCommentDate(value: string): string {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) {
      return value;
    }

    return date.toLocaleString(locale, {
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  }

  function setSummaryMessage(message: string) {
    if (summaryText) {
      summaryText.textContent = message;
    }
  }

  function countLabel(count: number): string {
    if (count <= 0) {
      return t('paragraphComments.zeroNotes');
    }
    if (count === 1) {
      return t('paragraphComments.oneNote');
    }
    return t('paragraphComments.manyNotes', { count });
  }

  function markerCountText(count: number): string {
    if (count <= 0) {
      return '';
    }

    return count > 99 ? '99+' : String(count);
  }

  function previewReplyText(value: string | null | undefined, limit = 88) {
    const normalized = (value || '').replace(/\s+/g, ' ').trim();
    if (normalized.length <= limit) {
      return normalized || null;
    }

    return `${normalized.slice(0, limit).trimEnd()}...`;
  }

  function anchorForParagraph(key: string) {
    return `#paragraph-${key}`;
  }

  function paragraphKeyFromHash(hash: string) {
    const normalized = hash.startsWith('#') ? hash.slice(1) : hash;
    if (!normalized.startsWith('paragraph-')) {
      return null;
    }

    const key = normalized.slice('paragraph-'.length).trim();
    return key || null;
  }

  function updateSummaryFromCounts() {
    const paragraphCount = paragraphDescriptors.size;
    const discussedParagraphs = Array.from(paragraphCounts.values()).filter(count => count > 0).length;
    const approvedCount = Array.from(paragraphCounts.values()).reduce((sum, count) => sum + count, 0);

    if (paragraphCount === 0) {
      setSummaryMessage(t('paragraphComments.noParagraphs'));
      return;
    }

    if (!markersVisible) {
      setSummaryMessage(t('paragraphComments.markersHidden'));
      return;
    }

    setSummaryMessage(
      t('paragraphComments.summary', {
        paragraphCount,
        discussedCount: discussedParagraphs,
        approvedCount,
      })
    );
  }

  function updateMarkerState() {
    paragraphMarkers.forEach((marker, key) => {
      const count = paragraphCounts.get(key) || 0;
      const countNode = marker.querySelector('[data-marker-count]') as HTMLElement | null;
      const isActive = key === activeParagraphKey;

      marker.classList.toggle('has-comments', count > 0);
      marker.classList.toggle('is-active', isActive);
      marker.setAttribute(
        'aria-label',
        count > 0
          ? `${t('paragraphComments.badgeLabel')} (${countLabel(count)})`
          : t('paragraphComments.badgeLabel')
      );

      if (countNode) {
        countNode.textContent = markerCountText(count);
        countNode.classList.toggle('hidden', count <= 0);
      }
    });
  }

  function applyMarkerVisibility(visible: boolean, options?: { persist?: boolean }) {
    markersVisible = visible;

    if (articleRoot) {
      articleRoot.dataset.paragraphCommentsVisible = visible ? 'true' : 'false';
    }

    if (toggleButton) {
      toggleButton.setAttribute('aria-pressed', visible ? 'true' : 'false');
    }

    if (toggleLabel) {
      toggleLabel.textContent = visible
        ? t('paragraphComments.hideMarkers')
        : t('paragraphComments.showMarkers');
    }

    if (options?.persist !== false) {
      localStorage.setItem(storageKey, visible ? 'true' : 'false');
    }

    if (!visible) {
      closePanel(true);
    }

    updateSummaryFromCounts();
  }

  function createParagraphMarker(key: string, excerpt: string) {
    const marker = document.createElement('button');
    marker.type = 'button';
    marker.className = 'paragraph-comment-marker';
    marker.dataset.paragraphKey = key;
    marker.title = excerpt;
    marker.innerHTML = `
      <span class="paragraph-comment-marker-icon" aria-hidden="true">
        <i class="fas fa-message"></i>
      </span>
      <span class="paragraph-comment-marker-count hidden" data-marker-count></span>
    `;

    marker.addEventListener('click', () => {
      void openPanelForParagraph(key, { focusForm: true, syncHash: true });
    });

    return marker;
  }

  const panel = document.createElement('section');
  panel.className = 'paragraph-comment-panel terminal-panel hidden';
  panel.innerHTML = `
    <div class="paragraph-comment-panel-head">
      <div class="space-y-2">
        <span class="terminal-kicker">
          <i class="fas fa-terminal"></i>
          ${escapeHtml(t('paragraphComments.panelKicker'))}
        </span>
        <div>
          <h4 class="text-lg font-semibold text-[var(--title-color)]">${escapeHtml(t('paragraphComments.panelTitle'))}</h4>
          <p class="paragraph-comment-panel-excerpt text-sm leading-7 text-[var(--text-secondary)]"></p>
        </div>
      </div>
      <div class="paragraph-comment-panel-meta">
        <span class="terminal-chip" data-panel-count>${escapeHtml(t('paragraphComments.zeroNotes'))}</span>
        <span class="terminal-chip hidden" data-pending-count>0 ${escapeHtml(t('common.pending'))}</span>
        <button type="button" class="terminal-action-button" data-close-panel>
          <i class="fas fa-xmark"></i>
          <span>${escapeHtml(t('paragraphComments.close'))}</span>
        </button>
      </div>
    </div>

    <div class="paragraph-comment-thread" data-thread></div>

    <form class="paragraph-comment-form">
      <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
        <div>
          <label class="terminal-form-label">
            ${escapeHtml(t('paragraphComments.nickname'))} <span class="text-[var(--primary)]">*</span>
          </label>
          <input
            type="text"
            name="nickname"
            required
            placeholder="${escapeHtml(t('paragraphComments.nicknamePlaceholder'))}"
            class="terminal-form-input"
          />
        </div>
        <div>
          <label class="terminal-form-label">
            ${escapeHtml(t('paragraphComments.email'))} <span class="text-[var(--text-tertiary)] normal-case tracking-normal">(${escapeHtml(t('common.optional'))})</span>
          </label>
          <input
            type="email"
            name="email"
            placeholder="${escapeHtml(t('paragraphComments.emailPlaceholder'))}"
            class="terminal-form-input"
          />
        </div>
      </div>

      <div class="paragraph-comment-reply terminal-panel-muted hidden" data-reply-banner>
        <span class="text-sm text-[var(--text-secondary)]">
          ${escapeHtml(t('paragraphComments.replyTo'))} -> <span class="font-medium text-[var(--primary)]" data-reply-target></span>
        </span>
        <button type="button" class="terminal-action-button" data-cancel-reply>
          <i class="fas fa-rotate-left"></i>
          <span>${escapeHtml(t('common.clear'))}</span>
        </button>
      </div>

      <div>
        <label class="terminal-form-label">
          ${escapeHtml(t('paragraphComments.comment'))} <span class="text-[var(--primary)]">*</span>
        </label>
        <textarea
          name="content"
          required
          rows="5"
          maxlength="500"
          placeholder="${escapeHtml(t('paragraphComments.commentPlaceholder'))}"
          class="terminal-form-textarea resize-y"
        ></textarea>
        <p class="mt-2 text-right text-xs text-[var(--text-tertiary)]">${escapeHtml(t('paragraphComments.maxChars'))}</p>
      </div>

      <div class="hidden" aria-hidden="true">
        <label>
          Website
          <input type="text" name="website" tabindex="-1" autocomplete="off" />
        </label>
      </div>

      ${
        useTurnstile || useCaptcha
          ? `<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--header-bg)]/60 px-4 py-3">
               <div class="flex flex-wrap items-center justify-between gap-2">
                 <p class="text-xs font-semibold uppercase tracking-[0.18em] text-[var(--text-tertiary)]">${escapeHtml(t('common.humanVerification'))}</p>
                 ${
                   useTurnstile
                     ? `<span class="text-xs text-[var(--text-tertiary)]">Cloudflare Turnstile</span>`
                     : `<button type="button" class="terminal-action-button px-3 py-2 text-xs" data-refresh-captcha>
                          <i class="fas fa-rotate-right"></i>
                          <span>${escapeHtml(t('common.refresh'))}</span>
                        </button>`
                 }
               </div>
               ${
                 useTurnstile
                   ? `<div class="mt-3" data-turnstile-container></div>
                      <input type="hidden" name="turnstileToken" />
                      <p class="mt-3 text-sm text-[var(--text-secondary)]">${escapeHtml(t('common.turnstileHint'))}</p>`
                   : `<p class="mt-2 text-sm text-[var(--text-secondary)]" data-captcha-question>加载中...</p>
                      <input type="hidden" name="captchaToken" />
                      <input
                        type="text"
                        name="captchaAnswer"
                        required
                        inputmode="numeric"
                        placeholder="请输入上方答案"
                        class="mt-3 terminal-form-input"
                      />`
               }
             </div>`
          : ''
      }

      <div class="flex flex-wrap gap-3">
        <button type="submit" class="terminal-action-button terminal-action-button-primary">
          <i class="fas fa-paper-plane"></i>
          <span>${escapeHtml(t('common.submit'))}</span>
        </button>
        <button type="button" class="terminal-action-button" data-focus-paragraph>
          <i class="fas fa-crosshairs"></i>
          <span>${escapeHtml(t('paragraphComments.locateParagraph'))}</span>
        </button>
      </div>

      <div class="paragraph-comment-status hidden" data-status></div>
    </form>
  `;

  const panelExcerpt = panel.querySelector('.paragraph-comment-panel-excerpt') as HTMLElement;
  const panelCount = panel.querySelector('[data-panel-count]') as HTMLElement;
  const pendingCountChip = panel.querySelector('[data-pending-count]') as HTMLElement;
  const threadContainer = panel.querySelector('[data-thread]') as HTMLElement;
  const statusBox = panel.querySelector('[data-status]') as HTMLElement;
  const form = panel.querySelector('.paragraph-comment-form') as HTMLFormElement;
  const replyBanner = panel.querySelector('[data-reply-banner]') as HTMLElement;
  const replyTarget = panel.querySelector('[data-reply-target]') as HTMLElement;
  const focusButton = panel.querySelector('[data-focus-paragraph]') as HTMLButtonElement;
  const captchaQuestion = panel.querySelector('[data-captcha-question]') as HTMLElement;
  const refreshCaptchaButton = panel.querySelector('[data-refresh-captcha]') as HTMLButtonElement;
  const turnstileContainer = form.querySelector('[data-turnstile-container]') as HTMLElement | null;
  const turnstileTokenInput = form.querySelector('input[name="turnstileToken"]') as HTMLInputElement | null;
  const captchaTokenInput = form.querySelector('input[name="captchaToken"]') as HTMLInputElement | null;
  const captchaAnswerInput = form.querySelector('input[name="captchaAnswer"]') as HTMLInputElement | null;

  function clearStatus() {
    statusBox.className = 'paragraph-comment-status hidden';
    statusBox.textContent = '';
  }

  function setStatus(message: string, tone: 'success' | 'error' | 'info') {
    statusBox.className = `paragraph-comment-status paragraph-comment-status-${tone}`;
    statusBox.textContent = message;
  }

  async function loadCaptcha(showStatusOnError = true) {
    if (!captchaQuestion || !captchaTokenInput || !captchaAnswerInput) {
      return;
    }

    captchaQuestion.textContent = '加载中...';
    captchaTokenInput.value = '';
    captchaAnswerInput.value = '';

    try {
      const response = await fetch(`${apiBase}/comments/captcha`);
      if (!response.ok) {
        throw new Error(await response.text());
      }

      const payload = await response.json() as { token?: string; question?: string };
      captchaTokenInput.value = payload.token || '';
      captchaQuestion.textContent = payload.question || '请刷新验证码';
    } catch (error) {
      captchaQuestion.textContent = '验证码加载失败，请刷新重试';
      if (showStatusOnError) {
        setStatus(
          t('paragraphComments.submitFailed', {
            message: error instanceof Error ? error.message : t('common.unknownError'),
          }),
          'error'
        );
      }
    }
  }

  async function ensureTurnstile(showStatusOnError = true) {
    if (!turnstileSiteKey || !turnstileContainer || !turnstileTokenInput) {
      return;
    }

    turnstileTokenInput.value = '';

    if (turnstileWidget) {
      turnstileWidget.reset();
      return;
    }

    try {
      turnstileWidget = await mountTurnstile(turnstileContainer, {
        siteKey: turnstileSiteKey,
        onToken(token) {
          turnstileTokenInput.value = token;
        },
        onExpire() {
          turnstileTokenInput.value = '';
        },
        onError() {
          turnstileTokenInput.value = '';
          if (showStatusOnError) {
            setStatus(t('common.turnstileLoadFailed'), 'error');
          }
        },
      });
    } catch (error) {
      if (showStatusOnError) {
        setStatus(
          error instanceof Error ? error.message : t('common.turnstileLoadFailed'),
          'error',
        );
      }
    }
  }

  function resetHumanCheck() {
    if (useTurnstile) {
      turnstileTokenInput && (turnstileTokenInput.value = '');
      turnstileWidget?.reset();
      return;
    }

    if (useCaptcha) {
      void loadCaptcha(false);
    }
  }

  function resetReplyState() {
    activeReplyToCommentId = null;
    replyBanner.classList.add('hidden');
    replyTarget.textContent = '';
  }

  function setReplyState(commentId: number, author: string) {
    activeReplyToCommentId = commentId;
    replyBanner.classList.remove('hidden');
    replyTarget.textContent = author;
  }

  function commentCardMarkup(
    comment: {
      id: number | string;
      author: string | null;
      content: string | null;
      created_at: string;
      reply_to_comment_id: number | null;
    },
    options?: {
      pending?: boolean;
      replyAuthor?: string | null;
      replyPreview?: string | null;
    }
  ) {
    const author = comment.author || t('paragraphComments.anonymous');
    const pending = options?.pending || false;
    const replyAuthor = options?.replyAuthor;
    const replyPreview = options?.replyPreview;

    return `
      <article class="paragraph-thread-item ${replyAuthor ? 'is-reply' : ''} ${pending ? 'is-pending' : ''}">
        <div class="paragraph-thread-head">
          <div class="space-y-2">
            <div class="flex flex-wrap items-center gap-2">
              <span class="font-semibold text-[var(--title-color)]">${escapeHtml(author)}</span>
              <span class="terminal-chip px-2 py-1 text-[10px]">
                <i class="far fa-clock text-[var(--primary)]"></i>
                ${escapeHtml(formatCommentDate(comment.created_at))}
              </span>
              ${
                pending
                  ? `<span class="terminal-chip px-2 py-1 text-[10px]">
                      <i class="fas fa-hourglass-half text-[var(--warning)]"></i>
                      ${escapeHtml(t('paragraphComments.waitingReview'))}
                    </span>`
                  : ''
              }
              ${
                replyAuthor
                  ? `<span class="terminal-chip px-2 py-1 text-[10px]">
                      <i class="fas fa-reply text-[var(--primary)]"></i>
                      ${escapeHtml(replyAuthor)}
                    </span>`
                  : ''
              }
            </div>
            ${
              replyPreview
                ? `<p class="paragraph-thread-quote">${escapeHtml(replyPreview)}</p>`
                : ''
            }
          </div>
          ${
            pending
              ? ''
              : `<button
                  type="button"
                  class="terminal-action-button px-3 py-2 text-[11px]"
                  data-reply-id="${comment.id}"
                  data-reply-author="${escapeHtml(author)}"
                >
                  <i class="fas fa-reply"></i>
                  <span>${escapeHtml(t('common.reply'))}</span>
                </button>`
          }
        </div>
        <p class="text-sm leading-7 text-[var(--text-secondary)]">${escapeHtml(comment.content || '')}</p>
      </article>
    `;
  }

  function renderThread(paragraphKey: string, comments: BrowserComment[]) {
    const pending = pendingComments.get(paragraphKey) || [];
    const authorById = new Map(comments.map(comment => [comment.id, comment.author || t('paragraphComments.anonymous')]));
    const contentById = new Map(comments.map(comment => [comment.id, comment.content || '']));

    panelCount.textContent = countLabel(comments.length);
    paragraphCounts.set(paragraphKey, comments.length);
    pendingCountChip.textContent = `${pending.length} ${t('common.pending')}`;
    pendingCountChip.classList.toggle('hidden', pending.length === 0);
    updateMarkerState();
    updateSummaryFromCounts();

    const approvedMarkup =
      comments.length === 0
        ? `
          <div class="terminal-empty">
            <div class="mx-auto flex max-w-md flex-col items-center gap-3">
              <span class="terminal-section-icon">
                <i class="fas fa-comment-slash"></i>
              </span>
              <h5 class="text-base font-semibold text-[var(--title-color)]">${escapeHtml(t('paragraphComments.emptyTitle'))}</h5>
              <p class="text-sm leading-7 text-[var(--text-secondary)]">
                ${escapeHtml(t('paragraphComments.emptyDescription'))}
              </p>
            </div>
          </div>
        `
        : comments
            .map(comment =>
              commentCardMarkup(comment, {
                replyAuthor: comment.reply_to_comment_id
                  ? authorById.get(comment.reply_to_comment_id) || `#${comment.reply_to_comment_id}`
                  : null,
                replyPreview: comment.reply_to_comment_id
                  ? previewReplyText(contentById.get(comment.reply_to_comment_id) || null)
                  : null,
              })
            )
            .join('');

    const pendingMarkup =
      pending.length === 0
        ? ''
        : `
          <section class="paragraph-thread-segment">
            <div class="paragraph-thread-segment-label">${escapeHtml(t('paragraphComments.pendingQueue'))}</div>
            ${pending
              .map(comment =>
                commentCardMarkup(comment, {
                  pending: true,
                  replyAuthor: comment.reply_to_comment_id
                    ? authorById.get(comment.reply_to_comment_id) || `#${comment.reply_to_comment_id}`
                    : null,
                  replyPreview: comment.reply_to_comment_id
                    ? previewReplyText(contentById.get(comment.reply_to_comment_id) || null)
                    : null,
                })
              )
              .join('')}
          </section>
        `;

    threadContainer.innerHTML = `
      <section class="paragraph-thread-segment">
        <div class="paragraph-thread-segment-label">${escapeHtml(t('paragraphComments.approvedThread'))}</div>
        ${approvedMarkup}
      </section>
      ${pendingMarkup}
    `;
  }

  async function loadThread(paragraphKey: string, forceReload = false) {
    if (!forceReload && threadCache.has(paragraphKey)) {
      return threadCache.get(paragraphKey) || [];
    }

    const response = await fetch(
      `${apiBase}/comments?${new URLSearchParams({
        post_slug: postSlug,
        scope: 'paragraph',
        paragraph_key: paragraphKey,
        approved: 'true',
      }).toString()}`
    );

    if (!response.ok) {
      throw new Error(await response.text());
    }

    const comments = (await response.json()) as BrowserComment[];
    threadCache.set(paragraphKey, comments);
    return comments;
  }

  function syncHashForParagraph(key: string | null) {
    const nextUrl = new URL(window.location.href);
    nextUrl.hash = key ? anchorForParagraph(key) : '';
    history.replaceState(null, '', nextUrl.toString());
  }

  async function openPanelForParagraph(
    paragraphKey: string,
    options?: {
      focusForm?: boolean;
      forceReload?: boolean;
      syncHash?: boolean;
      scrollIntoView?: boolean;
    }
  ) {
    const descriptor = paragraphDescriptors.get(paragraphKey);

    if (!descriptor) {
      return;
    }

    if (!markersVisible) {
      applyMarkerVisibility(true, { persist: false });
    }

    activeParagraphKey = paragraphKey;
    clearStatus();
    resetReplyState();
    panelExcerpt.textContent = descriptor.excerpt;
    descriptor.element.insertAdjacentElement('afterend', panel);
    panel.classList.remove('hidden');
    panel.dataset.paragraphKey = paragraphKey;
    if (useTurnstile) {
      if (!turnstileTokenInput?.value) {
        await ensureTurnstile(false);
      }
    } else if (useCaptcha && !captchaTokenInput?.value) {
      await loadCaptcha(false);
    }

    paragraphDescriptors.forEach((item, key) => {
      item.element.classList.toggle('is-comment-focused', key === paragraphKey);
    });

    if (options?.syncHash !== false) {
      syncHashForParagraph(paragraphKey);
    }

    if (options?.scrollIntoView) {
      descriptor.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    updateMarkerState();
    threadContainer.innerHTML = `
      <div class="terminal-panel-muted text-sm text-[var(--text-secondary)]">
        ${escapeHtml(t('paragraphComments.loadingThread'))}
      </div>
    `;

    try {
      const comments = await loadThread(paragraphKey, options?.forceReload || false);
      renderThread(paragraphKey, comments);
      if (options?.focusForm) {
        form.querySelector('textarea')?.focus();
      }
    } catch (error) {
      panelCount.textContent = t('paragraphComments.loadFailedShort');
      pendingCountChip.classList.add('hidden');
      threadContainer.innerHTML = `
        <div class="paragraph-comment-status paragraph-comment-status-error">
          ${escapeHtml(t('paragraphComments.loadFailed', { message: error instanceof Error ? error.message : t('common.unknownError') }))}
        </div>
      `;
    }
  }

  function closePanel(clearHash = true) {
    activeParagraphKey = null;
    panel.classList.add('hidden');
    resetReplyState();
    clearStatus();
    paragraphDescriptors.forEach(item => item.element.classList.remove('is-comment-focused'));
    updateMarkerState();

    if (clearHash) {
      syncHashForParagraph(null);
    }
  }

  async function openFromHash() {
    const paragraphKey = paragraphKeyFromHash(window.location.hash);
    if (!paragraphKey) {
      if (activeParagraphKey) {
        closePanel(false);
      }
      return;
    }

    if (!paragraphDescriptors.has(paragraphKey)) {
      return;
    }

    await openPanelForParagraph(paragraphKey, {
      focusForm: false,
      forceReload: activeParagraphKey === paragraphKey,
      syncHash: false,
      scrollIntoView: true,
    });
  }

  panel.addEventListener('click', event => {
    const target = event.target as HTMLElement | null;
    const replyButton = target?.closest('[data-reply-id]') as HTMLButtonElement | null;
    const closeButton = target?.closest('[data-close-panel]') as HTMLButtonElement | null;
    const cancelReplyButton = target?.closest('[data-cancel-reply]') as HTMLButtonElement | null;

    if (replyButton) {
      const replyId = Number(replyButton.dataset.replyId || '0');
      const author = replyButton.dataset.replyAuthor || t('paragraphComments.anonymous');
      if (replyId > 0) {
        setReplyState(replyId, author);
        form.querySelector('textarea')?.focus();
      }
      return;
    }

    if (closeButton) {
      closePanel(true);
      return;
    }

    if (cancelReplyButton) {
      resetReplyState();
    }
  });

  focusButton.addEventListener('click', () => {
    if (!activeParagraphKey) {
      return;
    }

    const descriptor = paragraphDescriptors.get(activeParagraphKey);
    descriptor?.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
  });

  refreshCaptchaButton?.addEventListener('click', () => {
    void loadCaptcha(false);
  });

  form.addEventListener('submit', async event => {
    event.preventDefault();

    if (!activeParagraphKey) {
      setStatus(t('paragraphComments.selectedRequired'), 'error');
      return;
    }

    const descriptor = paragraphDescriptors.get(activeParagraphKey);
    if (!descriptor) {
      setStatus(t('paragraphComments.contextMissing'), 'error');
      return;
    }

    const formData = new FormData(form);
    clearStatus();
    setStatus(t('paragraphComments.submitting'), 'info');

    if (useTurnstile) {
      const token = String(formData.get('turnstileToken') || '').trim();
      if (!token) {
        setStatus(t('common.turnstileRequired'), 'error');
        return;
      }
    }

    try {
      const response = await fetch(`${apiBase}/comments`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          postSlug,
          nickname: formData.get('nickname'),
          email: formData.get('email'),
          content: formData.get('content'),
          scope: 'paragraph',
          paragraphKey: descriptor.key,
          paragraphExcerpt: descriptor.excerpt,
          replyToCommentId: activeReplyToCommentId,
          turnstileToken: formData.get('turnstileToken'),
          captchaToken: formData.get('captchaToken'),
          captchaAnswer: formData.get('captchaAnswer'),
          website: formData.get('website'),
        }),
      });

      if (!response.ok) {
        throw new Error(await response.text());
      }

      const pending = pendingComments.get(descriptor.key) || [];
      pendingCounter += 1;
      pending.push({
        id: `pending-${pendingCounter}`,
        author: String(formData.get('nickname') || t('paragraphComments.anonymous')),
        content: String(formData.get('content') || ''),
        created_at: new Date().toISOString(),
        reply_to_comment_id: activeReplyToCommentId,
      });
      pendingComments.set(descriptor.key, pending);

      form.reset();
      resetReplyState();
      const approvedComments = await loadThread(descriptor.key, false);
      renderThread(descriptor.key, approvedComments);
      setStatus(t('paragraphComments.submitSuccess'), 'success');
      resetHumanCheck();
    } catch (error) {
      setStatus(t('paragraphComments.submitFailed', { message: error instanceof Error ? error.message : t('common.unknownError') }), 'error');
      resetHumanCheck();
    }
  });

  toggleButton?.addEventListener('click', () => {
    applyMarkerVisibility(!markersVisible);
  });

  async function init() {
    if (!wrapper || !articleRoot || !articleContent || !postSlug) {
      return;
    }

    const storedVisibility = localStorage.getItem(storageKey);
    markersVisible = storedVisibility !== 'false';

    const descriptors = buildParagraphDescriptors(articleContent);
    if (descriptors.length === 0) {
      setSummaryMessage(t('paragraphComments.noParagraphs'));
      return;
    }

    descriptors.forEach(descriptor => {
      paragraphDescriptors.set(descriptor.key, descriptor);
      descriptor.element.id = `paragraph-${descriptor.key}`;
      descriptor.element.dataset.paragraphKey = descriptor.key;
      descriptor.element.classList.add('paragraph-comment-paragraph');

      const marker = createParagraphMarker(descriptor.key, descriptor.excerpt);
      paragraphMarkers.set(descriptor.key, marker);
      descriptor.element.appendChild(marker);
    });

    try {
      const response = await fetch(
        `${apiBase}/comments/paragraphs/summary?${new URLSearchParams({ post_slug: postSlug }).toString()}`
      );

      if (!response.ok) {
        throw new Error(await response.text());
      }

      const summary = (await response.json()) as SummaryItem[];
      summary.forEach(item => {
        paragraphCounts.set(item.paragraph_key, item.count);
      });
    } catch (error) {
      console.error('Failed to load paragraph comment summary:', error);
    }

    descriptors.forEach(descriptor => {
      paragraphCounts.set(descriptor.key, paragraphCounts.get(descriptor.key) || 0);
    });

    updateMarkerState();
    applyMarkerVisibility(markersVisible, { persist: false });
    if (useTurnstile) {
      await ensureTurnstile(false);
    } else if (useCaptcha) {
      await loadCaptcha(false);
    }
    await openFromHash();
    window.addEventListener('hashchange', () => {
      void openFromHash();
    });
  }

  void init();
  }
</script>
