import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import type {
  ArticleDetailResponse,
  ArticleVersionDetailResponse,
  ArticleVersionEntry,
} from '../../service/visibility/visibility.interface';
import {
  deleteArticle,
  getArticle,
  getArticleVersion,
  getArticleVersions,
  patchArticle,
  publishArticle,
  regenerateArticle,
  unpublishArticle,
} from '../../service/visibility/visibility.service';
import { LANGUAGE_OPTIONS } from '../../data/locales';
import { RESOURCE_TYPE_ARTICLE_SUGGESTION } from '../../service/media/media.interface';
import type { MediaResponse } from '../../service/media/media.interface';
import { listMediaForResource } from '../../service/media/media.service';
import { deleteArticleTexts } from '../../utils/confirmTexts';
import ConfirmDialog from '../ui/ConfirmDialog';

import { editedByToTone, normaliseFaq, toneToBadgeClass } from './helpers';
import ImageSuggestionCard from './ImageSuggestionCard';
import MarkdownView, {
  extractHeadings,
  markdownToHtml,
  type TocEntry,
} from './MarkdownView';
import Modal from './Modal';

interface ArticleModalProps {
  open: boolean;
  articleId: string | null;
  onClose: () => void;
  clientId: string;
  token: string;
  /**
   * Invoked after the merchant hard-deletes this article. Parent should drop
   * it from its local article list; the modal also calls ``onClose`` itself.
   */
  onDeleted?: (articleId: string) => void;
  /**
   * Invoked after the merchant flips this article between ``ready`` and
   * ``published``. Parent should patch its local article list with the
   * updated summary so the Recommended/Published tabs re-balance.
   */
  onPublishedChanged?: (article: ArticleDetailResponse) => void;
}

/** Slug used for the meta description pseudo-section at the top. */
const SLUG_META = 'article-overview';

/** Code -> human label for the language-version switcher chips. */
const LANGUAGE_LABEL_BY_CODE: Map<string, string> = new Map(
  LANGUAGE_OPTIONS.map(option => [option.value, option.label])
);

/**
 * Human-readable language name for a code, falling back to the upper-cased
 * code so an unknown language still renders a sensible chip.
 *
 * @param code {string} ISO 639-1 language code.
 * @returns {string} The display label.
 */
const languageLabel = (code: string): string =>
  LANGUAGE_LABEL_BY_CODE.get(code) ?? code.toUpperCase();

/** Hard cap mirrors the backend ``ArticleRegenerateRequest`` schema. */
const REGEN_INSTRUCTIONS_MAX = 2000;

/** Auto-poll cadence (ms) while an article is in pending/generating state. */
const POLL_INTERVAL_MS = 2000;

/** Time-to-live for inline copy/save feedback messages (ms). */
const FEEDBACK_TIMEOUT_MS = 2400;

const REGEN_PLACEHOLDER =
  'Optional - tell the model what to change. Examples:\n' +
  '• Rewrite as a listicle with 7 numbered picks.\n' +
  '• Keep the article as-is, but never use the word "premium".\n' +
  '• Shift to a casual, second-person tone.\n' +
  '• Add a comparison section between brand A and brand B.';

/**
 * Full article viewer with two tabs:
 *   - **Current**: TOC sidebar + markdown body + FAQ + target prompts
 *   - **Versions**: timeline of edits + selected version preview
 */
