import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check, ChevronDown, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import CoverLightbox from '@/ui/CoverLightbox';
import { useThemeAnimationRisk } from '@/features/settings/hooks/useThemeAnimationRisk';
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
import CustomSelect from '@/ui/CustomSelect';
import { formatRelativeTime } from '@/lib/format/relativeTime';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore, type InstalledTheme } from '@/store/installedThemesStore';
import {
  assetUrl,
  fetchRegistry,
  type RegistryTheme,
} from '@/lib/themes/themeRegistry';
import { installThemeFromRegistry } from '@/lib/themes/installThemeFromRegistry';
import { uninstallTheme } from '@/lib/themes/uninstallTheme';
import { isNewer } from '@/lib/util/appUpdaterHelpers';

type ModeFilter = 'all' | 'dark' | 'light';
type SortMode = 'newest' | 'name';
type AnimFilter = 'all' | 'animated' | 'static';

const THEMES_REPO_URL = 'https://github.com/Psysonic/psysonic-themes';

/** Themes shown per page — the catalogue is large enough to paginate. */
const PAGE_SIZE = 12;

/** A theme's changelog as `[version, lines]` pairs, newest version first. */
function sortedChangelog(changelog: Record<string, string[]>): [string, string[]][] {
  return Object.entries(changelog).sort(([a], [b]) => (isNewer(a, b) ? -1 : isNewer(b, a) ? 1 : 0));
}

/** Page numbers for the pager: all of them when there are few, otherwise the
 *  first and last page plus a window around the current one, with gaps. */
function pageItemsList(current: number, total: number): (number | 'gap')[] {
  if (total <= 10) return Array.from({ length: total }, (_, i) => i + 1);
  const out: (number | 'gap')[] = [1];
  const lo = Math.max(2, current - 2);
  const hi = Math.min(total - 1, current + 2);
  if (lo > 2) out.push('gap');
  for (let p = lo; p <= hi; p++) out.push(p);
  if (hi < total - 1) out.push('gap');
  out.push(total);
  return out;
}

/**
 * The community Theme Store: browse the GitHub-hosted registry, filter by name
 * and light/dark, install (fetch + persist + runtime inject), apply, update and
 * uninstall. Built-in themes are not in the registry, so they never appear here.
 */
