import React, { useState, useEffect, useRef, useCallback } from 'react';
import type { EditorStore } from '../../store/editorStore';
import type { CbTemplate, PageData, CBNode } from '../../types';
import { useApi } from '../../hooks/useApi';

// Mirror the PHP class Jalpi_Templates::CATEGORIES constant
/**
 * Categories grouped for the sidebar. Groups keep 24 categories navigable —
 * a flat chip bar wrapped onto 5 rows and consumed 59% of the modal.
 * Empty categories are hidden at render time, so this list can stay complete
 * without cluttering the UI.
 */
const CATEGORY_GROUPS: Array<{ title: string; slugs: string[] }> = [
  { title: 'Pages',    slugs: ['landing-pages', 'home-pages', 'about-pages', 'contact-pages', 'services-pages', 'portfolio-pages', 'blog-pages'] },
  { title: 'Sections', slugs: ['hero-sections', 'features', 'cta-blocks', 'testimonials', 'pricing-tables', 'team', 'faq', 'statistics', 'logo-carousel', 'forms'] },
  { title: 'Site',     slugs: ['headers', 'footers', 'popups'] },
  { title: 'Utility',  slugs: ['404-pages', 'coming-soon', 'maintenance'] },
  { title: 'Commerce', slugs: ['woocommerce'] },
];

const CATEGORIES = [
  // Full pages
  { slug: 'landing-pages',   label: 'Landing Pages' },
  { slug: 'home-pages',      label: 'Home Pages' },
  { slug: 'about-pages',     label: 'About Pages' },
  { slug: 'contact-pages',   label: 'Contact Pages' },
  { slug: 'services-pages',  label: 'Services Pages' },
  { slug: 'portfolio-pages', label: 'Portfolio Pages' },
  { slug: 'blog-pages',      label: 'Blog Pages' },
  // Sections
  { slug: 'hero-sections',   label: 'Hero Sections' },
  { slug: 'features',        label: 'Features' },
  { slug: 'cta-blocks',      label: 'CTA Blocks' },
  { slug: 'testimonials',    label: 'Testimonials' },
  { slug: 'pricing-tables',  label: 'Pricing Tables' },
  { slug: 'team',            label: 'Team' },
  { slug: 'faq',             label: 'FAQ' },
  { slug: 'statistics',      label: 'Statistics / Counter' },
  { slug: 'logo-carousel',   label: 'Logo Carousel' },
  { slug: 'forms',           label: 'Forms' },
  // Site chrome
  { slug: 'headers',         label: 'Headers' },
  { slug: 'footers',         label: 'Footers' },
  { slug: 'popups',          label: 'Popups' },
  // Utility pages
  { slug: '404-pages',       label: '404 Pages' },
  { slug: 'coming-soon',     label: 'Coming Soon' },
  { slug: 'maintenance',     label: 'Maintenance' },
  // Commerce
  { slug: 'woocommerce',     label: 'WooCommerce Templates' },
];

interface Props {
  store: EditorStore;
  onClose: () => void;
}

type LoadState = 'loading' | 'idle' | 'error';

// All templates inlined by PHP into jalpiEditorConfig.templateList at page load.
// This avoids a REST API call for listing (which hangs on some dev environments).
const allInlineTemplates: CbTemplate[] =
  ((window as any).jalpiEditorConfig?.templateList as CbTemplate[]) ?? [];

