﻿const { useEffect, useMemo, useRef, useState } = React;
const QUEUE_VISIBILITY_KEY = 'slideshow.showQueue';
const SCHEDULE_ENABLED_KEY = 'slideshow.schedule.enabled';
const SCHEDULE_START_KEY = 'slideshow.schedule.start';
const SCHEDULE_END_KEY = 'slideshow.schedule.end';
const CAPTION_THEME_KEY = 'slideshow.caption.theme';
const NEW_IMAGE_WINDOW_MS = 2 * 24 * 60 * 60 * 1000;

function shouldAutoFullscreen() {
  try {
    const value = new URLSearchParams(window.location.search).get('fullscreen');
    if (!value) return false;
    return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase());
  } catch {
    return false;
  }
}

function getFullscreenElement() {
  return document.fullscreenElement || document.webkitFullscreenElement || null;
}

function isRecentImage(image) {
  if (!image || !image.modifiedAt) return false;
  const time = new Date(image.modifiedAt).getTime();
  if (!Number.isFinite(time)) return false;
  return (Date.now() - time) <= NEW_IMAGE_WINDOW_MS;
}

function getStoredValue(key, fallback) {
  try {
    const value = window.localStorage.getItem(key);
    if (value === null || value === undefined || value === '') return fallback;
    return value;
  } catch {
    return fallback;
  }
}

function parseClockMinutes(value) {
  if (typeof value !== 'string') return null;
  const parts = value.split(':');
  if (parts.length !== 2) return null;
  const hours = Number(parts[0]);
  const minutes = Number(parts[1]);
  if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return null;
  if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
  return (hours * 60) + minutes;
}

function isNowInScheduleWindow(startTime, endTime) {
  const start = parseClockMinutes(startTime);
  const end = parseClockMinutes(endTime);
  if (start === null || end === null) return true;

  const now = new Date();
  const current = (now.getHours() * 60) + now.getMinutes();

  if (start === end) return true;
  if (start < end) {
    return current >= start && current < end;
  }
  return current >= start || current < end;
}

function renderStars(rating) {
  const count = Number(rating);
  if (!Number.isFinite(count) || count < 1) return '';
  return '★'.repeat(Math.max(1, Math.min(5, Math.floor(count))));
}

function UserMenu({ user, onLogout, onChangePassword }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    function handler(e) {
      if (ref.current && !ref.current.contains(e.target)) setOpen(false);
    }
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, []);

  const initial = (user || '?').charAt(0).toUpperCase();

  return (
    <div className="user-menu" ref={ref}>
      <button
        type="button"
        className="user-menu-trigger"
        aria-haspopup="true"
        aria-expanded={open}
        onClick={() => setOpen(o => !o)}
      >
        <span className="user-avatar">{initial}</span>
        <span className="user-menu-name">{user}</span>
        <svg className={`user-chevron${open ? ' open' : ''}`} viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
          <path d="M7 10l5 5 5-5z"/>
        </svg>
      </button>
      {open && (
        <div className="user-menu-dropdown" role="menu">
          <div className="user-menu-header">
            <span className="user-avatar lg">{initial}</span>
            <span className="user-menu-fullname">{user}</span>
          </div>
          <hr className="user-menu-sep" />
          <a
            className="user-menu-item"
            role="menuitem"
            href="/remote.html"
            target="_blank"
            rel="noreferrer"
            onClick={() => setOpen(false)}
          >
            <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
              <path d="M12 2a10 10 0 0 0-7.07 17.07l1.41-1.41A8 8 0 1 1 20 12h2A10 10 0 0 0 12 2zm0 4a6 6 0 0 0-4.24 10.24l1.42-1.41A4 4 0 1 1 16 12h2a6 6 0 0 0-6-6zm0 4a2 2 0 0 0-1.41 3.41l2.82-2.82A2 2 0 0 0 12 10z"/>
            </svg>
            Remote
          </a>
          <button
            type="button"
            className="user-menu-item"
            role="menuitem"
            onClick={() => { setOpen(false); onChangePassword(); }}
          >
            <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
              <path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm-3 8V6a3 3 0 1 1 6 0v3H9zm3 4a2 2 0 0 1 1 3.73V19h-2v-2.27A2 2 0 0 1 12 13z"/>
            </svg>
            Change password
          </button>
          <button
            type="button"
            className="user-menu-item"
            role="menuitem"
            onClick={() => { setOpen(false); onLogout(); }}
          >
            <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true">
              <path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
            </svg>
            Log out
          </button>
        </div>
      )}
    </div>
  );
}

function ExifChips({ exif }) {
  if (!exif) return null;

  const chips = [];
  if (exif.DateTimeOriginal) {
    const date = new Date(exif.DateTimeOriginal);
    if (!Number.isNaN(date.getTime())) {
      chips.push({ key: 'date', label: `Date: ${date.toLocaleString()}` });
    }
  }
  if (exif.Make || exif.Model) {
    chips.push({ key: 'camera', label: `Camera: ${[exif.Make, exif.Model].filter(Boolean).join(' ')}` });
  }
  if (exif.LensModel) {
    chips.push({ key: 'lens', label: `Lens: ${exif.LensModel}` });
  }

  if (!chips.length) return null;

  return (
    <div className="exif-row">
      {chips.map((chip) => (
        <span key={chip.key} className="exif-chip">{chip.label}</span>
      ))}
    </div>
  );
}