export function ThemeStoreSection() {
  const { t, i18n } = useTranslation();
  const activeTheme = useThemeStore(s => s.theme);
  const setTheme = useThemeStore(s => s.setTheme);
  const installed = useInstalledThemesStore(s => s.themes);
  const animRisk = useThemeAnimationRisk();

  const [themes, setThemes] = useState<RegistryTheme[] | null>(null);
  const [generatedAt, setGeneratedAt] = useState('');
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [error, setError] = useState(false);
  const [stale, setStale] = useState(false);
  const [query, setQuery] = useState('');
  const [mode, setMode] = useState<ModeFilter>('all');
  const [sortMode, setSortMode] = useState<SortMode>('newest');
  const [animFilter, setAnimFilter] = useState<AnimFilter>('all');
  const [page, setPage] = useState(1);
  const [busyId, setBusyId] = useState<string | null>(null);
  const [failedId, setFailedId] = useState<string | null>(null);
  const [lightbox, setLightbox] = useState<{ src: string; name: string } | null>(null);
  const [openChangelogIds, setOpenChangelogIds] = useState<Set<string>>(() => new Set());
  const topRef = useRef<HTMLDivElement>(null);

  const toggleChangelog = (id: string) =>
    setOpenChangelogIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });

  // A manual refresh must not unmount the list: blanking it collapses the
  // scroll viewport's content height, which clamps scrollTop to 0 — i.e. the
  // page jumps to the top. So keep the existing list mounted (`refreshing`,
  // shown only via the spinning icon) and reserve the full-page loading/error
  // placeholders for the initial load, when there is nothing to show anyway.
  const load = (force = false) => {
    if (force) setRefreshing(true);
    else setLoading(true);
    setError(false);
    fetchRegistry({ force })
      .then(r => { setThemes(r.registry.themes); setGeneratedAt(r.registry.generatedAt); setStale(r.stale); })
      .catch(() => { if (force) setStale(true); else setError(true); })
      .finally(() => { setLoading(false); setRefreshing(false); });
  };

  // GitHub raw caches thumbnails only briefly, but the webview can still hold an
  // old copy; tie a cache-buster to the registry's generatedAt — it changes on
  // every themes push — so refreshed thumbnails show up after a registry refresh
  // instead of being stuck on the old image.
  const thumbUrl = (rel: string) =>
    generatedAt ? `${assetUrl(rel)}?v=${encodeURIComponent(generatedAt)}` : assetUrl(rel);

  // React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
  // eslint-disable-next-line react-hooks/set-state-in-effect
  useEffect(() => { load(false); }, []);

  const installedMap = useMemo(() => {
    const m = new Map<string, InstalledTheme>();
    for (const it of installed) m.set(it.id, it);
    return m;
  }, [installed]);

  const filtered = useMemo(() => {
    if (!themes) return [];
    const q = query.trim().toLowerCase();
    const matched = themes.filter(th => {
      if (mode !== 'all' && th.mode !== mode) return false;
      if (animFilter === 'animated' && !th.animated) return false;
      if (animFilter === 'static' && th.animated) return false;
      if (!q) return true;
      return (
        th.name.toLowerCase().includes(q) ||
        th.author.toLowerCase().includes(q) ||
        th.description.toLowerCase().includes(q) ||
        (th.tags || []).some(tag => tag.includes(q))
      );
    });
    // Name is the stable tie-breaker — keeps ordering deterministic when many
    // themes share the same last-modified date.
    const byName = (a: RegistryTheme, b: RegistryTheme) => a.name.localeCompare(b.name);
    const bySort =
      sortMode === 'name'
        ? byName
        : (a: RegistryTheme, b: RegistryTheme) =>
            (b.updatedAt || '').localeCompare(a.updatedAt || '') || byName(a, b);
    // Installed themes with a pending update float to the top (in either sort
    // mode) so the user finds them instead of hunting through the catalogue.
    const hasUpdate = (th: RegistryTheme) => {
      const inst = installedMap.get(th.id);
      return inst ? isNewer(th.version, inst.version) : false;
    };
    return matched.sort((a, b) => Number(hasUpdate(b)) - Number(hasUpdate(a)) || bySort(a, b));
  }, [themes, query, mode, sortMode, animFilter, installedMap]);

  // A changed filter can shrink the result set below the current page; reset to
  // the first page whenever the query or mode filter changes.
  // React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effect’s dependencies change.
  // eslint-disable-next-line react-hooks/set-state-in-effect
  useEffect(() => { setPage(1); }, [query, mode, sortMode, animFilter]);

  const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  // Clamp defensively so a stale `page` (e.g. after the registry shrank) never
  // points past the end and shows a blank list.
  const safePage = Math.min(page, pageCount);
  const pageItems = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);

  const goToPage = (n: number) => {
    setPage(Math.min(Math.max(1, n), pageCount));
    // Start the new page from the top of the store instead of mid-scroll.
    topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };

  const handleInstall = async (th: RegistryTheme) => {
    setBusyId(th.id);
    setFailedId(null);
    const result = await installThemeFromRegistry(th);
    if (result !== 'ok') setFailedId(th.id);
    setBusyId(null);
  };


  const modeBtns: { key: ModeFilter; label: string }[] = [
    { key: 'all', label: t('settings.themeStoreModeAll') },
    { key: 'dark', label: t('settings.themeStoreModeDark') },
    { key: 'light', label: t('settings.themeStoreModeLight') },
  ];

  const sortOptions = [
    { value: 'newest', label: t('settings.themeStoreSortNewest') },
    { value: 'name', label: t('settings.themeStoreSortName') },
  ];

  const animFilterOptions = [
    { value: 'all', label: t('settings.themeStoreAnimAll') },
    { value: 'animated', label: t('settings.themeStoreAnimAnimated') },
    { value: 'static', label: t('settings.themeStoreAnimStatic') },
  ];

  return (
    <div className="settings-card">
      {/* Submit-your-own-theme hint */}
      <div className="settings-hint settings-hint-info" style={{ marginBottom: '1rem' }}>
        {t('settings.themeStoreSubmitText')}{' '}
        <button
          type="button"
          onClick={() => void openUrl(THEMES_REPO_URL)}
          style={{ background: 'none', border: 'none', padding: 0, font: 'inherit', color: 'var(--accent)', cursor: 'pointer', textDecoration: 'underline' }}
        >
          {t('settings.themeStoreSubmitLink')}
        </button>
      </div>

      {/* Network disclosure — the store reaches external services. */}
      <p style={{ fontSize: 12, color: 'var(--text-muted)', margin: '0 0 1rem', lineHeight: 1.5 }}>
        {t('settings.themeStoreNetworkNotice')}{' '}{t('settings.themeStoreStatsNotice')}
      </p>

      {/* Toolbar: search + mode filter + refresh. Hidden when offline with no
          catalogue to browse — the offline banner below stands in for it. */}
      {!error && (
      <div ref={topRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginBottom: '1rem', scrollMarginTop: 8 }}>
        <input
          type="search"
          className="input"
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder={t('settings.themeStoreSearchPlaceholder')}
          aria-label={t('settings.themeStoreSearchPlaceholder')}
          style={{ flex: '1 1 180px', minWidth: 140 }}
        />
        <CustomSelect
          value={sortMode}
          options={sortOptions}
          onChange={v => setSortMode(v as SortMode)}
          style={{
            width: 170,
            flexShrink: 0,
            alignSelf: 'stretch',
            boxSizing: 'border-box',
            padding: '0 var(--space-4)',
            background: 'var(--input-bg)',
            border: '1px solid var(--input-border)',
            borderRadius: 'var(--radius-md)',
            fontSize: 14,
            lineHeight: 1,
          }}
        />
        <CustomSelect
          value={animFilter}
          options={animFilterOptions}
          onChange={v => setAnimFilter(v as AnimFilter)}
          style={{
            width: 150,
            flexShrink: 0,
            alignSelf: 'stretch',
            boxSizing: 'border-box',
            padding: '0 var(--space-4)',
            background: 'var(--input-bg)',
            border: '1px solid var(--input-border)',
            borderRadius: 'var(--radius-md)',
            fontSize: 14,
            lineHeight: 1,
          }}
        />
        <div style={{ display: 'flex', gap: 4 }} role="group" aria-label={t('settings.themeStoreFilterMode')}>
          {modeBtns.map(b => (
            <button
              key={b.key}
              className={`btn ${mode === b.key ? 'btn-primary' : 'btn-ghost'}`}
              style={{ fontSize: 12, padding: '4px 10px' }}
              aria-pressed={mode === b.key}
              onClick={() => setMode(b.key)}
            >
              {b.label}
            </button>
          ))}
        </div>
        <button
          className="btn btn-ghost"
          style={{ padding: '4px 10px' }}
          onClick={() => load(true)}
          disabled={loading || refreshing}
          aria-label={t('settings.themeStoreRefresh')}
          data-tooltip={t('settings.themeStoreRefresh')}
          data-tooltip-pos="left"
        >
          <RefreshCw size={15} className={refreshing ? 'animate-spin' : ''} />
        </button>
      </div>
      )}

      {!loading && stale && (
        <div className="settings-hint settings-hint-info" role="status" style={{ marginBottom: '0.75rem' }}>
          {t('settings.themeStoreOffline')}
        </div>
      )}

      {loading && (
        <p role="status" style={{ fontSize: 13, color: 'var(--text-muted)' }}>{t('settings.themeStoreLoading')}</p>
      )}

      {!loading && error && (
        <div
          role="alert"
          style={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            textAlign: 'center',
            gap: 12,
            padding: '28px 16px',
            border: '1px dashed var(--border)',
            borderRadius: 'var(--radius-md, 10px)',
            background: 'var(--bg-elevated)',
          }}
        >
          <WifiOff size={28} style={{ color: 'var(--text-muted)' }} aria-hidden="true" />
          <div>
            <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>{t('settings.themeStoreOfflineTitle')}</div>
            <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>{t('settings.themeStoreError')}</div>
          </div>
          <button className="btn btn-ghost" onClick={() => load(true)} disabled={refreshing}>
            {t('settings.themeStoreRetry')}
          </button>
        </div>
      )}

      {!loading && !error && filtered.length === 0 && (
        <p role="status" style={{ fontSize: 13, color: 'var(--text-muted)' }}>{t('settings.themeStoreEmpty')}</p>
      )}

      {!loading && !error && filtered.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {pageItems.map((th, idx) => {
            const inst = installedMap.get(th.id);
            const isInstalled = !!inst;
            const updateAvailable = isInstalled && isNewer(th.version, inst!.version);
            const isActive = activeTheme === th.id;
            const busy = busyId === th.id;
            const changelogEntries = th.changelog ? sortedChangelog(th.changelog) : [];
            const changelogOpen = openChangelogIds.has(th.id);
            return (
              <div
                key={th.id}
                className="theme-store-row"
                style={{
                  display: 'flex',
                  flexWrap: 'wrap',
                  gap: 14,
                  padding: 12,
                  border: `1px solid ${isActive ? 'var(--accent)' : 'var(--border)'}`,
                  borderRadius: 'var(--radius-md, 10px)',
                  // Subtle zebra striping so adjacent rows read as distinct boxes.
                  background: idx % 2 === 1 ? 'var(--bg-hover)' : 'var(--bg-card)',
                }}
              >
                <button
                  type="button"
                  onClick={() => setLightbox({ src: thumbUrl(th.thumbnail), name: th.name })}
                  aria-label={t('settings.themeStoreEnlarge')}
                  data-tooltip={t('settings.themeStoreEnlarge')}
                  data-tooltip-pos="right"
                  style={{ padding: 0, border: 'none', background: 'none', cursor: 'zoom-in', flexShrink: 0, alignSelf: 'flex-start', lineHeight: 0, borderRadius: 6 }}
                >
                  <img
                    src={thumbUrl(th.thumbnail)}
                    alt=""
                    loading="lazy"
                    width={200}
                    height={112}
                    // Offline / missing thumbnail: hide the broken-image glyph; the
                    // image's own neutral background stands in as a placeholder.
                    onError={e => { e.currentTarget.style.opacity = '0'; }}
                    style={{ width: 200, height: 112, objectFit: 'cover', borderRadius: 6, background: 'var(--bg-deep)' }}
                  />
                </button>
                <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <span style={{ fontWeight: 600 }}>{th.name}</span>
                    {isActive && (
                      <span style={{ fontSize: 11, color: 'var(--accent)', display: 'inline-flex', alignItems: 'center', gap: 3 }}>
                        <Check size={12} /> {t('settings.themeStoreActive')}
                      </span>
                    )}
                    {animRisk && th.animated && <AnimatedThemeBadge variant="inline" />}
                  </div>
                  <div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
                    {t('settings.themeStoreByAuthor', { author: th.author })}
                    {' · '}
                    {updateAvailable ? (
                      <>v{inst!.version} <span style={{ color: 'var(--accent)' }}>→ v{th.version}</span></>
                    ) : (
                      <>v{th.version}</>
                    )}
                  </div>
                  <div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.4, marginTop: 10 }}>
                    {th.description}
                  </div>
                  {th.updatedAt && (
                    <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 8 }}>
                      {t('settings.themeStoreLastChanged')}: {formatRelativeTime(th.updatedAt, i18n.language)}
                    </div>
                  )}
                  {changelogEntries.length > 0 && (
                    <div style={{ marginTop: 10 }}>
                      <button
                        type="button"
                        className="btn btn-ghost"
                        aria-expanded={changelogOpen}
                        aria-controls={`theme-changelog-${th.id}`}
                        onClick={() => toggleChangelog(th.id)}
                        style={{ fontSize: 12, padding: '2px 8px', display: 'inline-flex', alignItems: 'center', gap: 5 }}
                      >
                        <ChevronDown
                          size={14}
                          style={{ transform: changelogOpen ? 'rotate(180deg)' : 'none', transition: 'transform 120ms' }}
                        />
                        {t('settings.themeStoreWhatsNew')}
                      </button>
                      {changelogOpen && (
                        <div
                          id={`theme-changelog-${th.id}`}
                          style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 8 }}
                        >
                          {/* Card shows only the latest version's notes; sortedChangelog is newest-first. */}
                          {changelogEntries.slice(0, 1).map(([version, lines]) => (
                            <div key={version}>
                              <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>
                                v{version}
                              </div>
                              <ul style={{ margin: '2px 0 0', paddingInlineStart: 18, fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
                                {lines.map((line, i) => (
                                  <li key={i}>{line}</li>
                                ))}
                              </ul>
                            </div>
                          ))}
                        </div>
                      )}
                    </div>
                  )}
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 12 }}>
                    {!isInstalled && (
                      <button
                        className="btn btn-primary"
                        style={{ fontSize: 12, padding: '4px 12px', display: 'inline-flex', alignItems: 'center', gap: 5 }}
                        onClick={() => handleInstall(th)}
                        disabled={busy}
                      >
                        <Download size={14} /> {busy ? t('settings.themeStoreInstalling') : t('settings.themeStoreInstall')}
                      </button>
                    )}
                    {isInstalled && !isActive && (
                      <button
                        className="btn btn-ghost"
                        style={{ fontSize: 12, padding: '4px 12px' }}
                        onClick={() => setTheme(th.id)}
                      >
                        {t('settings.themeStoreApply')}
                      </button>
                    )}
                    {updateAvailable && (
                      <button
                        className="btn btn-primary"
                        style={{ fontSize: 12, padding: '4px 12px' }}
                        onClick={() => handleInstall(th)}
                        disabled={busy}
                      >
                        {busy ? t('settings.themeStoreUpdating') : t('settings.themeStoreUpdate')}
                      </button>
                    )}
                    {isInstalled && (
                      <button
                        className="btn btn-ghost"
                        style={{ fontSize: 12, padding: '4px 12px', display: 'inline-flex', alignItems: 'center', gap: 5 }}
                        onClick={() => uninstallTheme(th.id)}
                      >
                        <Trash2 size={14} /> {t('settings.themeStoreUninstall')}
                      </button>
                    )}
                    {failedId === th.id && (
                      <span role="status" style={{ fontSize: 12, color: 'var(--danger)', alignSelf: 'center' }}>
                        {t('settings.themeStoreInstallFailed')}
                      </span>
                    )}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}

      {!loading && !error && pageCount > 1 && (
        <div
          role="navigation"
          aria-label={t('settings.themeStorePageStatus', { page: safePage, total: pageCount })}
          style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexWrap: 'wrap', gap: 6, marginTop: 16 }}
        >
          <button
            className="btn btn-ghost"
            style={{ padding: '4px 10px' }}
            onClick={() => goToPage(safePage - 1)}
            disabled={safePage <= 1}
            aria-label={t('settings.themeStorePagePrev')}
            data-tooltip={t('settings.themeStorePagePrev')}
            data-tooltip-pos="top"
          >
            <ChevronLeft size={16} />
          </button>
          {pageItemsList(safePage, pageCount).map((it, i) =>
            it === 'gap' ? (
              <span key={`gap-${i}`} style={{ color: 'var(--text-muted)', padding: '0 2px' }}>…</span>
            ) : (
              <button
                key={it}
                className={`btn ${it === safePage ? 'btn-primary' : 'btn-ghost'}`}
                style={{ fontSize: 12.5, padding: '4px 10px', minWidth: 34 }}
                aria-current={it === safePage ? 'page' : undefined}
                onClick={() => goToPage(it)}
              >
                {it}
              </button>
            )
          )}
          <button
            className="btn btn-ghost"
            style={{ padding: '4px 10px' }}
            onClick={() => goToPage(safePage + 1)}
            disabled={safePage >= pageCount}
            aria-label={t('settings.themeStorePageNext')}
            data-tooltip={t('settings.themeStorePageNext')}
            data-tooltip-pos="top"
          >
            <ChevronRight size={16} />
          </button>
        </div>
      )}

      {lightbox && (
        <CoverLightbox src={lightbox.src} alt={lightbox.name} onClose={() => setLightbox(null)} />
      )}
    </div>
  );
}