const ArticleModal = ({
  open,
  articleId,
  onClose,
  clientId,
  token,
  onDeleted,
  onPublishedChanged,
}: ArticleModalProps): JSX.Element | null => {
  const [article, setArticle] = useState<ArticleDetailResponse | null>(null);
  // The article actually shown. Defaults to the ``articleId`` the parent
  // opened the modal with, but the language-version switcher swaps it for a
  // sibling translation without touching the parent's prop.
  const [activeArticleId, setActiveArticleId] = useState<string | null>(
    articleId
  );
  const [mediaBySuggestion, setMediaBySuggestion] = useState<
    Record<string, MediaResponse>
  >({});
  const [mediaError, setMediaError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [activeSlug, setActiveSlug] = useState<string | null>(null);
  const [copyFeedback, setCopyFeedback] = useState<string | null>(null);
  const [regenerateBusy, setRegenerateBusy] = useState(false);
  const [regenerateOpen, setRegenerateOpen] = useState(false);
  const [regenerateInstructions, setRegenerateInstructions] = useState('');
  const [selectedTab, setSelectedTab] = useState(0);
  const [deleteBusy, setDeleteBusy] = useState(false);
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [publishBusy, setPublishBusy] = useState(false);

  const [editing, setEditing] = useState(false);
  const [editDraft, setEditDraft] = useState('');
  const [saveBusy, setSaveBusy] = useState(false);

  const [versionsLoading, setVersionsLoading] = useState(false);
  const [versionsError, setVersionsError] = useState<string | null>(null);
  const [versions, setVersions] = useState<ArticleVersionEntry[]>([]);
  const [selectedVersionId, setSelectedVersionId] = useState<string | null>(
    null
  );
  const [versionDetail, setVersionDetail] =
    useState<ArticleVersionDetailResponse | null>(null);
  const [versionDetailLoading, setVersionDetailLoading] = useState(false);

  const contentPaneRef = useRef<HTMLDivElement | null>(null);

  const showFeedback = useCallback((message: string) => {
    setCopyFeedback(message);
    window.setTimeout(() => setCopyFeedback(null), FEEDBACK_TIMEOUT_MS);
  }, []);

  const loadArticle = useCallback(async () => {
    if (!activeArticleId) return;
    setLoading(true);
    setError(null);
    try {
      const data = await getArticle(clientId, token, activeArticleId);
      setArticle(data);
    } catch (loadError) {
      setError(
        loadError instanceof Error
          ? loadError.message
          : 'Failed to load article.'
      );
    } finally {
      setLoading(false);
    }
  }, [activeArticleId, clientId, token]);

  // When the parent opens the modal (or swaps which article it points at),
  // reset the active article to the parent's choice so a previous in-modal
  // language switch doesn't leak across opens.
  useEffect(() => {
    setActiveArticleId(articleId);
  }, [articleId]);

  useEffect(() => {
    if (open && activeArticleId) {
      loadArticle();
    }
    if (!open) {
      setArticle(null);
      setError(null);
      setActiveSlug(null);
      setCopyFeedback(null);
      setSelectedTab(0);
      setVersions([]);
      setSelectedVersionId(null);
      setVersionDetail(null);
      setVersionsError(null);
      setEditing(false);
      setEditDraft('');
      setRegenerateOpen(false);
      setRegenerateInstructions('');
      setMediaBySuggestion({});
      setMediaError(null);
    }
  }, [open, activeArticleId, loadArticle]);

  // Switching to a sibling language version reuses the open modal, so clear
  // the per-article transient panels (versions, edit draft, media) that would
  // otherwise carry over from the article we just left.
  useEffect(() => {
    if (!open) return;
    setActiveSlug(null);
    setSelectedTab(0);
    setVersions([]);
    setSelectedVersionId(null);
    setVersionDetail(null);
    setVersionsError(null);
    setEditing(false);
    setEditDraft('');
    setRegenerateOpen(false);
    setRegenerateInstructions('');
    setMediaBySuggestion({});
    setMediaError(null);
  }, [activeArticleId]);

  useEffect(() => {
    if (!open || !activeArticleId || !clientId || !token) return;
    let cancelled: boolean = false;
    listMediaForResource(
      clientId,
      token,
      RESOURCE_TYPE_ARTICLE_SUGGESTION,
      activeArticleId
    )
      .then((rows: MediaResponse[]) => {
        if (cancelled) return;
        const map: Record<string, MediaResponse> = {};
        for (const row of rows) map[row.suggestion_id] = row;
        setMediaBySuggestion(map);
      })
      .catch(listError => {
        if (!cancelled)
          console.warn('[ArticleModal] listMediaForResource failed', listError);
      });

    return () => {
      cancelled = true;
    };
  }, [open, activeArticleId, clientId, token]);

  const handleMediaChanged = useCallback((fresh: MediaResponse): void => {
    setMediaBySuggestion(previous => ({
      ...previous,
      [fresh.suggestion_id]: fresh,
    }));
  }, []);

  const handleMediaDeleted = useCallback((suggestionId: string): void => {
    setMediaBySuggestion(previous => {
      const next: Record<string, MediaResponse> = { ...previous };
      delete next[suggestionId];

      return next;
    });
  }, []);

  /**
   * Auto-poll while the article is in a generating state - typical regen
   * lifecycle is ``pending`` → ``generating`` → ``ready``. Polling stops
   * automatically once status leaves the in-flight window.
   */
  const articleStatus = article?.summary.status;
  useEffect(() => {
    if (!open || !activeArticleId) return;
    if (articleStatus !== 'pending' && articleStatus !== 'generating') return;
    let cancelled = false;
    const pollOnce = async () => {
      if (cancelled) return;
      try {
        const fresh = await getArticle(clientId, token, activeArticleId);
        if (cancelled) return;
        setArticle(fresh);
        if (fresh.summary.status === 'ready') {
          showFeedback('Article ready.');
        } else if (fresh.summary.status === 'failed') {
          showFeedback('Regeneration failed - try again.');
        }
      } catch {
        /* transient errors shouldn't kill polling */
      }
    };
    const intervalHandle = window.setInterval(pollOnce, POLL_INTERVAL_MS);
    return () => {
      cancelled = true;
      window.clearInterval(intervalHandle);
    };
  }, [open, articleStatus, activeArticleId, clientId, token, showFeedback]);

  const isGenerating =
    articleStatus === 'pending' || articleStatus === 'generating';
  const isPublished = articleStatus === 'published';
  // Publish is reversible (ready → published → ready), but only those two
  // states can transition: pending/generating/failed have nothing to flip.
  const canTogglePublish =
    articleStatus === 'ready' || articleStatus === 'published';
  const publishLabel = isPublished
    ? 'Mark as not published'
    : 'Mark as published';

  useEffect(() => {
    if (!open || !activeArticleId || selectedTab !== 1) return;
    if (versions.length > 0 || versionsLoading) return;
    setVersionsLoading(true);
    setVersionsError(null);
    getArticleVersions(clientId, token, activeArticleId)
      .then(response => {
        setVersions(response.versions || []);
        const firstVersion = response.versions?.[0];
        if (firstVersion) setSelectedVersionId(firstVersion.version_id);
      })
      .catch(versionsLoadError => {
        setVersionsError(
          versionsLoadError instanceof Error
            ? versionsLoadError.message
            : 'Failed to load versions.'
        );
      })
      .finally(() => setVersionsLoading(false));
  }, [
    open,
    activeArticleId,
    selectedTab,
    clientId,
    token,
    versions.length,
    versionsLoading,
  ]);

  useEffect(() => {
    if (!activeArticleId || !selectedVersionId) {
      setVersionDetail(null);
      return;
    }
    let cancelled = false;
    setVersionDetailLoading(true);
    getArticleVersion(clientId, token, activeArticleId, selectedVersionId)
      .then(detail => {
        if (cancelled) return;
        setVersionDetail(detail);
      })
      .catch(versionLoadError => {
        if (cancelled) return;
        setVersionsError(
          versionLoadError instanceof Error
            ? versionLoadError.message
            : 'Failed to load the selected version.'
        );
      })
      .finally(() => {
        if (!cancelled) setVersionDetailLoading(false);
      });

    return () => {
      cancelled = true;
    };
  }, [activeArticleId, selectedVersionId, clientId, token]);

  const faq = useMemo(() => normaliseFaq(article?.faq), [article]);

  const tableOfContents: TocEntry[] = useMemo(() => {
    if (!article) return [];
    const headings = extractHeadings(article.markdown ?? '');
    const title = article.h1 || article.summary.title;
    const base: TocEntry[] = title
      ? [{ level: 0, text: title, slug: SLUG_META }, ...headings]
      : headings;
    const extras: TocEntry[] = [];
    if (faq.length > 0)
      extras.push({ level: 0, text: 'FAQ', slug: 'faq-section' });
    if (
      Array.isArray(
        (article.outline as { sections?: unknown } | null)?.sections
      ) &&
      ((article.outline as { sections: unknown[] }).sections.length ?? 0) > 0
    )
      extras.push({ level: 0, text: 'Outline', slug: 'outline' });
    if (
      Array.isArray(article.image_suggestions) &&
      article.image_suggestions.length > 0
    )
      extras.push({
        level: 0,
        text: 'Image suggestions',
        slug: 'image-suggestions',
      });
    if (
      Array.isArray(article.internal_links) &&
      article.internal_links.length > 0
    )
      extras.push({ level: 0, text: 'Internal links', slug: 'internal-links' });
    if (
      Array.isArray(article.external_refs) &&
      article.external_refs.length > 0
    )
      extras.push({
        level: 0,
        text: 'External references',
        slug: 'external-refs',
      });
    if (article.schema_article_jsonld || article.schema_faq_jsonld)
      extras.push({
        level: 0,
        text: 'Schema (JSON-LD)',
        slug: 'schema-jsonld',
      });
    return [...base, ...extras];
  }, [article, faq]);

  const scrollToSlug = (slug: string) => {
    const contentPane = contentPaneRef.current;
    if (!contentPane) return;
    const target = contentPane.querySelector<HTMLElement>(
      `#${CSS.escape(slug)}`
    );
    if (target) {
      target.scrollIntoView({ behavior: 'smooth', block: 'start' });
      setActiveSlug(slug);
    }
  };

  useEffect(() => {
    if (!open || !article) return;
    const contentPane = contentPaneRef.current;
    if (!contentPane) return;
    const observer = new IntersectionObserver(
      entries => {
        for (const entry of entries) {
          if (entry.isIntersecting && entry.target.id) {
            setActiveSlug(entry.target.id);
            break;
          }
        }
      },
      { root: contentPane, rootMargin: '0px 0px -70% 0px', threshold: 0 }
    );
    contentPane.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(headingEl => {
      if (headingEl.id) observer.observe(headingEl);
    });
    const overviewEl = contentPane.querySelector(`#${SLUG_META}`);
    if (overviewEl) observer.observe(overviewEl);
    [
      'faq-section',
      'outline',
      'image-suggestions',
      'internal-links',
      'external-refs',
      'schema-jsonld',
    ].forEach(id => {
      const sectionEl = contentPane.querySelector(`#${id}`);
      if (sectionEl) observer.observe(sectionEl);
    });

    return () => observer.disconnect();
  }, [open, article, tableOfContents]);

  /**
   * Best-effort clipboard write with a hidden-textarea fallback for
   * environments where the async API is unavailable (older webviews,
   * embedded iframes with restricted permissions).
   */
  const writeToClipboard = async (value: string): Promise<boolean> => {
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(value);
        return true;
      }
    } catch {
      /* fall through to the textarea fallback */
    }
    try {
      const fallbackTextarea = document.createElement('textarea');
      fallbackTextarea.value = value;
      fallbackTextarea.setAttribute('readonly', '');
      fallbackTextarea.style.position = 'absolute';
      fallbackTextarea.style.left = '-9999px';
      document.body.appendChild(fallbackTextarea);
      fallbackTextarea.select();
      document.execCommand('copy');
      document.body.removeChild(fallbackTextarea);
      return true;
    } catch {
      return false;
    }
  };

  const handleCopyHtml = async () => {
    if (!article?.markdown) return;
    const html = markdownToHtml(article.markdown);
    const success = await writeToClipboard(html);
    showFeedback(
      success
        ? 'HTML copied - paste into the WordPress post body.'
        : "Couldn't copy - select manually."
    );
  };

  const handleCopyText = async () => {
    if (!article?.markdown) return;
    const html = markdownToHtml(article.markdown);
    const tmp = document.createElement('div');
    tmp.innerHTML = html;
    const plain = (tmp as HTMLElement).innerText ?? tmp.textContent ?? '';
    const ok = await writeToClipboard(plain);
    showFeedback(
      ok ? 'Plain text copied.' : "Couldn't copy — select manually."
    );
  };

  const handleCopyJsonLd = async (value: string, label: string) => {
    const ok = await writeToClipboard(value);
    showFeedback(ok ? `${label} copied.` : "Couldn't copy - select manually.");
  };

  const handleStartEdit = () => {
    if (!article?.markdown) return;
    setEditDraft(article.markdown);
    setEditing(true);
  };

  const handleCancelEdit = () => {
    setEditing(false);
    setEditDraft('');
  };

  const handleSaveEdit = async () => {
    if (!article) return;
    if (editDraft === article.markdown) {
      setEditing(false);
      return;
    }
    setSaveBusy(true);
    try {
      const updated = await patchArticle(
        clientId,
        token,
        article.summary.article_id,
        { markdown: editDraft }
      );
      setArticle(updated);
      setEditing(false);
      setEditDraft('');
      // Force the versions tab to refetch so the new version row shows up.
      setVersions([]);
      showFeedback('Edits saved - a new version was recorded.');
    } catch (saveError) {
      showFeedback(
        saveError instanceof Error ? saveError.message : 'Failed to save edits.'
      );
    } finally {
      setSaveBusy(false);
    }
  };

  const handleRegenerate = async () => {
    if (!article) return;
    setRegenerateBusy(true);
    try {
      const instructions = regenerateInstructions.trim();
      await regenerateArticle(
        clientId,
        token,
        article.summary.article_id,
        instructions ? { instructions } : undefined
      );
      // Optimistically flip status so the polling effect picks up
      // immediately, even before the backend has flipped to "generating".
      setArticle(previous =>
        previous
          ? {
              ...previous,
              summary: { ...previous.summary, status: 'generating' },
            }
          : previous
      );
      showFeedback(
        instructions
          ? 'Regeneration started with your directive.'
          : 'Regeneration started.'
      );
      setRegenerateOpen(false);
      setRegenerateInstructions('');
    } catch (regenerateError) {
      showFeedback(
        regenerateError instanceof Error
          ? regenerateError.message
          : 'Regenerate failed.'
      );
    } finally {
      setRegenerateBusy(false);
    }
  };

  /**
   * Toggle the article between ``ready`` and ``published`` on the backend
   * and patch the modal's local copy with the response. Forwards the
   * updated detail to the parent so the Recommended/Published tabs
   * re-balance without a full refetch.
   */
  const handlePublishToggle = async () => {
    if (!article) return;
    const wasPublished = article.summary.status === 'published';
    setPublishBusy(true);
    try {
      const updated = wasPublished
        ? await unpublishArticle(clientId, token, article.summary.article_id)
        : await publishArticle(clientId, token, article.summary.article_id);
      setArticle(updated);
      onPublishedChanged?.(updated);
      showFeedback(
        wasPublished ? 'Marked as not published.' : 'Marked as published.'
      );
    } catch (err) {
      showFeedback(
        err instanceof Error ? err.message : 'Failed to update status.'
      );
    } finally {
      setPublishBusy(false);
    }
  };

  /**
   * Confirm and hard-delete the currently-loaded article. On success
   * notifies the parent via ``onDeleted`` and closes the modal; on
   * failure surfaces the error in the inline copy/feedback strip.
   *
   * @returns {Promise<void>} Resolves once the delete request and modal teardown have finished.
   */
  const handleDelete = async () => {
    if (!article) return;
    setDeleteConfirmOpen(false);
    setDeleteBusy(true);
    try {
      await deleteArticle(clientId, token, article.summary.article_id);
      onDeleted?.(article.summary.article_id);
      onClose();
    } catch (deleteError) {
      showFeedback(
        deleteError instanceof Error
          ? deleteError.message
          : 'Failed to delete article.'
      );
    } finally {
      setDeleteBusy(false);
    }
  };

  const deleteTitle = article
    ? article.summary.title || article.h1 || 'this article'
    : '';

  const handleCopyVersionHtml = async () => {
    if (!versionDetail?.markdown) return;
    const html = markdownToHtml(versionDetail.markdown);
    const success = await writeToClipboard(html);
    showFeedback(
      success
        ? 'HTML copied - paste into the WordPress post body.'
        : "Couldn't copy - select manually."
    );
  };

  const handleCopyVersionText = async () => {
    if (!versionDetail?.markdown) return;
    const html = markdownToHtml(versionDetail.markdown);
    const tmp = document.createElement('div');
    tmp.innerHTML = html;
    const plain = (tmp as HTMLElement).innerText ?? tmp.textContent ?? '';
    const ok = await writeToClipboard(plain);
    showFeedback(
      ok ? 'Plain text copied.' : "Couldn't copy - select manually."
    );
  };

  const statsLine = useMemo(() => {
    if (!article) return '';
    const parts: string[] = [];
    if (article.summary.word_count) {
      parts.push(`${article.summary.word_count} words`);
    }
    if (article.summary.seo_score) {
      parts.push(`SEO ${article.summary.seo_score}/100`);
    }
    if (article.summary.aeo_score) {
      parts.push(`AEO ${article.summary.aeo_score}/100`);
    }
    return parts.join(' · ');
  }, [article]);

  if (!open) return null;

  const renderCurrentTab = (): JSX.Element => (
    <div
      className="grid gap-6"
      style={{
        gridTemplateColumns: 'minmax(200px, 240px) 1fr',
        minHeight: '60vh',
      }}
    >
      <aside
        className="border-r border-gray-200 pr-4 sticky top-0 self-start overflow-y-auto"
        style={{ maxHeight: '75vh' }}
      >
        <h3 className="text-sm font-semibold text-gray-900">Contents</h3>
        {loading && !article ? (
          <div className="mt-3 animate-pulse space-y-2">
            <div className="h-3 bg-gray-200 rounded w-full" />
            <div className="h-3 bg-gray-200 rounded w-10/12" />
            <div className="h-3 bg-gray-200 rounded w-8/12" />
            <div className="h-3 bg-gray-200 rounded w-9/12" />
          </div>
        ) : tableOfContents.length === 0 ? (
          <p className="mt-3 text-xs text-gray-500">No headings yet.</p>
        ) : (
          <nav className="mt-3 flex flex-col gap-1">
            {tableOfContents.map(entry => {
              const isActive = activeSlug === entry.slug;
              const showLevelBadge = entry.level >= 1;
              const levelLabel = entry.level === 0 ? '' : `H${entry.level}`;
              const levelBadgeColor =
                entry.level === 1
                  ? '#B7007C'
                  : entry.level === 2
                    ? '#2c6ecb'
                    : entry.level === 3
                      ? '#3f7b00'
                      : '#8c9196';

              return (
                <button
                  key={entry.slug}
                  type="button"
                  onClick={() => scrollToSlug(entry.slug)}
                  style={{
                    display: 'flex',
                    alignItems: 'flex-start',
                    gap: 8,
                    textAlign: 'left',
                    padding: '6px 8px',
                    paddingLeft: 8 + Math.max(0, entry.level - 1) * 12,
                    border: 'none',
                    background: isActive ? '#fff1f8' : 'transparent',
                    color: isActive ? '#B7007C' : '#202223',
                    borderLeft: isActive
                      ? '3px solid #B7007C'
                      : '3px solid transparent',
                    cursor: 'pointer',
                    borderRadius: 4,
                    fontSize:
                      entry.level <= 1 ? 14 : entry.level === 2 ? 13 : 12,
                    fontWeight:
                      entry.level <= 1 ? 600 : entry.level === 2 ? 500 : 400,
                    lineHeight: 1.35,
                    width: '100%',
                  }}
                >
                  {showLevelBadge && (
                    <span
                      style={{
                        flexShrink: 0,
                        fontSize: 10,
                        fontWeight: 700,
                        padding: '1px 6px',
                        borderRadius: 4,
                        background: `${levelBadgeColor}1a`,
                        color: levelBadgeColor,
                        letterSpacing: 0.4,
                        marginTop: 2,
                        fontFamily:
                          'ui-monospace, SFMono-Regular, Menlo, monospace',
                      }}
                      aria-hidden="true"
                    >
                      {levelLabel}
                    </span>
                  )}
                  <span style={{ flex: 1, minWidth: 0 }}>{entry.text}</span>
                </button>
              );
            })}
          </nav>
        )}
      </aside>

      <div
        ref={contentPaneRef}
        className="overflow-y-auto pr-2"
        style={{ maxHeight: '75vh' }}
      >
        {loading && !article ? (
          <div className="flex flex-col gap-4 animate-pulse">
            <div className="h-8 bg-gray-200 rounded w-3/4" />
            <div className="h-3 bg-gray-200 rounded w-full" />
            <div className="h-3 bg-gray-200 rounded w-11/12" />
            <div className="h-3 bg-gray-200 rounded w-10/12" />
          </div>
        ) : article ? (
          <div className="flex flex-col gap-4">
            <div id={SLUG_META} style={{ scrollMarginTop: 16 }}>
              <div className="flex flex-col gap-2">
                <div className="flex items-center gap-2 flex-wrap">
                  <span
                    className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${toneToBadgeClass(article.summary.status === 'ready' ? 'success' : 'attention')}`}
                  >
                    {article.summary.status}
                  </span>
                  <span
                    className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${toneToBadgeClass(undefined)}`}
                  >
                    {article.summary.language.toUpperCase()}
                  </span>
                  {statsLine && (
                    <span className="text-xs text-gray-500">{statsLine}</span>
                  )}
                </div>
                <h1 className="text-3xl font-bold text-gray-900">
                  {article.h1 || article.summary.title}
                </h1>
                {article.meta_description && (
                  <p className="text-base text-gray-500">
                    {article.meta_description}
                  </p>
                )}
                {Array.isArray(article.language_versions) &&
                  article.language_versions.length > 1 && (
                    <div className="flex flex-wrap items-center gap-2 pt-1">
                      <span className="text-xs text-gray-500">
                        Language version:
                      </span>
                      {article.language_versions.map(version => {
                        const isCurrent =
                          version.article_id === article.summary.article_id;

                        return isCurrent ? (
                          <span
                            key={version.article_id}
                            aria-current="true"
                            className="inline-flex items-center rounded-full bg-pink-600 px-2.5 py-1 text-xs font-medium text-white"
                          >
                            {languageLabel(version.language)}
                          </span>
                        ) : (
                          <button
                            key={version.article_id}
                            type="button"
                            onClick={() =>
                              setActiveArticleId(version.article_id)
                            }
                            className="inline-flex items-center rounded-full border border-gray-300 px-2.5 py-1 text-xs text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
                          >
                            {languageLabel(version.language)}
                            {version.status !== 'ready' &&
                            version.status !== 'published'
                              ? ' (…)'
                              : ''}
                          </button>
                        );
                      })}
                    </div>
                  )}
              </div>
            </div>

            {editing ? (
              <div className="flex flex-col gap-1">
                <textarea
                  value={editDraft}
                  onChange={event => setEditDraft(event.target.value)}
                  disabled={saveBusy}
                  spellCheck
                  className="w-full rounded-md border border-gray-300 bg-white p-3 font-mono text-[13px] leading-relaxed text-gray-900 focus:border-pink-600 focus:outline-none focus:ring-1 focus:ring-pink-600 disabled:opacity-50"
                  style={{
                    minHeight: 320,
                    height: `${Math.min(640, Math.max(320, (editDraft.split('\n').length + 2) * 18))}px`,
                  }}
                />
                <p className="text-xs text-gray-500">
                  Edits are stored as a new version with edit author
                  &quot;user&quot; - the previous text stays browsable in the
                  Versions tab.
                </p>
              </div>
            ) : article.markdown ? (
              <MarkdownView source={article.markdown} />
            ) : (
              <p className="text-sm text-gray-500">
                Article body isn&apos;t available yet. Regenerate or wait for
                the job to finish.
              </p>
            )}

            {faq.length > 0 && (
              <div
                id="faq-section"
                className="border-t border-gray-200 pt-6"
                style={{ scrollMarginTop: 16 }}
              >
                <div className="flex flex-col gap-3">
                  <h2 className="text-lg font-semibold text-gray-900">FAQ</h2>
                  <div className="flex flex-col gap-2">
                    {faq.map((entry, entryIndex) => (
                      <div key={entryIndex} className="flex flex-col gap-1">
                        {entry.question && (
                          <p className="text-sm font-semibold text-gray-900">
                            {entry.question}
                          </p>
                        )}
                        {entry.answer && (
                          <p className="text-sm text-gray-700">
                            {entry.answer}
                          </p>
                        )}
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            )}

            {Array.isArray(
              (article.outline as { sections?: unknown } | null)?.sections
            ) &&
              ((article.outline as { sections: unknown[] }).sections.length ??
                0) > 0 && (
                <div
                  id="outline"
                  className="border-t border-gray-200 pt-6"
                  style={{ scrollMarginTop: 16 }}
                >
                  <div className="flex flex-col gap-3">
                    <h2 className="text-lg font-semibold text-gray-900">
                      Outline
                    </h2>
                    <p className="text-xs text-gray-500">
                      Section blueprint with key points and the target question
                      each H2 is meant to answer.
                    </p>
                    <div className="flex flex-col gap-2">
                      {(
                        article.outline as {
                          sections: Array<{
                            h2?: string;
                            key_points?: string[];
                            target_question?: string;
                          }>;
                        }
                      ).sections.map((outlineSection, outlineIdx) => (
                        <div
                          key={outlineIdx}
                          className="flex flex-col gap-2 rounded-lg border border-gray-200 bg-white p-4"
                        >
                          {outlineSection.h2 && (
                            <p className="text-sm font-semibold text-gray-900">
                              {outlineSection.h2}
                            </p>
                          )}
                          {outlineSection.target_question && (
                            <p className="text-xs text-gray-500">
                              Target question:{' '}
                              <span className="text-gray-900">
                                {outlineSection.target_question}
                              </span>
                            </p>
                          )}
                          {Array.isArray(outlineSection.key_points) &&
                            outlineSection.key_points.length > 0 && (
                              <ul className="ml-4 list-disc space-y-0.5 text-sm text-gray-700">
                                {outlineSection.key_points.map(
                                  (keyPoint, keyPointIdx) => (
                                    <li key={keyPointIdx}>{keyPoint}</li>
                                  )
                                )}
                              </ul>
                            )}
                        </div>
                      ))}
                    </div>
                  </div>
                </div>
              )}

            {Array.isArray(article.image_suggestions) &&
              article.image_suggestions.length > 0 &&
              activeArticleId && (
                <div
                  id="image-suggestions"
                  className="border-t border-gray-200 pt-6"
                  style={{ scrollMarginTop: 16 }}
                >
                  <div className="flex flex-col gap-3">
                    <h3 className="text-base font-semibold text-gray-900">
                      Image suggestions
                    </h3>
                    {mediaError && (
                      <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
                        {mediaError}
                      </div>
                    )}
                    <div className="flex flex-col gap-2">
                      {(
                        article.image_suggestions as Array<{
                          placement?: string;
                          caption?: string;
                          alt?: string;
                        }>
                      ).map((imageSuggestion, imageIdx) => (
                        <ImageSuggestionCard
                          key={imageIdx}
                          articleId={activeArticleId}
                          suggestionIndex={imageIdx}
                          suggestion={imageSuggestion}
                          media={mediaBySuggestion[String(imageIdx)] ?? null}
                          clientId={clientId}
                          token={token}
                          onMediaChanged={handleMediaChanged}
                          onMediaDeleted={handleMediaDeleted}
                          onError={setMediaError}
                        />
                      ))}
                    </div>
                  </div>
                </div>
              )}

            {Array.isArray(article.internal_links) &&
              article.internal_links.length > 0 && (
                <div
                  id="internal-links"
                  className="border-t border-gray-200 pt-6"
                  style={{ scrollMarginTop: 16 }}
                >
                  <div className="flex flex-col gap-3">
                    <h3 className="text-base font-semibold text-gray-900">
                      Internal links
                    </h3>
                    <ul className="ml-4 list-disc space-y-1 text-sm">
                      {(
                        article.internal_links as Array<{
                          anchor?: string;
                          path_hint?: string;
                        }>
                      ).map((internalLink, linkIdx) => (
                        <li key={linkIdx}>
                          <span className="font-medium text-gray-900">
                            {internalLink.anchor}
                          </span>
                          {internalLink.path_hint && (
                            <span className="text-gray-500">
                              {' → '}
                              <code>{internalLink.path_hint}</code>
                            </span>
                          )}
                        </li>
                      ))}
                    </ul>
                  </div>
                </div>
              )}

            {Array.isArray(article.external_refs) &&
              article.external_refs.length > 0 && (
                <div
                  id="external-refs"
                  className="border-t border-gray-200 pt-6"
                  style={{ scrollMarginTop: 16 }}
                >
                  <div className="flex flex-col gap-3">
                    <h3 className="text-base font-semibold text-gray-900">
                      External references
                    </h3>
                    <ul className="ml-4 list-disc space-y-1 text-sm">
                      {(
                        article.external_refs as Array<{
                          title?: string;
                          url?: string;
                        }>
                      ).map((externalRef, refIdx) => (
                        <li key={refIdx}>
                          {externalRef.url ? (
                            <a
                              href={externalRef.url}
                              target="_blank"
                              rel="noreferrer"
                              className="text-[#B7007C] hover:underline"
                            >
                              {externalRef.title || externalRef.url}
                            </a>
                          ) : (
                            <span className="text-gray-900">
                              {externalRef.title}
                            </span>
                          )}
                        </li>
                      ))}
                    </ul>
                  </div>
                </div>
              )}

            {(article.schema_article_jsonld || article.schema_faq_jsonld) && (
              <div
                id="schema-jsonld"
                className="border-t border-gray-200 pt-6"
                style={{ scrollMarginTop: 16 }}
              >
                <div className="flex flex-col gap-3">
                  <h3 className="text-base font-semibold text-gray-900">
                    Schema (JSON-LD)
                  </h3>
                  <p className="text-xs text-gray-500">
                    Drop these into your page&apos;s <code>&lt;head&gt;</code>{' '}
                    inside a{' '}
                    <code>
                      &lt;script type=&quot;application/ld+json&quot;&gt;
                    </code>{' '}
                    tag.
                  </p>
                  <div className="flex flex-wrap gap-2">
                    {article.schema_article_jsonld && (
                      <button
                        type="button"
                        onClick={() =>
                          handleCopyJsonLd(
                            article.schema_article_jsonld as string,
                            'Article JSON-LD'
                          )
                        }
                        className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
                      >
                        Copy Article JSON-LD
                      </button>
                    )}
                    {article.schema_faq_jsonld && (
                      <button
                        type="button"
                        onClick={() =>
                          handleCopyJsonLd(
                            article.schema_faq_jsonld as string,
                            'FAQ JSON-LD'
                          )
                        }
                        className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
                      >
                        Copy FAQ JSON-LD
                      </button>
                    )}
                  </div>
                </div>
              </div>
            )}
          </div>
        ) : (
          <div className="flex items-center justify-center p-12">
            <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700" />
          </div>
        )}
      </div>
    </div>
  );

  const renderVersionsTab = (): JSX.Element => {
    if (versionsLoading && versions.length === 0) {
      return (
        <div className="p-4 animate-pulse space-y-2">
          <div className="h-3 bg-gray-200 rounded w-full" />
          <div className="h-3 bg-gray-200 rounded w-10/12" />
          <div className="h-3 bg-gray-200 rounded w-9/12" />
        </div>
      );
    }

    if (versionsError) {
      return (
        <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700">
          {versionsError}
        </div>
      );
    }

    if (versions.length === 0) {
      return (
        <p className="text-sm text-gray-500">
          No historical versions yet. Versions are recorded each time someone
          edits the article or requests a regenerate.
        </p>
      );
    }

    return (
      <div
        className="grid gap-6"
        style={{
          gridTemplateColumns: 'minmax(220px, 280px) 1fr',
          minHeight: '60vh',
        }}
      >
        <aside
          className="border-r border-gray-200 pr-4 overflow-y-auto"
          style={{ maxHeight: '70vh' }}
        >
          <div className="flex flex-col gap-2">
            {versions.map(version => {
              const isActive = version.version_id === selectedVersionId;

              return (
                <button
                  key={version.version_id}
                  type="button"
                  onClick={() => setSelectedVersionId(version.version_id)}
                  className="text-left w-full rounded-lg p-3"
                  style={{
                    border: '1px solid',
                    borderColor: isActive ? '#B7007C' : '#e1e3e5',
                    background: isActive ? '#fff1f8' : '#fff',
                    cursor: 'pointer',
                  }}
                >
                  <div className="flex flex-col gap-1">
                    <div className="flex items-center gap-2 flex-wrap">
                      <span
                        className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${toneToBadgeClass(editedByToTone(version.edited_by))}`}
                      >
                        {version.edited_by}
                      </span>
                      {version.created_at && (
                        <span className="text-xs text-gray-500">
                          {version.created_at.slice(0, 16).replace('T', ' ')}
                        </span>
                      )}
                    </div>
                    {version.diff_summary && (
                      <p className="text-xs text-gray-700">
                        {version.diff_summary}
                      </p>
                    )}
                  </div>
                </button>
              );
            })}
          </div>
        </aside>

        <div className="overflow-y-auto pr-2" style={{ maxHeight: '70vh' }}>
          {versionDetailLoading && !versionDetail ? (
            <div className="animate-pulse space-y-2">
              <div className="h-6 bg-gray-200 rounded w-1/2" />
              <div className="h-3 bg-gray-200 rounded w-11/12" />
              <div className="h-3 bg-gray-200 rounded w-10/12" />
            </div>
          ) : versionDetail ? (
            <div className="flex flex-col gap-3">
              <div className="flex items-center justify-between flex-wrap gap-2">
                <div className="flex flex-col gap-1">
                  {versionDetail.title && (
                    <h3 className="text-base font-semibold text-gray-900">
                      {versionDetail.title}
                    </h3>
                  )}
                  <div className="flex items-center gap-2 flex-wrap">
                    <span
                      className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${toneToBadgeClass(editedByToTone(versionDetail.edited_by))}`}
                    >
                      {versionDetail.edited_by}
                    </span>
                    {versionDetail.created_at && (
                      <span className="text-xs text-gray-500">
                        {versionDetail.created_at
                          .slice(0, 16)
                          .replace('T', ' ')}
                      </span>
                    )}
                  </div>
                  {versionDetail.diff_summary && (
                    <p className="text-xs text-gray-500">
                      {versionDetail.diff_summary}
                    </p>
                  )}
                </div>
                <div className="flex items-center gap-2">
                  <button
                    type="button"
                    onClick={handleCopyVersionHtml}
                    disabled={!versionDetail.markdown}
                    className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
                  >
                    Copy HTML
                  </button>
                  <button
                    type="button"
                    onClick={handleCopyVersionText}
                    disabled={!versionDetail.markdown}
                    className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
                  >
                    Copy plain text
                  </button>
                </div>
              </div>
              {versionDetail.markdown ? (
                <MarkdownView source={versionDetail.markdown} />
              ) : (
                <p className="text-sm text-gray-500">
                  This version doesn&apos;t have a markdown snapshot recorded.
                </p>
              )}
            </div>
          ) : (
            <p className="text-sm text-gray-500">
              Select a version from the timeline to preview it here.
            </p>
          )}
        </div>
      </div>
    );
  };

  return (
    <Modal
      open={open}
      onClose={onClose}
      size="xl"
      title={article?.h1 || article?.summary.title || 'Article'}
    >
      <div className="flex flex-col gap-4">
        <div className="flex items-center justify-end gap-2 flex-wrap">
          {copyFeedback && (
            <span className="text-xs text-gray-500">{copyFeedback}</span>
          )}
          {editing ? (
            <>
              <button
                type="button"
                onClick={handleCancelEdit}
                disabled={saveBusy}
                className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                Cancel
              </button>
              <button
                type="button"
                onClick={handleSaveEdit}
                disabled={saveBusy || editDraft === article?.markdown}
                className="inline-flex items-center gap-1 rounded-md border border-pink-600 bg-pink-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-pink-700 disabled:opacity-50"
              >
                {saveBusy ? 'Saving…' : 'Save edits'}
              </button>
            </>
          ) : (
            <>
              <button
                type="button"
                onClick={handleCopyHtml}
                disabled={!article?.markdown}
                className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                Copy HTML
              </button>
              <button
                type="button"
                onClick={handleCopyText}
                disabled={!article?.markdown}
                className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                Copy plain text
              </button>
              <button
                type="button"
                onClick={handleStartEdit}
                disabled={!article?.markdown || isGenerating}
                className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                Edit
              </button>
              <button
                type="button"
                onClick={() => setRegenerateOpen(previous => !previous)}
                disabled={!article || isGenerating}
                className={`inline-flex items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium disabled:opacity-50 ${regenerateOpen ? 'border-pink-600 bg-pink-50 text-pink-700' : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'}`}
              >
                {regenerateOpen ? 'Hide directive' : 'Customize regeneration'}
              </button>
              <button
                type="button"
                onClick={handleRegenerate}
                disabled={!article || regenerateBusy || isGenerating}
                className="inline-flex items-center gap-1 rounded-md border border-pink-600 bg-pink-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-pink-700 disabled:opacity-50"
              >
                {regenerateBusy ? 'Starting…' : 'Regenerate'}
              </button>
              <button
                type="button"
                onClick={handlePublishToggle}
                disabled={!canTogglePublish || isGenerating || publishBusy}
                className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
              >
                {publishBusy ? 'Saving…' : publishLabel}
              </button>
              <button
                type="button"
                onClick={() => setDeleteConfirmOpen(true)}
                disabled={!article || deleteBusy || isGenerating}
                className="inline-flex items-center gap-1 rounded-md border border-red-600 bg-white px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50 disabled:opacity-50"
              >
                {deleteBusy ? 'Deleting...' : 'Delete'}
              </button>
            </>
          )}
        </div>

        {regenerateOpen && !editing && (
          <div className="rounded-lg border border-gray-200 bg-gray-50 p-3 flex flex-col gap-2">
            <div className="flex items-center justify-between gap-2">
              <p className="text-sm font-semibold text-gray-900">
                Regeneration directive (optional)
              </p>
              <span className="text-xs text-gray-500">
                {regenerateInstructions.length}/{REGEN_INSTRUCTIONS_MAX}
              </span>
            </div>
            <p className="text-xs text-gray-500">
              Threaded into the model as the highest-priority constraint - tone
              shifts, structure (listicle / how-to / comparison), word
              exclusions, or new sections to add.
            </p>
            <textarea
              value={regenerateInstructions}
              onChange={event =>
                setRegenerateInstructions(
                  event.target.value.slice(0, REGEN_INSTRUCTIONS_MAX)
                )
              }
              placeholder={REGEN_PLACEHOLDER}
              maxLength={REGEN_INSTRUCTIONS_MAX}
              disabled={regenerateBusy}
              rows={5}
              className="w-full rounded-md border border-gray-300 bg-white p-2 text-sm text-gray-900 focus:border-pink-600 focus:outline-none focus:ring-1 focus:ring-pink-600 disabled:opacity-50"
            />
          </div>
        )}

        {isGenerating && (
          <div className="rounded-lg border border-blue-200 bg-blue-50 p-3 flex items-center gap-2">
            <div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-300 border-t-blue-700" />
            <span className="text-sm text-blue-900">
              {article?.markdown
                ? 'Regenerating article - content below refreshes automatically when the new version is ready.'
                : 'Generating article - this typically takes 30-90 seconds.'}
            </span>
          </div>
        )}

        {error && (
          <div className="rounded-lg border border-red-200 bg-red-50 p-3 flex items-center justify-between gap-2">
            <p className="text-sm text-red-700">{error}</p>
            <button
              type="button"
              onClick={loadArticle}
              className="text-sm font-medium text-red-700 hover:text-red-900 underline flex-shrink-0"
            >
              Retry
            </button>
          </div>
        )}

        <div className="border-b border-gray-200 flex gap-4">
          {[
            { id: 'current', content: 'Current' },
            {
              id: 'versions',
              content:
                versions.length > 0
                  ? `Versions (${versions.length})`
                  : 'Versions',
            },
          ].map((tab, tabIndex) => (
            <button
              key={tab.id}
              type="button"
              onClick={() => setSelectedTab(tabIndex)}
              className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px ${
                selectedTab === tabIndex
                  ? 'border-pink-600 text-pink-600'
                  : 'border-transparent text-gray-600 hover:text-gray-900'
              }`}
            >
              {tab.content}
            </button>
          ))}
        </div>

        <div className="pt-2">
          {selectedTab === 0 && renderCurrentTab()}
          {selectedTab === 1 && renderVersionsTab()}
        </div>
      </div>
      <ConfirmDialog
        open={deleteConfirmOpen}
        onOpenChange={setDeleteConfirmOpen}
        busy={deleteBusy}
        onConfirm={handleDelete}
        {...deleteArticleTexts(deleteTitle)}
      />
    </Modal>
  );
};

export default ArticleModal;