function ExifDetails({ exif }) {
  if (!exif) {
    return <p className="exif-empty">No EXIF data available for this image.</p>;
  }

  const rows = Object.entries(exif).map(([key, value]) => {
    let display;
    if (value instanceof Object && !Array.isArray(value)) {
      display = JSON.stringify(value);
    } else if (Array.isArray(value)) {
      display = value.join(', ');
    } else {
      display = String(value);
    }
    return { key, display };
  });

  if (!rows.length) {
    return <p className="exif-empty">No EXIF data available for this image.</p>;
  }

  return (
    <table className="exif-table">
      <tbody>
        {rows.map(({ key, display }) => (
          <tr key={key}>
            <th>{key}</th>
            <td>{display}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function ImageCaption({ image, theme }) {
  if (!image) {
    return <figcaption className="slide-caption">No image files found in the configured directory.</figcaption>;
  }

  const displayLabel = (image.title || '').trim() || image.name;
  const formattedModifiedAt = new Date(image.modifiedAt).toLocaleString(undefined, {
    month: 'long',
    day: 'numeric',
    year: 'numeric',
    hour: 'numeric',
    minute: '2-digit'
  });
  const attributionName =
    (image.contributorName || '').trim() ||
    (image.uploadedBy || '').trim() ||
    'Unknown contributor';

  return (
    <figcaption className={`slide-caption slide-caption-theme-${theme || 'classic'}`}>
      {image.title ? <div className="caption-title">{image.title}</div> : null}
      {image.description ? (
        <div className="caption-desc">
          {image.description}
          {image.locationLabel ? ` (${image.locationLabel})` : ''}
        </div>
      ) : image.locationLabel ? (
        <div className="caption-desc">{image.locationLabel}</div>
      ) : null}
      <div className="caption-meta">
        {attributionName}, added at {formattedModifiedAt}
      </div>
      {Number.isFinite(image.rating) ? <div className="caption-rating">Rated: {renderStars(image.rating)} ({image.rating}/5)</div> : null}
      <ExifChips exif={image.exif} />
    </figcaption>
  );
}

function App() {
  const [images, setImages] = useState([]);
  const [index, setIndex] = useState(0);
  const [status, setStatus] = useState({ text: 'Loading image list...', css: '' });
  const [uploadResult, setUploadResult] = useState('');
  const [intervalSeconds, setIntervalSeconds] = useState(7);
  const [uploadTitle, setUploadTitle] = useState('');
  const [uploadDescription, setUploadDescription] = useState('');
  const [editMode, setEditMode] = useState(false);
  const [showUpload, setShowUpload] = useState(false);
  const [editTitle, setEditTitle] = useState('');
  const [editDescription, setEditDescription] = useState('');
  const [editSaving, setEditSaving] = useState(false);
  const [editResult, setEditResult] = useState('');
  const [showExif, setShowExif] = useState(false);
  const [user, setUser] = useState('');
  const [authChecked, setAuthChecked] = useState(false);
  const [loginUsername, setLoginUsername] = useState('');
  const [loginPassword, setLoginPassword] = useState('');
  const [loginSubmitting, setLoginSubmitting] = useState(false);
  const [loginError, setLoginError] = useState('');
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [isPlaying, setIsPlaying] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [deleting, setDeleting] = useState(false);
  const [showTrash, setShowTrash] = useState(false);
  const [onlyTrashed, setOnlyTrashed] = useState(false);
  const [onlyFavorites, setOnlyFavorites] = useState(false);
  const [minRatingFilter, setMinRatingFilter] = useState(0);
  const [tagFilter, setTagFilter] = useState('');
  const [sortMode, setSortMode] = useState('recent');
  const [captionTheme, setCaptionTheme] = useState(() => getStoredValue(CAPTION_THEME_KEY, 'classic'));
  const [scheduleEnabled, setScheduleEnabled] = useState(() => getStoredValue(SCHEDULE_ENABLED_KEY, 'false') === 'true');
  const [scheduleStart, setScheduleStart] = useState(() => getStoredValue(SCHEDULE_START_KEY, '08:00'));
  const [scheduleEnd, setScheduleEnd] = useState(() => getStoredValue(SCHEDULE_END_KEY, '22:00'));
  const [editTags, setEditTags] = useState('');
  const [selectedNames, setSelectedNames] = useState([]);
  const [bulkWorking, setBulkWorking] = useState(false);
  const [dupesLoading, setDupesLoading] = useState(false);
  const [duplicateGroups, setDuplicateGroups] = useState([]);
  const [batchTitle, setBatchTitle] = useState('');
  const [batchDescription, setBatchDescription] = useState('');
  const [batchTags, setBatchTags] = useState('');
  const [batchRating, setBatchRating] = useState('');
  const [showQueue, setShowQueue] = useState(() => {
    try {
      const saved = window.localStorage.getItem(QUEUE_VISIBILITY_KEY);
      if (saved === 'true') return true;
      if (saved === 'false') return false;
    } catch {
      // Ignore storage read errors and keep default behavior.
    }
    return true;
  });
  const [showPasswordModal, setShowPasswordModal] = useState(false);
  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [passwordSaving, setPasswordSaving] = useState(false);
  const [passwordError, setPasswordError] = useState('');
  const remoteSeqRef = useRef(null);
  const autoFullscreenEnabledRef = useRef(shouldAutoFullscreen());
  const autoPlayEnabledRef = useRef(shouldAutoFullscreen());
  const autoFullscreenAttemptedRef = useRef(false);
  const autoPlayAttemptedRef = useRef(false);
  const timerRef = useRef(null);
  const fileInputRef = useRef(null);
  const viewerRef = useRef(null);

  const current = useMemo(() => {
    if (!images.length) {
      return null;
    }

    return images[index % images.length];
  }, [images, index]);

  const trashedImages = useMemo(() => images.filter((img) => img.trashedAt), [images]);
  const selectedTrashedNames = useMemo(
    () => selectedNames.filter((name) => images.some((img) => img.name === name && img.trashedAt)),
    [selectedNames, images]
  );

  const activeFilterChips = useMemo(() => {
    const chips = [];
    if (showTrash) chips.push('Show trash');
    if (onlyTrashed) chips.push('Trash only');
    if (onlyFavorites) chips.push('Favorites only');
    if (minRatingFilter > 0) chips.push(`Rating >= ${minRatingFilter}`);
    if (tagFilter.trim()) chips.push(`Tag: ${tagFilter.trim()}`);
    if (sortMode !== 'recent') chips.push(`Sort: ${sortMode}`);
    return chips;
  }, [showTrash, onlyTrashed, onlyFavorites, minRatingFilter, tagFilter, sortMode]);

  useEffect(() => {
    try {
      window.localStorage.setItem(QUEUE_VISIBILITY_KEY, String(showQueue));
    } catch {
      // Ignore storage write errors.
    }
  }, [showQueue]);

  useEffect(() => {
    try {
      window.localStorage.setItem(CAPTION_THEME_KEY, captionTheme);
    } catch {
      // Ignore storage write errors.
    }
  }, [captionTheme]);

  useEffect(() => {
    try {
      window.localStorage.setItem(SCHEDULE_ENABLED_KEY, String(scheduleEnabled));
      window.localStorage.setItem(SCHEDULE_START_KEY, scheduleStart);
      window.localStorage.setItem(SCHEDULE_END_KEY, scheduleEnd);
    } catch {
      // Ignore storage write errors.
    }
  }, [scheduleEnabled, scheduleStart, scheduleEnd]);

  function stopAutoPlay() {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
    setIsPlaying(false);
  }

  function nextImage() {
    if (!images.length) {
      return;
    }

    setIndex((prev) => (prev + 1) % images.length);
  }

  function previousImage() {
    if (!images.length) {
      return;
    }

    setIndex((prev) => (prev - 1 + images.length) % images.length);
  }

  async function toggleFullscreen() {
    try {
      const fullElement = getFullscreenElement();
      if (!fullElement) {
        const target = viewerRef.current;
        if (!target || !target.requestFullscreen) return;
        await target.requestFullscreen();
        setIsFullscreen(true);
      } else {
        if (document.exitFullscreen) {
          await document.exitFullscreen();
        } else if (document.webkitExitFullscreen) {
          document.webkitExitFullscreen();
        }
        setIsFullscreen(false);
      }
    } catch (err) {
      console.error('Fullscreen toggle failed:', err);
    }
  }

  useEffect(() => {
    const handleFullscreenChange = () => {
      setIsFullscreen(Boolean(getFullscreenElement()));
    };
    document.addEventListener('fullscreenchange', handleFullscreenChange);
    document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
    return () => {
      document.removeEventListener('fullscreenchange', handleFullscreenChange);
      document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
    };
  }, []);

  useEffect(() => {
    if (!autoPlayEnabledRef.current || autoPlayAttemptedRef.current) {
      return;
    }
    if (!authChecked || !images.length) {
      return;
    }

    autoPlayAttemptedRef.current = true;
    startAutoPlay();
  }, [authChecked, images.length]);

  useEffect(() => {
    if (!scheduleEnabled || !authChecked || !user || !images.length) {
      return;
    }

    const runTick = () => {
      const allowed = isNowInScheduleWindow(scheduleStart, scheduleEnd);
      if (allowed && !isPlaying) {
        startAutoPlay();
        setStatus({ text: 'Schedule mode resumed autoplay.', css: 'ok' });
        return;
      }
      if (!allowed && isPlaying) {
        stopAutoPlay();
        setStatus({ text: 'Schedule mode paused autoplay (outside scheduled hours).', css: 'warn' });
      }
    };

    runTick();
    const id = setInterval(runTick, 30000);
    return () => clearInterval(id);
  }, [scheduleEnabled, scheduleStart, scheduleEnd, authChecked, user, images.length, isPlaying]);

  useEffect(() => {
    if (!autoFullscreenEnabledRef.current || autoFullscreenAttemptedRef.current) {
      return;
    }
    if (!authChecked || !viewerRef.current) {
      return;
    }

    autoFullscreenAttemptedRef.current = true;

    if (getFullscreenElement()) {
      setIsFullscreen(true);
      return;
    }

    const attemptFullscreen = () => {
      const target = viewerRef.current;
      if (!target || getFullscreenElement()) {
        return Promise.resolve();
      }
      return target.requestFullscreen().then(() => {
        setIsFullscreen(true);
      });
    };

    let retryHandler = null;

    attemptFullscreen().catch(() => {
      setStatus({ text: 'Auto-fullscreen was blocked by the browser. Press any key or click once to allow fullscreen.', css: 'warn' });
      retryHandler = () => {
        attemptFullscreen().catch(() => {});
      };
      window.addEventListener('pointerdown', retryHandler, { once: true });
      window.addEventListener('keydown', retryHandler, { once: true });
    });

    return () => {
      if (retryHandler) {
        window.removeEventListener('pointerdown', retryHandler);
        window.removeEventListener('keydown', retryHandler);
      }
    };
  }, [authChecked]);

  useEffect(() => {
    const imageCount = images.length;

    function isTypingTarget(target) {
      if (!(target instanceof HTMLElement)) return false;
      const tag = target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
      return target.isContentEditable;
    }

    function onKeyDown(event) {
      if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
      if (showPasswordModal) return;
      if (isTypingTarget(event.target)) return;
      if (!imageCount) return;

      if (event.key === 'ArrowRight') {
        event.preventDefault();
        setIndex((prev) => (prev + 1) % imageCount);
        return;
      }

      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        setIndex((prev) => (prev - 1 + imageCount) % imageCount);
      }
    }

    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [images.length, showPasswordModal]);

  function startAutoPlay() {
    stopAutoPlay();
    const seconds = Math.max(1, Number(intervalSeconds) || 7);
    timerRef.current = setInterval(() => {
      nextImage();
    }, seconds * 1000);
    setIsPlaying(true);
    setStatus({ text: `Autoplay running every ${seconds} second(s).`, css: 'ok' });
  }

  function togglePlay() {
    if (isPlaying) {
      stopAutoPlay();
      setStatus({ text: 'Autoplay paused.', css: 'warn' });
    } else {
      startAutoPlay();
    }
  }

  async function applyRemoteCommand(command) {
    if (!command || !command.action || command.action === 'noop') {
      return;
    }

    if (command.action === 'next') {
      nextImage();
      setStatus({ text: 'Remote: next image.', css: 'ok' });
      return;
    }

    if (command.action === 'previous') {
      previousImage();
      setStatus({ text: 'Remote: previous image.', css: 'ok' });
      return;
    }

    if (command.action === 'play') {
      if (!isPlaying) startAutoPlay();
      return;
    }

    if (command.action === 'pause') {
      if (isPlaying) {
        stopAutoPlay();
        setStatus({ text: 'Remote: autoplay paused.', css: 'warn' });
      }
      return;
    }

    if (command.action === 'togglePlay') {
      togglePlay();
      return;
    }

    if (command.action === 'reloadImages') {
      await loadImages();
      setStatus({ text: 'Remote: image list reloaded.', css: 'ok' });
      return;
    }

    if (command.action === 'setInterval') {
      const seconds = Number(command.payload && command.payload.seconds);
      if (Number.isFinite(seconds) && seconds >= 1 && seconds <= 120) {
        setIntervalSeconds(seconds);
        if (isPlaying) {
          stopAutoPlay();
          setTimeout(() => startAutoPlay(), 0);
        }
        setStatus({ text: `Remote: interval set to ${seconds}s.`, css: 'ok' });
      }
    }
  }

  async function checkSession() {
    const response = await fetch('/api/auth/session', { cache: 'no-store' });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Session check failed (${response.status})`);
    }
    if (payload.authenticated && payload.user) {
      const displayName = payload.displayName || payload.user;
      setUser(displayName);
      return displayName;
    }
    setUser('');
    return '';
  }

  async function login(event) {
    event.preventDefault();
    setLoginSubmitting(true);
    setLoginError('');

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: loginUsername, password: loginPassword })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Login failed (${response.status})`);
      }

      setUser(payload.displayName || payload.user || loginUsername);
      setLoginPassword('');
      await loadImages();
    } catch (err) {
      setLoginError(err.message);
    } finally {
      setLoginSubmitting(false);
    }
  }

  async function logout() {
    await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
    stopAutoPlay();
    setUser('');
    setImages([]);
    setIndex(0);
    setShowExif(false);
    setEditMode(false);
    setShowPasswordModal(false);
    setCurrentPassword('');
    setNewPassword('');
    setConfirmPassword('');
    setPasswordError('');
  }

  function openPasswordModal() {
    setPasswordError('');
    setCurrentPassword('');
    setNewPassword('');
    setConfirmPassword('');
    setShowPasswordModal(true);
  }

  function closePasswordModal() {
    if (passwordSaving) return;
    setShowPasswordModal(false);
    setPasswordError('');
  }

  async function submitPasswordChange(event) {
    event.preventDefault();
    setPasswordError('');

    if (!currentPassword || !newPassword || !confirmPassword) {
      setPasswordError('All fields are required.');
      return;
    }
    if (newPassword !== confirmPassword) {
      setPasswordError('New passwords do not match.');
      return;
    }
    if (newPassword.length < 8) {
      setPasswordError('New password must be at least 8 characters long.');
      return;
    }

    setPasswordSaving(true);
    try {
      const response = await fetch('/api/auth/change-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ currentPassword, newPassword })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Password change failed (${response.status})`);
      }

      setShowPasswordModal(false);
      setCurrentPassword('');
      setNewPassword('');
      setConfirmPassword('');
      setStatus({ text: 'Password changed successfully.', css: 'ok' });
    } catch (err) {
      setPasswordError(err.message);
    } finally {
      setPasswordSaving(false);
    }
  }

  async function loadImages() {
    setStatus({ text: 'Loading image list...', css: '' });

    const params = new URLSearchParams();
    if (showTrash || onlyTrashed) params.set('includeTrashed', '1');
    if (onlyTrashed) params.set('onlyTrashed', '1');
    if (onlyFavorites) params.set('onlyFavorites', '1');
    if (minRatingFilter > 0) params.set('minRating', String(minRatingFilter));
    if (tagFilter.trim()) params.set('tag', tagFilter.trim().toLowerCase());
    if (sortMode) params.set('sort', sortMode);

    const query = params.toString();
    const response = await fetch(`/api/images${query ? `?${query}` : ''}`, { cache: 'no-store' });
    if (!response.ok) {
      if (response.status === 401) {
        setUser('');
      }
      throw new Error(`Image list failed (${response.status})`);
    }

    const payload = await response.json();
    const incoming = Array.isArray(payload.images) ? payload.images : [];

    setImages(incoming);
    setIndex(0);

    if (!incoming.length) {
      setStatus({ text: 'No images found. Upload files to begin.', css: 'warn' });
    } else {
      setStatus({ text: `Loaded ${incoming.length} image(s).`, css: 'ok' });
    }
  }

  async function onUploadSubmit(event) {
    event.preventDefault();

    const files = fileInputRef.current?.files;
    if (!files || !files.length) {
      setUploadResult('Choose at least one file first.');
      return;
    }

    const formData = new FormData();
    for (const file of files) {
      formData.append('files', file);
    }
    formData.append('title', uploadTitle);
    formData.append('description', uploadDescription);

    setUploadResult('Uploading...');

    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData
    });

    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Upload failed (${response.status})`);
    }

    const uploadedCount = Array.isArray(payload.uploaded) ? payload.uploaded.length : 0;
    setUploadResult(`Uploaded ${uploadedCount} file(s).`);
    setUploadTitle('');
    setUploadDescription('');
    event.target.reset();
    setShowUpload(false);
    await loadImages();
  }

  function openEdit() {
    if (!current) return;
    setEditTitle(current.title || '');
    setEditDescription(current.description || '');
    setEditTags(Array.isArray(current.tags) ? current.tags.join(', ') : '');
    setEditResult('');
    setEditMode(true);
    setShowExif(false);
  }

  function toggleEdit() {
    if (editMode) {
      setEditMode(false);
      setEditResult('');
      return;
    }
    setShowUpload(false);
    setShowQueue(false);
    openEdit();
  }

  function toggleUpload() {
    setShowUpload(v => {
      if (!v) { setEditMode(false); setShowExif(false); setShowQueue(false); }
      return !v;
    });
  }

  async function saveEdit(event) {
    event.preventDefault();
    if (!current) return;
    setEditSaving(true);
    setEditResult('');
    try {
      const response = await fetch(`/api/images/${encodeURIComponent(current.name)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: editTitle, description: editDescription, tags: editTags })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Save failed (${response.status})`);
      }
      setImages((prev) => prev.map((img) =>
        img.name === current.name
          ? {
            ...img,
            title: payload.title || '',
            description: payload.description || '',
            tags: Array.isArray(payload.tags) ? payload.tags : img.tags
          }
          : img
      ));
      setEditResult('');
      setEditMode(false);
      setStatus({ text: 'Details saved.', css: 'ok' });
    } catch (err) {
      setEditResult(err.message);
    } finally {
      setEditSaving(false);
    }
  }

  async function patchCurrentMeta(patch, successText) {
    if (!current) return;
    const response = await fetch(`/api/images/${encodeURIComponent(current.name)}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch)
    });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Update failed (${response.status})`);
    }
    setImages((prev) => prev.map((img) =>
      img.name === current.name
        ? {
          ...img,
          favorite: payload.favorite !== undefined ? payload.favorite : img.favorite,
          tags: Array.isArray(payload.tags) ? payload.tags : img.tags,
          pinnedRank: payload.pinnedRank !== undefined ? payload.pinnedRank : img.pinnedRank,
          rating: payload.rating !== undefined ? payload.rating : img.rating
        }
        : img
    ));
    setStatus({ text: successText, css: 'ok' });
  }

  async function toggleFavorite() {
    if (!current) return;
    try {
      await patchCurrentMeta({ favorite: !current.favorite }, current.favorite ? 'Removed from favorites.' : 'Marked as favorite.');
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  async function restoreImage() {
    if (!current) return;
    try {
      const response = await fetch(`/api/images/${encodeURIComponent(current.name)}/restore`, { method: 'POST' });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Restore failed (${response.status})`);
      }
      await loadImages();
      setStatus({ text: 'Image restored from trash.', css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  async function restoreByName(filename) {
    const response = await fetch(`/api/images/${encodeURIComponent(filename)}/restore`, { method: 'POST' });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Restore failed (${response.status})`);
    }
  }

  async function hardDeleteByName(filename) {
    const response = await fetch(`/api/images/${encodeURIComponent(filename)}?hard=1`, { method: 'DELETE' });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Delete failed (${response.status})`);
    }
  }

  async function patchImageMetaByName(filename, patch) {
    const response = await fetch(`/api/images/${encodeURIComponent(filename)}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch)
    });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(payload.error || `Update failed (${response.status})`);
    }
    return payload;
  }

  async function loadDuplicateGroups() {
    setDupesLoading(true);
    try {
      const response = await fetch('/api/images/duplicates', { cache: 'no-store' });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Duplicate scan failed (${response.status})`);
      }
      setDuplicateGroups(Array.isArray(payload.groups) ? payload.groups : []);
      setStatus({ text: `Duplicate scan complete (${payload.totalGroups || 0} group(s)).`, css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    } finally {
      setDupesLoading(false);
    }
  }

  async function applyBulkMetadataPatch() {
    if (!selectedNames.length) {
      setStatus({ text: 'Select one or more images before applying bulk metadata.', css: 'warn' });
      return;
    }

    const patch = {};
    if (batchTitle.trim()) patch.title = batchTitle.trim();
    if (batchDescription.trim()) patch.description = batchDescription.trim();
    if (batchTags.trim()) patch.tags = batchTags.trim();
    if (batchRating !== '') patch.rating = Number(batchRating);

    if (!Object.keys(patch).length) {
      setStatus({ text: 'Enter at least one bulk metadata value.', css: 'warn' });
      return;
    }

    setBulkWorking(true);
    try {
      const response = await fetch('/api/images/bulk', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'patchMeta', filenames: selectedNames, meta: patch })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Bulk metadata failed (${response.status})`);
      }
      await loadImages();
      setStatus({ text: `Bulk metadata applied to ${payload.count || 0} image(s).`, css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    } finally {
      setBulkWorking(false);
    }
  }

  async function setCurrentRating(nextRating) {
    if (!current) return;
    try {
      const value = current.rating === nextRating ? null : nextRating;
      await patchCurrentMeta({ rating: value }, value ? `Rating set to ${value}/5.` : 'Rating cleared.');
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  async function setRatingByName(filename, currentRating, nextRating) {
    if (!filename) return;
    const value = currentRating === nextRating ? null : nextRating;
    await patchImageMetaByName(filename, { rating: value });
  }

  function toggleSelectedName(filename) {
    setSelectedNames((prev) => (
      prev.includes(filename)
        ? prev.filter((name) => name !== filename)
        : [...prev, filename]
    ));
  }

  async function runBulkAction(action, extra = {}, explicitNames = null) {
    const targetNames = Array.isArray(explicitNames) ? explicitNames : selectedNames;

    if (!targetNames.length) {
      setStatus({ text: 'Select one or more images first.', css: 'warn' });
      return;
    }

    setBulkWorking(true);
    try {
      const response = await fetch('/api/images/bulk', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action, filenames: targetNames, ...extra })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) {
        throw new Error(payload.error || `Bulk action failed (${response.status})`);
      }
      await loadImages();
      setSelectedNames((prev) => prev.filter((name) => !targetNames.includes(name)));
      setStatus({ text: `Bulk action complete (${payload.count || 0} image(s)).`, css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    } finally {
      setBulkWorking(false);
    }
  }

  function confirmDangerous(message) {
    return window.confirm(message);
  }

  async function pinCurrent() {
    if (!current) return;
    try {
      const maxRank = images
        .map((img) => Number.isFinite(img.pinnedRank) ? img.pinnedRank : 0)
        .reduce((max, next) => Math.max(max, next), 0);
      await patchImageMetaByName(current.name, { pinnedRank: maxRank + 1 });
      await loadImages();
      setStatus({ text: 'Image pinned to queue.', css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  async function togglePinByName(filename, isPinned) {
    if (!filename) return;
    if (isPinned) {
      await patchImageMetaByName(filename, { pinnedRank: null });
      return;
    }

    const maxRank = images
      .map((img) => Number.isFinite(img.pinnedRank) ? img.pinnedRank : 0)
      .reduce((max, next) => Math.max(max, next), 0);
    await patchImageMetaByName(filename, { pinnedRank: maxRank + 1 });
  }

  async function toggleFavoriteByName(filename, isFavorite) {
    if (!filename) return;
    await patchImageMetaByName(filename, { favorite: !isFavorite });
  }

  async function unpinCurrent() {
    if (!current) return;
    try {
      await patchImageMetaByName(current.name, { pinnedRank: null });
      await loadImages();
      setStatus({ text: 'Image unpinned.', css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  async function savePinnedOrder(orderedNames) {
    for (let i = 0; i < orderedNames.length; i += 1) {
      await patchImageMetaByName(orderedNames[i], { pinnedRank: i + 1 });
    }
  }

  async function movePinnedByName(filename, direction) {
    const pinned = images
      .filter((img) => Number.isFinite(img.pinnedRank))
      .slice()
      .sort((a, b) => a.pinnedRank - b.pinnedRank);

    const idx = pinned.findIndex((img) => img.name === filename);
    if (idx < 0) return;

    const names = pinned.map((img) => img.name);
    let nextIdx = idx;
    if (direction === 'top') {
      nextIdx = 0;
    } else {
      const offset = Number(direction);
      if (!Number.isFinite(offset)) return;
      nextIdx = Math.max(0, Math.min(names.length - 1, idx + offset));
    }

    if (nextIdx === idx) return;

    const [moved] = names.splice(idx, 1);
    names.splice(nextIdx, 0, moved);
    await savePinnedOrder(names);
    await loadImages();
  }

  async function movePinnedCurrent(direction) {
    if (!current || !Number.isFinite(current.pinnedRank)) return;
    try {
      await movePinnedByName(current.name, direction);
      setStatus({ text: 'Pinned order updated.', css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
    }
  }

  useEffect(() => {
    setEditMode(false);
    setShowExif(false);
    setEditResult('');
    setConfirmDelete(false);
  }, [index]);

  async function deleteImage() {
    if (!current) return;
    setDeleting(true);
    try {
      const isHardDelete = Boolean(current.trashedAt);
      const response = await fetch(`/api/images/${encodeURIComponent(current.name)}${isHardDelete ? '?hard=1' : ''}`, {
        method: 'DELETE'
      });
      if (!response.ok) {
        const payload = await response.json().catch(() => ({}));
        throw new Error(payload.error || `Delete failed (${response.status})`);
      }
      await loadImages();
      setConfirmDelete(false);
      setStatus({ text: isHardDelete ? 'Image permanently deleted.' : 'Image moved to trash.', css: 'ok' });
    } catch (err) {
      setStatus({ text: err.message, css: 'warn' });
      setConfirmDelete(false);
    } finally {
      setDeleting(false);
    }
  }

  useEffect(() => {
    checkSession()
      .then((currentUser) => {
        if (currentUser) {
          return loadImages();
        }
        return null;
      })
      .catch((error) => {
        setStatus({ text: error.message, css: 'warn' });
      })
      .finally(() => {
        setAuthChecked(true);
      });

    return () => {
      stopAutoPlay();
    };
  }, []);

  useEffect(() => {
    if (!authChecked || !user) return;
    loadImages().catch((error) => {
      setStatus({ text: error.message, css: 'warn' });
    });
  }, [showTrash, onlyTrashed, onlyFavorites, minRatingFilter, tagFilter, sortMode]);

  useEffect(() => {
    if (!authChecked || !user) return;

    let disposed = false;
    async function pollRemote() {
      try {
        const response = await fetch('/api/control/state', { cache: 'no-store' });
        const payload = await response.json().catch(() => ({}));
        if (!response.ok || !payload.command) return;

        const command = payload.command;
        const seq = Number(command.seq);
        if (!Number.isFinite(seq)) return;

        if (remoteSeqRef.current === null) {
          remoteSeqRef.current = seq;
          return;
        }

        if (seq <= remoteSeqRef.current) return;
        remoteSeqRef.current = seq;

        if (!disposed) {
          await applyRemoteCommand(command);
        }
      } catch {
        // Ignore transient polling errors.
      }
    }

    pollRemote().catch(() => {});
    const id = setInterval(() => {
      pollRemote().catch(() => {});
    }, 2000);

    return () => {
      disposed = true;
      clearInterval(id);
    };
  }, [authChecked, user, isPlaying, intervalSeconds, index, images.length]);

  useEffect(() => {
    setSelectedNames((prev) => prev.filter((name) => images.some((img) => img.name === name)));
  }, [images]);

  if (!authChecked) {
    return (
      <>
        <div className="backdrop"></div>
        <main className="app-shell login-shell">
          <section className="panel login-panel login-panel-loading">
            <div className="login-mark" aria-hidden="true">KB</div>
            <div className="login-copy-block">
              <p className="login-kicker">Photo Slideshow</p>
              <h1>Kwanzaa Bulletin Board</h1>
              <p className="subhead login-subhead">Checking session...</p>
            </div>
          </section>
        </main>
      </>
    );
  }

  if (!user) {
    return (
      <>
        <div className="backdrop"></div>
        <main className="app-shell login-shell">
          <section className="panel login-panel">
            <div className="login-hero">
              <div className="login-mark" aria-hidden="true">KB</div>
              <div className="login-copy-block">
                <p className="login-kicker">Family Photo Slideshow</p>
                <h1>Kwanzaa Bulletin Board</h1>
                <p className="subhead login-subhead">Sign in to manage uploads, captions, and the live slideshow.</p>
                <p className="login-copy">
                  A small control room for the display: keep photos moving, clean up metadata, and make quick changes without interrupting the screen.
                </p>
              </div>
              <div className="login-board" aria-hidden="true">
                <div className="login-board-tape login-board-tape-left"></div>
                <div className="login-board-tape login-board-tape-right"></div>
                <div className="login-polaroid login-polaroid-large">
                  <div className="login-photo login-photo-one"></div>
                  <span>Family</span>
                </div>
                <div className="login-polaroid login-polaroid-tilt-right">
                  <div className="login-photo login-photo-two"></div>
                  <span>Candles</span>
                </div>
                <div className="login-note-card">
                  <strong>Tonight</strong>
                  <span>Uploads</span>
                  <span>Captions</span>
                  <span>Rotation</span>
                </div>
                <div className="login-polaroid login-polaroid-tilt-left">
                  <div className="login-photo login-photo-three"></div>
                  <span>Guests</span>
                </div>
              </div>
            </div>

            <form
              className="login-form"
              onSubmit={(event) => {
                login(event).catch((error) => setLoginError(error.message));
              }}
            >
              <div className="login-form-head">
                <p className="login-form-kicker">Admin Access</p>
                <h2 className="login-form-title">Open the control panel</h2>
                <p className="login-form-copy">Sign in to upload photos, tune captions, remove mistakes, and keep the slideshow moving.</p>
              </div>
              <div className="upload-field login-field">
                <label htmlFor="loginUsername">Username</label>
                <input
                  id="loginUsername"
                  type="text"
                  value={loginUsername}
                  onChange={(event) => setLoginUsername(event.target.value)}
                  autoComplete="username"
                  required
                />
              </div>
              <div className="upload-field login-field">
                <label htmlFor="loginPassword">Password</label>
                <input
                  id="loginPassword"
                  type="password"
                  value={loginPassword}
                  onChange={(event) => setLoginPassword(event.target.value)}
                  autoComplete="current-password"
                  required
                />
              </div>
              <div className="login-actions">
                <button className="login-submit" type="submit" disabled={loginSubmitting}>
                  {loginSubmitting ? 'Signing In...' : 'Open Board'}
                </button>
                <span className="login-side-note">Sign in to manage uploads, edits, and cleanup.</span>
              </div>
            </form>
            {loginError ? <p className="warn login-error">{loginError}</p> : null}
          </section>
        </main>
      </>
    );
  }

  return (
    <>
      <div className="backdrop"></div>

      <main className="app-shell">
        <header className="topbar">
          <div>
            <h1>Kwanzaa BBS</h1>
          </div>

          <UserMenu
            user={user}
            onLogout={() => logout().catch(() => {})}
            onChangePassword={openPasswordModal}
          />
        </header>

        <section className="viewer" ref={viewerRef}>
          <figure className={`stage${isRecentImage(current) ? ' stage-is-new' : ''}`}>
            {current ? (
              <img
                src={current.name.toLowerCase().endsWith('.heic')
                  ? `/api/convert/${encodeURIComponent(current.name)}`
                  : current.url}
                alt={current.title || current.name}
                loading="eager"
                onError={() => {
                  setStatus({
                    text: `${current.name} could not be rendered. Browser may not support this format.`,
                    css: 'warn'
                  });
                }}
              />
            ) : (
              <img alt="No image" loading="eager" />
            )}
            <button className="nav-btn nav-prev" type="button" aria-label="Previous image" onClick={previousImage}>
              &#10094;
            </button>
            <button className="nav-btn nav-next" type="button" aria-label="Next image" onClick={nextImage}>
              &#10095;
            </button>
            {current && (
              <div className="stage-top-controls">
                {isRecentImage(current) ? <span className="stage-new-badge">NEW</span> : null}
                <button
                  className="stage-top-btn"
                  type="button"
                  aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
                  onClick={toggleFullscreen}
                  title={isFullscreen ? 'Exit fullscreen (F or ESC)' : 'Enter fullscreen (F)'}
                >
                  <span className="stage-top-btn-icon" aria-hidden="true">{isFullscreen ? '⤢' : '⤡'}</span>
                </button>
                <button
                  type="button"
                  className={`stage-top-btn stage-favorite-btn ${current.favorite ? 'is-open' : ''}`}
                  onClick={() => toggleFavorite().catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                  aria-label={current.favorite ? 'Unfavorite image' : 'Favorite image'}
                  title={current.favorite ? 'Unfavorite image' : 'Favorite image'}
                >
                  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor">
                    <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
                  </svg>
                </button>
                <button
                  type="button"
                  className={`stage-top-btn stage-pin-btn ${Number.isFinite(current.pinnedRank) ? 'is-open' : ''}`}
                  onClick={() => (Number.isFinite(current.pinnedRank) ? unpinCurrent() : pinCurrent()).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                  aria-label={Number.isFinite(current.pinnedRank) ? 'Unpin image' : 'Pin image'}
                  title={Number.isFinite(current.pinnedRank) ? 'Unpin image' : 'Pin image'}
                >
                  <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="currentColor">
                    <path d="M16 9V4l1-1V2H7v1l1 1v5l-2 2v1h5v7h2v-7h5v-1z"/>
                  </svg>
                </button>
                <span className="stage-rating-row" aria-label={`Rating: ${Number.isFinite(current?.rating) ? current.rating : 'none'}`}>
                  {[1, 2, 3, 4, 5].map((star) => (
                    <button
                      key={star}
                      type="button"
                      className={`stage-rating-btn ${Number(current?.rating) >= star ? 'is-on' : ''}`}
                      onClick={() => setCurrentRating(star).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                      aria-label={`Rate ${star} star${star > 1 ? 's' : ''}`}
                      title={`Rate ${star} star${star > 1 ? 's' : ''}`}
                    >★</button>
                  ))}
                </span>
              </div>
            )}
            <ImageCaption image={current} theme={captionTheme} />
          </figure>
        </section>

        {current && (
          <div className="image-actions">
            <div className="action-group">
              <button type="button" className="icon-btn" onClick={togglePlay} title={isPlaying ? 'Pause' : 'Play'}>
                {isPlaying ? (
                  <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
                    <rect x="5" y="4" width="4" height="16" rx="1"/>
                    <rect x="15" y="4" width="4" height="16" rx="1"/>
                  </svg>
                ) : (
                  <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
                    <polygon points="6,3 20,12 6,21"/>
                  </svg>
                )}
              </button>
              <select
                id="intervalSeconds"
                className="interval-select"
                value={intervalSeconds}
                onChange={(event) => setIntervalSeconds(Number(event.target.value))}
                title="Slide interval"
                aria-label="Slide interval"
              >
                {[3, 5, 7, 10, 15, 20, 30].map(s => (
                  <option key={s} value={s}>{s}s</option>
                ))}
              </select>
            </div>

            <div className="action-group">
              <button
                type="button"
                className={`action-btn exif-icon-btn ${showExif ? 'is-open' : ''}`}
                onClick={() => { setShowExif((v) => !v); setEditMode(false); setShowUpload(false); setShowQueue(false); }}
                aria-label={showExif ? 'Hide EXIF details' : 'Show EXIF details'}
                title={showExif ? 'Hide EXIF details' : 'Show EXIF details'}
              >
                <svg viewBox="0 0 64 64" width="24" height="24" aria-hidden="true">
                  <path d="M8 52l8-1-2-8z" fill="currentColor" opacity="0.95"/>
                  <circle cx="34" cy="30" r="25" fill="currentColor"/>
                  <circle cx="34" cy="30" r="20" fill="none" stroke="rgba(255,255,255,0.95)" strokeWidth="2.5"/>
                  <circle cx="34" cy="22" r="3" fill="rgba(255,255,255,0.95)"/>
                  <rect x="31" y="28" width="6" height="16" rx="1" fill="rgba(255,255,255,0.95)"/>
                </svg>
              </button>
              <button
                type="button"
                className={`action-btn queue-icon-btn ${showQueue ? 'is-open' : ''}`}
                onClick={() => setShowQueue(v => { if (!v) { setShowExif(false); setEditMode(false); setShowUpload(false); } return !v; })}
                aria-label={showQueue ? 'Hide queue manager' : 'Show queue manager'}
                title={showQueue ? 'Hide queue manager' : 'Show queue manager'}
              >
                <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor">
                  <path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h10v2H4v-2z"/>
                </svg>
              </button>
            </div>

            <div className="action-group">
              <button
                type="button"
                className={`action-btn upload-icon-btn ${showUpload ? 'is-open' : ''}`}
                onClick={toggleUpload}
                aria-label={showUpload ? 'Hide upload panel' : 'Upload images'}
                title={showUpload ? 'Hide upload panel' : 'Upload images'}
              >
                <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor">
                  <path d="M5 15v4h14v-4h2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4h2zm7-13l7 7h-4v6H9V9H5l7-7z"/>
                </svg>
              </button>
              <a
                href={`/api/download/${encodeURIComponent(current.name)}`}
                download={current.title ? `${current.title}${current.name.slice(current.name.lastIndexOf('.'))}` : current.name}
                className="action-btn download-btn"
                title="Download with metadata"
                aria-label="Download with metadata"
              >
                <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor">
                  <path d="M19 9h-4V3H9v6H5l7 7 7-7zm-14 9v2h14v-2H5z"/>
                </svg>
              </a>
            </div>

            <div className="action-group">
              <button
                type="button"
                className={`action-btn edit-icon-btn ${editMode ? 'is-open' : ''}`}
                onClick={toggleEdit}
                aria-label={editMode ? 'Close edit details' : 'Open edit details'}
                title={editMode ? 'Close edit details' : 'Open edit details'}
              >
                <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="currentColor">
                  <path d="M3 17.25V21h3.75L18.81 8.94l-3.75-3.75L3 17.25zm2.92 2.33H5v-.92l8.06-8.06.92.92L5.92 19.58zM20.71 7.04a1.003 1.003 0 0 0 0-1.42L18.37 3.29a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.83z"/>
                </svg>
              </button>
              {current?.trashedAt && (
                <button
                  type="button"
                  className="action-btn restore-icon-btn"
                  onClick={() => restoreImage().catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                  aria-label="Restore image"
                  title="Restore image"
                >
                  <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor">
                    <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.81l1.46 1.46A7.941 7.941 0 0 0 20 13c0-4.42-3.58-8-8-8zm-6.76.73L3.78 7.19A7.931 7.931 0 0 0 4 13c0 4.42 3.58 8 8 8v4l5-5-5-5v4c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.81z"/>
                  </svg>
                </button>
              )}
              <button
                type="button"
                className={`action-btn delete-icon-btn${confirmDelete ? ' is-open' : ''}`}
                onClick={() => setConfirmDelete((v) => !v)}
                aria-label="Delete image"
                title="Delete image"
              >
                <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor">
                  <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"/>
                </svg>
              </button>
            </div>
          </div>
        )}

        {confirmDelete && current && (
          <div className="confirm-strip">
            <span className="confirm-strip-text">{current.trashedAt ? 'Permanently delete' : 'Move to trash'} <strong>{current.title || current.name}</strong>?{current.trashedAt ? ' This cannot be undone.' : ''}</span>
            <button
              type="button"
              className="confirm-strip-yes"
              onClick={deleteImage}
              disabled={deleting}
            >{deleting ? 'Deleting…' : 'Yes, delete'}</button>
            <button
              type="button"
              className="confirm-strip-cancel"
              onClick={() => setConfirmDelete(false)}
              disabled={deleting}
            >Cancel</button>
          </div>
        )}

        {showPasswordModal && (
          <div className="modal-backdrop" onClick={closePasswordModal}>
            <section className="panel password-modal" onClick={(e) => e.stopPropagation()}>
              <h3>Change Password</h3>
              <form onSubmit={(event) => submitPasswordChange(event).catch((err) => setPasswordError(err.message))}>
                <div className="upload-field">
                  <label htmlFor="currentPassword">Current password</label>
                  <input
                    id="currentPassword"
                    type="password"
                    autoComplete="current-password"
                    value={currentPassword}
                    onChange={(event) => setCurrentPassword(event.target.value)}
                    disabled={passwordSaving}
                    required
                  />
                </div>
                <div className="upload-field">
                  <label htmlFor="newPassword">New password</label>
                  <input
                    id="newPassword"
                    type="password"
                    autoComplete="new-password"
                    value={newPassword}
                    onChange={(event) => setNewPassword(event.target.value)}
                    disabled={passwordSaving}
                    minLength={8}
                    required
                  />
                </div>
                <div className="upload-field">
                  <label htmlFor="confirmPassword">Confirm new password</label>
                  <input
                    id="confirmPassword"
                    type="password"
                    autoComplete="new-password"
                    value={confirmPassword}
                    onChange={(event) => setConfirmPassword(event.target.value)}
                    disabled={passwordSaving}
                    minLength={8}
                    required
                  />
                </div>
                {passwordError ? <p className="warn password-error">{passwordError}</p> : null}
                <div className="password-modal-actions">
                  <button type="button" className="manager-btn" onClick={closePasswordModal} disabled={passwordSaving}>Cancel</button>
                  <button type="submit" disabled={passwordSaving}>{passwordSaving ? 'Saving...' : 'Update password'}</button>
                </div>
              </form>
            </section>
          </div>
        )}

        {editMode && current && (
          <section className="panel edit-drawer">
            <h3>Edit: {current.title || current.name}</h3>
            <form onSubmit={(event) => {
              saveEdit(event).catch((err) => setEditResult(err.message));
            }}>
              <div className="upload-field">
                <label htmlFor="editTitle">Title</label>
                <input
                  id="editTitle"
                  type="text"
                  value={editTitle}
                  onChange={(event) => setEditTitle(event.target.value)}
                  maxLength="200"
                />
              </div>
              <div className="upload-field">
                <label htmlFor="editDescription">Description</label>
                <textarea
                  id="editDescription"
                  value={editDescription}
                  onChange={(event) => setEditDescription(event.target.value)}
                  maxLength="2000"
                ></textarea>
              </div>
              <div className="upload-field">
                <label htmlFor="editTags">Tags (comma separated)</label>
                <input
                  id="editTags"
                  type="text"
                  value={editTags}
                  onChange={(event) => setEditTags(event.target.value)}
                  placeholder="family, ceremony, favorites"
                  maxLength="300"
                />
              </div>
              <button type="submit" disabled={editSaving}>{editSaving ? 'Saving...' : 'Save'}</button>
              {editResult ? <span className={editResult === 'Saved.' ? 'ok' : 'warn'} style={{ marginLeft: '0.75rem' }}>{editResult}</span> : null}
            </form>
          </section>
        )}

        {showExif && current && (
          <section className="panel exif-panel">
            <h3>EXIF: {current.title || current.name}</h3>
            <ExifDetails exif={current.exif} />
          </section>
        )}

        {showUpload && (
        <section className="panel-grid">
          <article className="panel">
            <h2>Upload Images</h2>
            <p>Select JPEG, GIF, or HEIC files. Title and description apply to all files in this batch.</p>
            <form onSubmit={(event) => {
              onUploadSubmit(event).catch((error) => {
                setUploadResult(error.message);
                setStatus({ text: error.message, css: 'warn' });
              });
            }}>
              <div className="upload-field">
                <label htmlFor="uploadTitle">Title</label>
                <input
                  id="uploadTitle"
                  type="text"
                  value={uploadTitle}
                  onChange={(event) => setUploadTitle(event.target.value)}
                  placeholder="Optional title for this upload"
                  maxLength="200"
                />
              </div>

              <div className="upload-field">
                <label htmlFor="uploadDescription">Description</label>
                <textarea
                  id="uploadDescription"
                  value={uploadDescription}
                  onChange={(event) => setUploadDescription(event.target.value)}
                  placeholder="Optional description"
                  maxLength="2000"
                ></textarea>
              </div>

              <input
                ref={fileInputRef}
                name="files"
                type="file"
                accept=".jpg,.jpeg,.gif,.heic,image/jpeg,image/gif,image/heic"
                multiple
              />
              <button type="submit">Upload</button>
            </form>
            <div className="small-note">{uploadResult}</div>
          </article>

        </section>
        )}

        {showQueue && (
          <section className="panel queue-manager">
            <div className="queue-head">
              <h2>Queue Manager</h2>
              <span className="small-note">Selected: {selectedNames.length}</span>
            </div>

            <div className="manager-row queue-manager-controls">
              <label className="manager-toggle">
                <input type="checkbox" checked={showTrash} onChange={(e) => setShowTrash(e.target.checked)} />
                Show trash
              </label>
              <label className="manager-toggle">
                <input type="checkbox" checked={onlyTrashed} onChange={(e) => setOnlyTrashed(e.target.checked)} />
                Trash only
              </label>
              <label className="manager-toggle">
                <input type="checkbox" checked={onlyFavorites} onChange={(e) => setOnlyFavorites(e.target.checked)} />
                Favorites only
              </label>
              <select className="manager-sort" value={minRatingFilter} onChange={(e) => setMinRatingFilter(Number(e.target.value))}>
                <option value={0}>Any rating</option>
                <option value={1}>1 star+</option>
                <option value={2}>2 stars+</option>
                <option value={3}>3 stars+</option>
                <option value={4}>4 stars+</option>
                <option value={5}>5 stars</option>
              </select>
              <input
                className="manager-tag-filter"
                type="text"
                value={tagFilter}
                onChange={(e) => setTagFilter(e.target.value)}
                placeholder="Filter by tag"
              />
              <select className="manager-sort" value={sortMode} onChange={(e) => setSortMode(e.target.value)}>
                <option value="recent">Newest first</option>
                <option value="oldest">Oldest first</option>
                <option value="name">Name</option>
              </select>
              <select className="manager-sort" value={captionTheme} onChange={(e) => setCaptionTheme(e.target.value)}>
                <option value="classic">Caption theme: Classic</option>
                <option value="contrast">Caption theme: Contrast</option>
                <option value="warm">Caption theme: Warm</option>
              </select>
              <button
                type="button"
                className="manager-btn"
                onClick={() => setSelectedNames(images.map((img) => img.name))}
                disabled={!images.length}
              >Select all</button>
              <button
                type="button"
                className="manager-btn"
                onClick={() => setSelectedNames([])}
                disabled={!selectedNames.length}
              >Clear selection</button>
              <button
                type="button"
                className="manager-btn"
                onClick={() => loadDuplicateGroups().catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                disabled={dupesLoading || bulkWorking}
              >{dupesLoading ? 'Scanning...' : 'Find duplicates'}</button>
            </div>

            <div className="manager-row queue-schedule-row">
              <label className="manager-toggle">
                <input type="checkbox" checked={scheduleEnabled} onChange={(e) => setScheduleEnabled(e.target.checked)} />
                Schedule mode
              </label>
              <label className="manager-toggle" htmlFor="scheduleStart">Start</label>
              <input id="scheduleStart" className="manager-time" type="time" value={scheduleStart} onChange={(e) => setScheduleStart(e.target.value)} />
              <label className="manager-toggle" htmlFor="scheduleEnd">End</label>
              <input id="scheduleEnd" className="manager-time" type="time" value={scheduleEnd} onChange={(e) => setScheduleEnd(e.target.value)} />
            </div>

            {activeFilterChips.length > 0 && (
              <div className="manager-active-filters">
                {activeFilterChips.map((chip) => (
                  <span key={chip} className="manager-chip">{chip}</span>
                ))}
                <button
                  type="button"
                  className="manager-btn manager-clear-btn"
                  onClick={() => {
                    setShowTrash(false);
                    setOnlyTrashed(false);
                    setOnlyFavorites(false);
                    setMinRatingFilter(0);
                    setTagFilter('');
                    setSortMode('recent');
                  }}
                >Clear filters</button>
              </div>
            )}

            <div className="queue-bulk-actions">
              <button type="button" onClick={() => runBulkAction('trash').catch((err) => setStatus({ text: err.message, css: 'warn' }))} disabled={bulkWorking || !selectedNames.length}>Trash</button>
              <button type="button" onClick={() => runBulkAction('restore').catch((err) => setStatus({ text: err.message, css: 'warn' }))} disabled={bulkWorking || !selectedNames.length}>Restore</button>
              <button
                type="button"
                onClick={() => {
                  if (!confirmDangerous(`Permanently delete ${selectedNames.length} selected image(s)? This cannot be undone.`)) return;
                  runBulkAction('hardDelete').catch((err) => setStatus({ text: err.message, css: 'warn' }));
                }}
                disabled={bulkWorking || !selectedNames.length}
              >Hard Delete</button>
              <button type="button" onClick={() => runBulkAction('favoriteOn').catch((err) => setStatus({ text: err.message, css: 'warn' }))} disabled={bulkWorking || !selectedNames.length}>Favorite +</button>
              <button type="button" onClick={() => runBulkAction('favoriteOff').catch((err) => setStatus({ text: err.message, css: 'warn' }))} disabled={bulkWorking || !selectedNames.length}>Favorite -</button>
            </div>

            <details className="batch-editor">
              <summary className="batch-editor-summary">Batch metadata editor <span className="batch-editor-hint">({selectedNames.length} selected)</span></summary>
              <div className="batch-editor-body">
                <div className="batch-editor-fields">
                  <div className="upload-field batch-editor-field">
                    <label htmlFor="batchTitle">Title <span className="batch-editor-hint">(overwrites existing)</span></label>
                    <input id="batchTitle" type="text" value={batchTitle} onChange={(e) => setBatchTitle(e.target.value)} maxLength="200" placeholder="Shared title for all selected" />
                  </div>
                  <div className="upload-field batch-editor-field">
                    <label htmlFor="batchDescription">Description <span className="batch-editor-hint">(overwrites existing)</span></label>
                    <textarea id="batchDescription" value={batchDescription} onChange={(e) => setBatchDescription(e.target.value)} maxLength="2000" placeholder="Shared description for all selected" />
                  </div>
                  <div className="upload-field batch-editor-field">
                    <label htmlFor="batchTags">Tags <span className="batch-editor-hint">(replaces existing, comma separated)</span></label>
                    <input id="batchTags" type="text" value={batchTags} onChange={(e) => setBatchTags(e.target.value)} maxLength="300" placeholder="family, ceremony, kwanzaa" />
                  </div>
                  <div className="upload-field batch-editor-field">
                    <label htmlFor="batchRating">Rating</label>
                    <select id="batchRating" className="manager-sort" value={batchRating} onChange={(e) => setBatchRating(e.target.value)}>
                      <option value="">— no change —</option>
                      <option value="1">★ 1</option>
                      <option value="2">★★ 2</option>
                      <option value="3">★★★ 3</option>
                      <option value="4">★★★★ 4</option>
                      <option value="5">★★★★★ 5</option>
                    </select>
                  </div>
                </div>
                <button
                  type="button"
                  className="batch-editor-apply"
                  disabled={bulkWorking || !selectedNames.length}
                  onClick={() => applyBulkMetadataPatch().catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                >{bulkWorking ? 'Applying…' : `Apply to ${selectedNames.length} selected`}</button>
              </div>
            </details>

            {images.length ? (
              <ul className="queue-list">
                {images.map((image) => (
                  <li key={image.name} className="queue-item">
                    <label className="queue-select">
                      <input
                        type="checkbox"
                        checked={selectedNames.includes(image.name)}
                        onChange={() => toggleSelectedName(image.name)}
                      />
                    </label>
                    <div className="queue-main">
                      <div className="queue-title">
                        {image.title ? <strong>{image.title}</strong> : <span>{image.name}</span>}
                        {image.trashedAt ? <span className="queue-badge trash">Trashed</span> : null}
                        {image.favorite ? <span className="queue-badge favorite">Favorite</span> : null}
                        {Number.isFinite(image.pinnedRank) ? <span className="queue-badge pin">Pinned #{image.pinnedRank}</span> : null}
                        {Number.isFinite(image.rating) ? <span className="queue-badge rating">{'★'.repeat(image.rating)}</span> : null}
                      </div>
                      <div className="queue-meta">{image.name}</div>
                    </div>
                    <div className="queue-row-actions">
                      <button
                        type="button"
                        className={`queue-mini-btn queue-mini-icon-btn queue-mini-favorite-btn ${image.favorite ? 'is-open' : ''}`}
                        onClick={() => toggleFavoriteByName(image.name, Boolean(image.favorite)).then(loadImages).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                        title={image.favorite ? 'Unfavorite' : 'Favorite'}
                        aria-label={image.favorite ? 'Unfavorite' : 'Favorite'}
                      >
                        <span className="queue-icon" aria-hidden="true">{image.favorite ? '★' : '☆'}</span>
                      </button>
                      {(() => {
                        const isPinned = Number.isFinite(image.pinnedRank);
                        return (
                          <button
                            type="button"
                            className="queue-mini-btn queue-mini-icon-btn"
                            onClick={() => togglePinByName(image.name, isPinned).then(loadImages).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                            title={isPinned ? 'Unpin' : 'Pin'}
                            aria-label={isPinned ? 'Unpin' : 'Pin'}
                          >
                            <span className="queue-icon" aria-hidden="true">
                              {isPinned ? '📌−' : '📌'}
                            </span>
                          </button>
                        );
                      })()}
                      <span className="queue-row-stars" aria-label={`Rating: ${image.rating || 'none'}`}>
                        {[1, 2, 3, 4, 5].map((star) => (
                          <button
                            key={star}
                            type="button"
                            className={`queue-star-btn ${Number(image.rating) >= star ? 'is-on' : ''}`}
                            onClick={() => setRatingByName(image.name, image.rating, star).then(loadImages).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                            aria-label={`Rate ${star}`}
                            title={`Rate ${star}`}
                          >★</button>
                        ))}
                      </span>
                      {Number.isFinite(image.pinnedRank) ? (
                        <>
                          <button
                            type="button"
                            className="queue-mini-btn queue-mini-icon-btn"
                            onClick={() => movePinnedByName(image.name, 'top').then(() => setStatus({ text: 'Pinned image moved to top.', css: 'ok' })).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                            title="Move to top"
                            aria-label="Move to top"
                          ><span className="queue-icon" aria-hidden="true">⤒</span></button>
                          <button
                            type="button"
                            className="queue-mini-btn queue-mini-icon-btn"
                            onClick={() => movePinnedByName(image.name, -1).then(() => setStatus({ text: 'Pinned image moved up.', css: 'ok' })).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                            title="Move up"
                            aria-label="Move up"
                          ><span className="queue-icon" aria-hidden="true">↑</span></button>
                          <button
                            type="button"
                            className="queue-mini-btn queue-mini-icon-btn"
                            onClick={() => movePinnedByName(image.name, 1).then(() => setStatus({ text: 'Pinned image moved down.', css: 'ok' })).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                            title="Move down"
                            aria-label="Move down"
                          ><span className="queue-icon" aria-hidden="true">↓</span></button>
                        </>
                      ) : null}
                    </div>
                  </li>
                ))}
              </ul>
            ) : (
              <p className="small-note">No images loaded for current filters.</p>
            )}
          </section>
        )}

        {duplicateGroups.length > 0 && (
          <section className="panel dupes-panel">
            <div className="queue-head">
              <h2>Duplicate Images</h2>
              <span className="small-note">{duplicateGroups.length} group(s) &nbsp;<button type="button" className="manager-btn" onClick={() => setDuplicateGroups([])}>Dismiss</button></span>
            </div>
            {duplicateGroups.map((group) => (
              <div key={group.key} className="dupe-group">
                <div className="dupe-group-head">
                  <span className="dupe-group-label">{group.count} identical files · {(group.size / 1024).toFixed(0)} KB each</span>
                  <button
                    type="button"
                    className="manager-btn"
                    disabled={bulkWorking}
                    onClick={() => {
                      const toTrash = group.items.slice(1).map((i) => i.name);
                      if (!confirmDangerous(`Move ${toTrash.length} duplicate(s) to trash, keeping the newest?`)) return;
                      runBulkAction('trash', {}, toTrash)
                        .then(() => setDuplicateGroups((prev) => prev.filter((g) => g.key !== group.key)))
                        .catch((err) => setStatus({ text: err.message, css: 'warn' }));
                    }}
                  >Trash all but newest</button>
                </div>
                <ul className="dupe-list">
                  {group.items.map((item, idx) => (
                    <li key={item.name} className={`dupe-item${idx === 0 ? ' dupe-item-keep' : ''}`}>
                      <span className="dupe-item-badge">{idx === 0 ? 'keep' : 'dupe'}</span>
                      <span className="dupe-item-name">{item.title || item.name}</span>
                      <span className="dupe-item-meta">{item.name}</span>
                      <button
                        type="button"
                        className="queue-mini-btn danger"
                        disabled={bulkWorking || idx === 0}
                        onClick={() => {
                          if (!confirmDangerous(`Move "${item.title || item.name}" to trash?`)) return;
                          runBulkAction('trash', {}, [item.name]).then(() => {
                            setDuplicateGroups((prev) => prev
                              .map((g) => g.key !== group.key ? g : { ...g, items: g.items.filter((i) => i.name !== item.name), count: g.count - 1 })
                              .filter((g) => g.count > 1));
                          }).catch((err) => setStatus({ text: err.message, css: 'warn' }));
                        }}
                      >Trash</button>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </section>
        )}

        {trashedImages.length > 0 && (
          <section className="panel trash-panel">
            <div className="queue-head">
              <h2>Trash</h2>
              <span className="small-note">{trashedImages.length} item(s)</span>
            </div>
            <div className="trash-actions">
              <button
                type="button"
                className="queue-mini-btn"
                onClick={() => setSelectedNames((prev) => Array.from(new Set([...prev, ...trashedImages.map((img) => img.name)])))}
              >Select trashed</button>
              <button
                type="button"
                className="queue-mini-btn danger"
                disabled={bulkWorking || !selectedTrashedNames.length}
                onClick={() => {
                  if (!confirmDangerous(`Permanently delete ${selectedTrashedNames.length} selected trashed image(s)? This cannot be undone.`)) return;
                  runBulkAction('hardDelete', {}, selectedTrashedNames).catch((err) => setStatus({ text: err.message, css: 'warn' }));
                }}
              >Empty selected trash</button>
            </div>
            <ul className="queue-list">
              {trashedImages.map((image) => (
                <li key={`trash-${image.name}`} className="queue-item">
                  <div className="queue-main">
                    <div className="queue-title">
                      {image.title ? <strong>{image.title}</strong> : <span>{image.name}</span>}
                      <span className="queue-badge trash">Trashed</span>
                    </div>
                    <div className="queue-meta">{image.name}</div>
                  </div>
                  <div className="queue-row-actions">
                    <button
                      type="button"
                      className="queue-mini-btn"
                      onClick={() => restoreByName(image.name).then(loadImages).then(() => setStatus({ text: 'Image restored from trash.', css: 'ok' })).catch((err) => setStatus({ text: err.message, css: 'warn' }))}
                    >Restore</button>
                    <button
                      type="button"
                      className="queue-mini-btn danger"
                      onClick={() => {
                        if (!confirmDangerous(`Permanently delete ${image.title || image.name}? This cannot be undone.`)) return;
                        hardDeleteByName(image.name).then(loadImages).then(() => setStatus({ text: 'Image permanently deleted.', css: 'ok' })).catch((err) => setStatus({ text: err.message, css: 'warn' }));
                      }}
                    >Delete forever</button>
                  </div>
                </li>
              ))}
            </ul>
          </section>
        )}
      </main>
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