export function TemplatePicker({ store, onClose }: Props) {
  const api = useApi();
  const [allTemplates,  setAllTemplates]  = useState<CbTemplate[]>(allInlineTemplates);
  const [category,      setCategory]      = useState('');
  const [query,         setQuery]         = useState('');
  const [debouncedQ,    setDebouncedQ]    = useState('');
  const [applying,      setApplying]      = useState<number | string | null>(null);
  const [seeding,       setSeeding]       = useState(false);
  const [activeTag,     setActiveTag]     = useState('');
  const [viewMode,      setViewMode]      = useState<'grid' | 'list'>('grid');
  const [scope,         setScope]         = useState<'' | 'favorites' | 'recent'>('');
  // Per-user lists, persisted server-side so they follow the user between
  // browsers rather than living in localStorage.
  const [favorites,     setFavorites]     = useState<string[]>([]);
  const [recent,        setRecent]        = useState<string[]>([]);
  // How many cards are rendered; grows as the user scrolls (lazy rendering).
  const PAGE_SIZE = 24;
  const [visibleCount,  setVisibleCount]  = useState(PAGE_SIZE);
  const fileRef     = useRef<HTMLInputElement>(null);
  const sentinelRef = useRef<HTMLDivElement | null>(null);
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Client-side filter instead of REST calls
  const templates = allTemplates.filter((t) => {
    const matchCat = !category || t.category === category;
    const q = debouncedQ.toLowerCase();
    const matchQ = !debouncedQ ||
      t.name.toLowerCase().includes(q) ||
      (t.description ?? '').toLowerCase().includes(q) ||
      (t.category_label ?? '').toLowerCase().includes(q) ||
      (t.tags ?? []).some((tag) => tag.toLowerCase().includes(q));
    const matchTag = !activeTag || (t.tags ?? []).includes(activeTag);
    const matchScope =
      scope === 'favorites' ? favorites.includes(String(t.id))
      : scope === 'recent'  ? recent.includes(String(t.id))
      : true;
    return matchCat && matchQ && matchTag && matchScope;
  });

  // Counts and the tag vocabulary are derived from the inlined list, so they
  // need no extra request and stay correct as the list changes.
  const catCounts = React.useMemo(() => {
    const m: Record<string, number> = {};
    allTemplates.forEach((t) => { if (t.category) m[t.category] = (m[t.category] ?? 0) + 1; });
    return m;
  }, [allTemplates]);

  const allTags = React.useMemo(() => {
    const set = new Set<string>();
    allTemplates.forEach((t) => (t.tags ?? []).forEach((tag) => set.add(tag)));
    return Array.from(set).sort();
  }, [allTemplates]);

  const loadState: LoadState = 'idle';

  // Load per-user favourites / recents once.
  useEffect(() => {
    let alive = true;
    api.getTemplateUserLists()
      .then((l) => { if (alive) { setFavorites(l.favorites || []); setRecent(l.recent || []); } })
      .catch(() => { /* non-fatal: lists stay empty */ });
    return () => { alive = false; };
  }, [api]);

  const isFavorite = useCallback(
    (id: number | string) => favorites.includes(String(id)),
    [favorites],
  );

  const toggleFavorite = useCallback(async (id: number | string) => {
    const key = String(id);
    // Optimistic so the star reacts instantly.
    setFavorites((prev) => (prev.includes(key) ? prev.filter((x) => x !== key) : [key, ...prev]));
    try {
      const res = await api.toggleTemplateFavorite(id);
      setFavorites(res.favorites);
    } catch {
      api.getTemplateUserLists()
        .then((l) => setFavorites(l.favorites || []))
        .catch(() => {});
    }
  }, [api]);

  // Reset lazy rendering whenever the result set changes.
  useEffect(() => { setVisibleCount(PAGE_SIZE); }, [category, debouncedQ, activeTag, scope]);

  // Grow the rendered slice as the sentinel scrolls into view.
  useEffect(() => {
    const el = sentinelRef.current;
    if (!el || typeof IntersectionObserver === 'undefined') return;
    const io = new IntersectionObserver((entries) => {
      if (entries.some((e) => e.isIntersecting)) {
        setVisibleCount((n) => n + PAGE_SIZE);
      }
    }, { rootMargin: '260px' });
    io.observe(el);
    return () => io.disconnect();
  }, [templates.length, visibleCount]);

  // Debounce the search query
  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => setDebouncedQ(query.trim()), 350);
    return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
  }, [query]);

  const fetchTemplates = useCallback(() => {
    // no-op: data comes from inline jalpiEditorConfig.templateList
  }, []);

  // On mount: seed any new bundled templates that haven't been seeded yet.
  // Fast POST — no loopback hang.
  //
  // Reload on `backfilled` too, not just `seeded`: an already-seeded template
  // that just gained a thumbnail changes no row count, but the template list is
  // inlined into the page at load, so without a reload the card would keep
  // showing its placeholder icon until the next full page load.
  useEffect(() => {
    api.seedBundledTemplates(false)
      .then((result: any) => {
        if (result?.seeded > 0 || result?.backfilled > 0) {
          window.location.reload();
        }
      })
      .catch(() => { /* ignore network errors silently */ });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Close on Escape
  useEffect(() => {
    const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [onClose]);

  // Apply a library template.
  // Bundled templates are fetched as static JSON files (no REST, no loopback hang).
  // User-created templates fall back to the REST API.
  const applyTemplate = async (id: number | string) => {
    if (applying !== null) return;
    setApplying(id);
    try {
      const meta = allTemplates.find((t) => String(t.id) === String(id));
      let sections: CBNode[] | null = null;
      let sectionNode: CBNode | null = null;

      if (meta?.bundled_slug) {
        // Static file fetch — bypasses REST API entirely
        const pluginUrl = (window as any).jalpiEditorConfig?.pluginUrl ?? '';
        const res = await fetch(
          `${pluginUrl}templates/bundled/${meta.bundled_slug}.json`,
          { cache: 'no-store' },
        );
        if (!res.ok) throw new Error(`Failed to load template file (${res.status})`);
        const json = await res.json();
        if (Array.isArray(json?.data?.sections)) {
          sections = json.data.sections as CBNode[];
        } else if (json?.data) {
          sectionNode = json.data as CBNode;
        }
      } else {
        // User-created template — fetch via REST API
        const tpl = await api.getTemplate(id);
        const d = tpl.data as (PageData & CBNode);
        // The template's TYPE is authoritative. A 'section' template must be
        // INSERTED into the current page, never replace it — that is the whole
        // point of the section categories (FAQ, Hero, Pricing, …). Only fall
        // back to shape-sniffing when type is absent.
        const isSection = (tpl.type ?? meta?.type) === 'section' || (tpl.type ?? meta?.type) === 'element';
        if (isSection) {
          // A one-section document still yields a single node to insert.
          sectionNode = Array.isArray((d as PageData).sections)
            ? ((d as PageData).sections[0] as CBNode)
            : (d as CBNode);
        } else if (Array.isArray((d as PageData).sections)) {
          sections = (d as PageData).sections;
        } else {
          sectionNode = d as CBNode;
        }
      }

      if (sections) {
        store.loadTemplateAsPage(sections);
      } else if (sectionNode) {
        store.insertTemplateSection(sectionNode);
      }
      onClose();
    } catch (err) {
      console.error('[TemplatePicker] applyTemplate error:', err);
      setApplying(null);
    }
  };

  // Import from local JSON file
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      try {
        const data = JSON.parse(ev.target?.result as string);
        if (Array.isArray(data.sections)) {
          store.loadTemplateAsPage(data.sections as CBNode[]);
        } else if (typeof data.type === 'string') {
          store.insertTemplateSection(data as CBNode);
        }
        onClose();
      } catch { /* ignore malformed JSON */ }
    };
    reader.readAsText(file);
    e.target.value = '';
  };

  const loadBundled = async () => {
    if (seeding) return;
    setSeeding(true);
    try {
      const result = await api.seedBundledTemplates(false);
      if ((result as any).seeded > 0) {
        // Templates are now in the DB. Reload so the inline list picks them up.
        window.location.reload();
      }
    } catch {
      setSeeding(false);
    }
  };

  const isEmpty = loadState === 'idle' && templates.length === 0;

  return (
    <div
      className="jalpi-tpl-backdrop"
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div className="jalpi-tpl-modal" role="dialog" aria-label="Template Library">

        {/* ── Header ── */}
        <div className="jalpi-tpl-header">
          <div className="jalpi-tpl-header__left">
            <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="3" width="7" height="9" rx="1.5"/>
              <rect x="14" y="3" width="7" height="5" rx="1.5"/>
              <rect x="14" y="11" width="7" height="9" rx="1.5"/>
              <rect x="3" y="15" width="7" height="6" rx="1.5"/>
            </svg>
            <span className="jalpi-tpl-title">Template Library</span>
          </div>

          <div className="jalpi-tpl-header__right">
            {/* Search */}
            <div className="jalpi-tpl-search">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
                <circle cx="11" cy="11" r="8"/>
                <path d="M21 21l-4.35-4.35"/>
              </svg>
              <input
                type="text"
                className="jalpi-tpl-search__input"
                placeholder="Search templates…"
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                autoFocus
              />
              {query && (
                <button className="jalpi-tpl-search__clear" onClick={() => setQuery('')} title="Clear">
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                    <path d="M18 6L6 18M6 6l12 12"/>
                  </svg>
                </button>
              )}
            </div>

            {/* Import .json */}
            <button className="jalpi-tpl-import-btn" onClick={() => fileRef.current?.click()} title="Import a template from a JSON file">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                <polyline points="7 10 12 15 17 10"/>
                <line x1="12" y1="15" x2="12" y2="3"/>
              </svg>
              Import .json
            </button>

            {/* Close */}
            <button className="jalpi-tpl-close" onClick={onClose} title="Close (Esc)">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
                <path d="M18 6L6 18M6 6l12 12"/>
              </svg>
            </button>
          </div>
        </div>

        {/* ── Main: sidebar + body ── */}
        <div className="jalpi-tpl-main">

          {/* Sidebar: grouped categories. Empty ones are hidden so the list
              only ever shows somewhere worth clicking. */}
          <aside className="jalpi-tpl-side" role="tablist" aria-label="Categories">
            <button
              className={`jalpi-tpl-side__item${category === '' ? ' is-active' : ''}`}
              role="tab"
              aria-selected={category === ''}
              onClick={() => setCategory('')}
            >
              <span>All templates</span>
              <span className="jalpi-tpl-side__count">{allTemplates.length}</span>
            </button>

            <div className="jalpi-tpl-side__group">
              <span className="jalpi-tpl-side__title">Library</span>
              {([['favorites', 'Favorites'], ['recent', 'Recently Used']] as const).map(([val, label]) => (
                <button
                  key={val}
                  className={`jalpi-tpl-side__item${scope === val ? ' is-active' : ''}`}
                  onClick={() => setScope(scope === val ? '' : val)}
                  aria-pressed={scope === val}
                >
                  <span>{label}</span>
                  {val === 'favorites' && favorites.length > 0 && (
                    <span className="jalpi-tpl-side__count">{favorites.length}</span>
                  )}
                </button>
              ))}
            </div>

            {CATEGORY_GROUPS.map((group) => {
              const items = group.slugs
                .map((slug) => ({ slug, label: CATEGORIES.find((c) => c.slug === slug)?.label ?? slug, n: catCounts[slug] ?? 0 }))
                .filter((c) => c.n > 0);
              if (!items.length) return null;
              return (
                <div className="jalpi-tpl-side__group" key={group.title}>
                  <span className="jalpi-tpl-side__title">{group.title}</span>
                  {items.map((c) => (
                    <button
                      key={c.slug}
                      className={`jalpi-tpl-side__item${category === c.slug ? ' is-active' : ''}`}
                      role="tab"
                      aria-selected={category === c.slug}
                      onClick={() => setCategory(c.slug)}
                    >
                      <span>{c.label}</span>
                      <span className="jalpi-tpl-side__count">{c.n}</span>
                    </button>
                  ))}
                </div>
              );
            })}
          </aside>

          <div className="jalpi-tpl-content">

            {/* Slim toolbar: active filter chip + view mode. */}
            <div className="jalpi-tpl-toolbar">
              <span className="jalpi-tpl-toolbar__count">
                {templates.length} template{templates.length === 1 ? '' : 's'}
              </span>
              {activeTag && (
                <button className="jalpi-tpl-tag is-active" onClick={() => setActiveTag('')}>
                  {activeTag} ✕
                </button>
              )}
              <div className="jalpi-tpl-viewmode" role="group" aria-label="View mode">
                <button
                  type="button"
                  className={`jalpi-tpl-viewmode__btn${viewMode === 'grid' ? ' is-active' : ''}`}
                  onClick={() => setViewMode('grid')}
                  aria-pressed={viewMode === 'grid'}
                  title="Grid view"
                >
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8">
                    <rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" />
                    <rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" />
                  </svg>
                </button>
                <button
                  type="button"
                  className={`jalpi-tpl-viewmode__btn${viewMode === 'list' ? ' is-active' : ''}`}
                  onClick={() => setViewMode('list')}
                  aria-pressed={viewMode === 'list'}
                  title="List view"
                >
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8">
                    <line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
                  </svg>
                </button>
              </div>
            </div>

        {/* ── Body ── */}
        <div className="jalpi-tpl-body">

          {/* Loading skeletons */}
          {loadState === 'loading' && (
            <div className="jalpi-tpl-grid">
              {Array.from({ length: 6 }).map((_, i) => (
                <div key={i} className="jalpi-tpl-skeleton" aria-hidden="true" />
              ))}
            </div>
          )}

          {/* Error */}
          {loadState === 'error' && (
            <div className="jalpi-tpl-state">
              <p className="jalpi-tpl-state__title">Could not load templates</p>
              <p className="jalpi-tpl-state__body">Check your connection and try again.</p>
              <button className="jalpi-tpl-retry" onClick={fetchTemplates}>Try again</button>
            </div>
          )}

          {/* Empty state */}
          {isEmpty && (
            <div className="jalpi-tpl-state">
              <svg width="124" height="96" viewBox="0 0 124 96" fill="none" className="jalpi-tpl-state__illo" aria-hidden="true">
                <rect x="6"  y="8" width="32" height="80" rx="5" fill="#f3f4f6" stroke="#e5e7eb" strokeWidth="1.5"/>
                <rect x="46" y="8" width="32" height="80" rx="5" fill="#f0f0f0" stroke="#e0e0e0" strokeWidth="1.5"/>
                <rect x="86" y="8" width="32" height="80" rx="5" fill="#f3f4f6" stroke="#e5e7eb" strokeWidth="1.5"/>
                <rect x="11" y="14" width="22" height="6"  rx="2.5" fill="#e5e7eb"/>
                <rect x="11" y="24" width="16" height="4"  rx="2"   fill="#f3f4f6"/>
                <rect x="51" y="14" width="22" height="6"  rx="2.5" fill="#bbbbbb"/>
                <rect x="51" y="24" width="16" height="4"  rx="2"   fill="#d0d0d0"/>
                <rect x="91" y="14" width="22" height="6"  rx="2.5" fill="#e5e7eb"/>
                <rect x="91" y="24" width="16" height="4"  rx="2"   fill="#f3f4f6"/>
              </svg>
              <p className="jalpi-tpl-state__title">
                {debouncedQ ? `No results for "${debouncedQ}"` : 'No templates yet'}
              </p>
              <p className="jalpi-tpl-state__body">
                {debouncedQ
                  ? 'Try a different term or browse all categories.'
                  : 'Load the included starter templates or import your own JSON file.'}
              </p>
              {!debouncedQ && (
                <button
                  className="jalpi-tpl-import-cta"
                  onClick={loadBundled}
                  disabled={seeding}
                  style={{ marginBottom: '10px' }}
                >
                  {seeding ? (
                    <>
                      <div className="jalpi-tpl-spinner" style={{ width: '13px', height: '13px', borderWidth: '2px' }} />
                      Loading templates…
                    </>
                  ) : (
                    <>
                      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                        <polyline points="17 8 12 3 7 8"/>
                        <line x1="12" y1="3" x2="12" y2="15"/>
                      </svg>
                      Load Default Templates
                    </>
                  )}
                </button>
              )}
              <button className="jalpi-tpl-import-cta" style={{ background: 'transparent', color: '#6b7280', border: '1px solid #e5e7eb' }} onClick={() => fileRef.current?.click()}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                  <polyline points="7 10 12 15 17 10"/>
                  <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
                Import from JSON file
              </button>
            </div>
          )}

          {/* Template grid */}
          {loadState === 'idle' && templates.length > 0 && (
            <div className={viewMode === 'list' ? 'jalpi-tpl-grid is-list' : 'jalpi-tpl-grid'}>
              {templates.slice(0, visibleCount).map((tpl) => (
                // A div, not a button: the card contains its own favourite
                // button and nesting buttons is invalid HTML.
                <div
                  key={String(tpl.id)}
                  className={`jalpi-tpl-card${applying === tpl.id ? ' is-applying' : ''}`}
                  role="button"
                  tabIndex={0}
                  aria-label={`Use template ${tpl.name}`}
                  onClick={() => { if (applying === null) applyTemplate(tpl.id); }}
                  onKeyDown={(e) => {
                    if ((e.key === 'Enter' || e.key === ' ') && applying === null) {
                      e.preventDefault();
                      applyTemplate(tpl.id);
                    }
                  }}
                  title={tpl.name}
                >
                  <div className="jalpi-tpl-card__thumb">
                    {tpl.thumbnail ? (
                      <img src={tpl.thumbnail} alt={tpl.name} loading="lazy" />
                    ) : (
                      <div className="jalpi-tpl-card__placeholder" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '10px', padding: '16px', background: 'linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%)' }}>
                        <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" opacity="0.6">
                          <rect x="2" y="3" width="20" height="18" rx="2"/>
                          <path d="M8 7h8M8 11h6M8 15h4"/>
                        </svg>
                        <span style={{ fontSize: '12px', fontWeight: '600', color: '#4338ca', textAlign: 'center', lineHeight: '1.3', wordBreak: 'break-word' }}>
                          {tpl.name}
                        </span>
                      </div>
                    )}
                    {/* Apply spinner */}
                    {applying === tpl.id && (
                      <div className="jalpi-tpl-card__applying">
                        <div className="jalpi-tpl-spinner"/>
                      </div>
                    )}
                    {/* Hover overlay */}
                    {applying === null && (
                      <div className="jalpi-tpl-card__hover" aria-hidden="true">
                        <span>Use Template</span>
                      </div>
                    )}
                    <button
                      type="button"
                      className={`jalpi-tpl-card__fav${isFavorite(tpl.id) ? ' is-on' : ''}`}
                      onClick={(e) => { e.stopPropagation(); toggleFavorite(tpl.id); }}
                      aria-pressed={isFavorite(tpl.id)}
                      aria-label={isFavorite(tpl.id) ? 'Remove from favorites' : 'Add to favorites'}
                      title={isFavorite(tpl.id) ? 'Remove from favorites' : 'Add to favorites'}
                    >
                      <svg viewBox="0 0 24 24" width="15" height="15"
                           fill={isFavorite(tpl.id) ? 'currentColor' : 'none'}
                           stroke="currentColor" strokeWidth="1.8">
                        <path d="M12 17.3l-5.6 3 1.1-6.3-4.6-4.4 6.3-.9L12 3l2.8 5.7 6.3.9-4.6 4.4 1.1 6.3z" />
                      </svg>
                    </button>
                  </div>
                  <div className="jalpi-tpl-card__meta">
                    <span className="jalpi-tpl-card__name">{tpl.name}</span>
                    <div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' }}>
                      {tpl.category_label && (
                        <span className="jalpi-tpl-card__cat">{tpl.category_label}</span>
                      )}
                      <span className="jalpi-tpl-card__cat" style={{ backgroundColor: tpl.type === 'page' ? '#dbeafe' : '#dcfce7', color: tpl.type === 'page' ? '#1d4ed8' : '#15803d' }}>
                        {tpl.type === 'page' ? 'Full Page' : 'Section'}
                      </span>
                    </div>
                    {!!tpl.tags?.length && (
                      <div className="jalpi-tpl-card__tags">
                        {tpl.tags.slice(0, 3).map((t) => (
                          <button
                            key={t}
                            type="button"
                            className="jalpi-tpl-card__tag"
                            onClick={(e) => { e.stopPropagation(); setActiveTag(t); }}
                            title={`Filter by "${t}"`}
                          >
                            {t}
                          </button>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
              ))}
            </div>
          )}

          {/* Lazy-render sentinel: grows the rendered slice on scroll. */}
          {loadState === 'idle' && visibleCount < templates.length && (
            <div ref={sentinelRef} className="jalpi-tpl-sentinel">
              <div className="jalpi-tpl-spinner" />
            </div>
          )}
          {loadState === 'idle' && templates.length > 0 && visibleCount >= templates.length && (
            <p className="jalpi-tpl-endcap">
              Showing all {templates.length} template{templates.length === 1 ? '' : 's'}
            </p>
          )}

        </div>{/* /body */}
          </div>{/* /content */}
        </div>{/* /main */}

        {/* Hidden file input */}
        <input
          ref={fileRef}
          type="file"
          accept=".json,application/json"
          className="jalpi-tpl-file-input"
          onChange={handleFileChange}
        />

      </div>{/* /modal */}
    </div>
  );
}
