// galerie.jsx — Fotogalerie (#galerie): oeffentliches Thumbnail-Raster mit
// Lightbox, Admin-Upload mit Browser-Kompression, optionale Captions.
// Daten: Tabelle gallery_photos + Storage-Bucket gallery
// (Pfade: thumb/<id>.jpg, full/<id>.jpg). Kein Realtime: Laden beim
// Seitenaufruf reicht, die Galerie ist kein Live-Datum.
// Reihenfolge: Spalte position (kleiner = weiter vorne), Admin sortiert
// per Drag&Drop (SortableJS). Oeffentlich paginiert (PAGE_SIZE/Seite), der
// Admin sieht die komplette Liste am Stueck (nur so ist Drag ueber alle
// Fotos moeglich).

const GALLERY_BUCKET = 'gallery';
const GALLERY_PAGE_SIZE = 60; // ab mehr Fotos erscheint die Paginierung

function galleryPublicUrl(path) {
  return sb.storage.from(GALLERY_BUCKET).getPublicUrl(path).data.publicUrl;
}

// Sichtbare Seiten-Buttons: bis 7 Seiten alle, sonst 1 / aktuell±1 / letzte
// mit '…'-Luecken dazwischen.
function galleryPagerTokens(cur, total) {
  if (total <= 7) return Array.from({ length: total }, (_, i) => i);
  const wanted = [0, cur - 1, cur, cur + 1, total - 1].filter((n) => n >= 0 && n < total);
  const pages = [...new Set(wanted)].sort((a, b) => a - b);
  const tokens = [];
  let prev = null;
  for (const p of pages) {
    if (prev !== null && p - prev > 1) tokens.push('gap');
    tokens.push(p);
    prev = p;
  }
  return tokens;
}

// Sentinel-Muster wie loadRegistrations: null bei Fehler, Aufrufer laesst
// den State dann stehen.
async function loadGalleryPhotos() {
  const { data, error } = await sb
    .from('gallery_photos')
    .select('id, created_at, caption, thumb_path, full_path, position')
    .order('position', { ascending: true })
    .order('created_at', { ascending: false })
    .order('id', { ascending: true });
  if (error) { console.error('Galerie laden fehlgeschlagen', error); return null; }
  return data;
}

// Verkleinert ein Foto im Browser: laengste Kante auf maxEdge px,
// JPEG mit gegebener Qualitaet. imageOrientation 'from-image' wendet
// die EXIF-Rotation an (Hochkant-Fotos vom Handy/Kamera).
async function shrinkImage(file, maxEdge, quality) {
  const bmp = await createImageBitmap(file, { imageOrientation: 'from-image' });
  const scale = Math.min(1, maxEdge / Math.max(bmp.width, bmp.height));
  const w = Math.max(1, Math.round(bmp.width * scale));
  const h = Math.max(1, Math.round(bmp.height * scale));
  const canvas = document.createElement('canvas');
  canvas.width = w; canvas.height = h;
  canvas.getContext('2d').drawImage(bmp, 0, 0, w, h);
  bmp.close();
  return await new Promise((resolve, reject) =>
    canvas.toBlob(
      (b) => (b ? resolve(b) : reject(new Error('Bild konnte nicht umgewandelt werden'))),
      'image/jpeg', quality));
}

// Grossansicht: dunkles Overlay, Blaettern per Pfeil-Buttons, Pfeiltasten
// und Touch-Swipe. Traegt .modal-overlay mit, damit der globale
// Scroll-Lock (html:has(.modal-overlay)) greift; Overlay-Klick schliesst
// bewusst NICHT (Modal-Konvention).
function GalleryLightbox({ photos, idx, onClose, onPrev, onNext }) {
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      else if (e.key === 'ArrowLeft') onPrev();
      else if (e.key === 'ArrowRight') onNext();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose, onPrev, onNext]);

  const touchX = React.useRef(null);
  const p = photos[idx];
  if (!p) return null;

  return (
    <div
      className="modal-overlay lightbox-overlay"
      onTouchStart={(e) => { touchX.current = e.touches[0].clientX; }}
      onTouchEnd={(e) => {
        if (touchX.current === null) return;
        const dx = e.changedTouches[0].clientX - touchX.current;
        touchX.current = null;
        if (dx > 48) onPrev();
        else if (dx < -48) onNext();
      }}
    >
      <button className="modal-close lightbox-close" onClick={onClose} aria-label="Schließen">×</button>
      {photos.length > 1 && (
        <button className="lightbox-arrow lightbox-prev" onClick={onPrev} aria-label="Vorheriges Foto">‹</button>
      )}
      <figure className="lightbox-figure">
        <img className="lightbox-img" src={galleryPublicUrl(p.full_path)} alt={p.caption || 'Turnierfoto'} />
        {p.caption ? <figcaption className="lightbox-caption">{p.caption}</figcaption> : null}
        <div className="lightbox-count">{idx + 1} / {photos.length}</div>
      </figure>
      {photos.length > 1 && (
        <button className="lightbox-arrow lightbox-next" onClick={onNext} aria-label="Nächstes Foto">›</button>
      )}
    </div>
  );
}

// Bildunterschrift bearbeiten (Admin). Leer speichern ist erlaubt und
// entfernt die Caption wieder.
function CaptionModal({ photo, onSave, onClose }) {
  const [text, setText] = React.useState(photo.caption || '');
  return (
    <div className="modal-overlay">
      <div className="modal" style={{ maxWidth: 460 }}>
        <button className="modal-close" onClick={onClose} aria-label="Schließen">×</button>
        <h3>Bildunterschrift</h3>
        <div className="field">
          <input
            className="input"
            type="text"
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="Optional"
            autoFocus
            onKeyDown={(e) => { if (e.key === 'Enter') onSave(text.trim()); }}
          />
        </div>
        <div style={{ display: 'flex', gap: 10, marginTop: 16 }}>
          <button className="btn" onClick={() => onSave(text.trim())}>Speichern</button>
        </div>
      </div>
    </div>
  );
}

function GaleriePage({ isAdmin }) {
  const [photos, setPhotos] = React.useState(null); // null = laedt noch
  const [loadFailed, setLoadFailed] = React.useState(false);
  const [lightboxIdx, setLightboxIdx] = React.useState(null);
  const [captionPhoto, setCaptionPhoto] = React.useState(null);
  const [uploadState, setUploadState] = React.useState(null); // {done, total, errors:[]}
  const [page, setPage] = React.useState(0);
  const [gridKey, setGridKey] = React.useState(0); // Remount-Schluessel nach Drag
  const fileInputRef = React.useRef(null);
  const gridRef = React.useRef(null);

  const reload = React.useCallback(async () => {
    const rows = await loadGalleryPhotos();
    if (rows === null) { setLoadFailed(true); return; }
    setLoadFailed(false);
    setPhotos(rows);
  }, []);

  React.useEffect(() => { reload(); }, [reload]);

  // Paginierung nur oeffentlich; der Admin bekommt die komplette Liste, damit
  // Drag&Drop ueber alle Fotos hinweg funktioniert.
  const paginated = !isAdmin && photos !== null && photos.length > GALLERY_PAGE_SIZE;
  const pageCount = paginated ? Math.ceil(photos.length / GALLERY_PAGE_SIZE) : 1;
  const safePage = Math.min(page, pageCount - 1);
  const start = paginated ? safePage * GALLERY_PAGE_SIZE : 0;
  const visible = photos === null ? [] : (paginated ? photos.slice(start, start + GALLERY_PAGE_SIZE) : photos);

  const goToPage = (n) => {
    setPage(Math.max(0, Math.min(n, pageCount - 1)));
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  async function handleFiles(fileList) {
    const files = Array.from(fileList).filter((f) => f.type.startsWith('image/'));
    if (!files.length) return;
    setUploadState({ done: 0, total: files.length, errors: [] });
    const errors = [];
    // Neue Uploads nach vorne, in Auswahl-Reihenfolge: kleinste position zuerst.
    const baseTs = Date.now();
    for (let i = 0; i < files.length; i++) {
      try {
        // Grossansicht max 2000 px in hoher Qualitaet, Thumb 400 px.
        const fullBlob = await shrinkImage(files[i], 2000, 0.85);
        const thumbBlob = await shrinkImage(files[i], 400, 0.8);
        const id = crypto.randomUUID();
        const fullPath = 'full/' + id + '.jpg';
        const thumbPath = 'thumb/' + id + '.jpg';
        const opts = { contentType: 'image/jpeg', cacheControl: '31536000' };
        const up1 = await sb.storage.from(GALLERY_BUCKET).upload(fullPath, fullBlob, opts);
        if (up1.error) throw up1.error;
        const up2 = await sb.storage.from(GALLERY_BUCKET).upload(thumbPath, thumbBlob, opts);
        if (up2.error) throw up2.error;
        const ins = await sb.from('gallery_photos')
          .insert({ id, caption: '', thumb_path: thumbPath, full_path: fullPath, position: -(baseTs - i) / 1000 });
        if (ins.error) {
          // Zeile fehlgeschlagen: Storage-Dateien wieder aufraeumen.
          await sb.storage.from(GALLERY_BUCKET).remove([fullPath, thumbPath]);
          throw ins.error;
        }
      } catch (e) {
        errors.push(files[i].name + ': ' + (e.message || 'Fehler'));
      }
      setUploadState({ done: i + 1, total: files.length, errors: [...errors] });
    }
    await reload();
  }

  async function saveCaption(photo, caption) {
    const { error } = await sb.from('gallery_photos').update({ caption }).eq('id', photo.id);
    if (error) { window.alert('Speichern fehlgeschlagen: ' + error.message); return; }
    setCaptionPhoto(null);
    await reload();
  }

  async function deletePhoto(photo) {
    if (!window.confirm('Dieses Foto wirklich löschen?')) return;
    const { error } = await sb.from('gallery_photos').delete().eq('id', photo.id);
    if (error) { window.alert('Löschen fehlgeschlagen: ' + error.message); return; }
    // Erst DB-Zeile, dann Dateien: schlaegt das Aufraeumen fehl, bleibt
    // hoechstens eine verwaiste Datei zurueck, nie ein kaputter Eintrag.
    await sb.storage.from(GALLERY_BUCKET).remove([photo.full_path, photo.thumb_path]);
    await reload();
  }

  // Notbremse falls zwischen zwei Nachbarn kein Float-Platz mehr ist:
  // komplette Liste neu durchnummerieren (sehr selten).
  async function renumberAll(ordered) {
    const next = ordered.map((p, i) => ({ ...p, position: i }));
    setPhotos(next);
    setGridKey((k) => k + 1);
    for (const p of next) {
      const { error } = await sb.from('gallery_photos').update({ position: p.position }).eq('id', p.id);
      if (error) { window.alert('Reihenfolge speichern fehlgeschlagen: ' + error.message); await reload(); return; }
    }
  }

  // Drag-Ende (nur Admin, komplette Liste -> DOM-Index == Array-Index).
  // Nur die verschobene Zeile bekommt eine neue position (Mittel der Nachbarn).
  async function reorder(oldIndex, newIndex) {
    if (oldIndex == null || newIndex == null || oldIndex === newIndex || !photos) return;
    const next = photos.slice();
    const [moved] = next.splice(oldIndex, 1);
    next.splice(newIndex, 0, moved);
    const before = next[newIndex - 1];
    const after = next[newIndex + 1];
    let pos;
    if (!before) pos = (after ? after.position : 0) - 1;
    else if (!after) pos = before.position + 1;
    else pos = (before.position + after.position) / 2;
    if ((before && pos <= before.position) || (after && pos >= after.position)) {
      return renumberAll(next); // Praezision erschoepft
    }
    const updated = { ...moved, position: pos };
    next[newIndex] = updated;
    setPhotos(next);
    setGridKey((k) => k + 1);
    const { error } = await sb.from('gallery_photos').update({ position: pos }).eq('id', updated.id);
    if (error) { window.alert('Reihenfolge speichern fehlgeschlagen: ' + error.message); await reload(); }
  }

  // Seite klemmen, falls nach Loeschen weniger Seiten uebrig sind.
  React.useEffect(() => {
    if (page > pageCount - 1) setPage(Math.max(0, pageCount - 1));
  }, [pageCount, page]);

  // SortableJS am Grid (nur Admin). Neu aufsetzen, sobald sich die Liste
  // aendert; Remount ueber gridKey haelt DOM und React-State im Gleichschritt.
  React.useEffect(() => {
    if (!isAdmin || !window.Sortable) return;
    const el = gridRef.current;
    if (!el) return;
    const sortable = window.Sortable.create(el, {
      handle: '.gallery-drag',
      animation: 150,
      ghostClass: 'gallery-item-ghost',
      // Reorder erst nach dem onEnd-Stack: SortableJS raeumt intern noch auf,
      // und der anschliessende Grid-Remount (gridKey) darf die Instanz nicht
      // mitten im Callback zerstoeren.
      onEnd: (evt) => {
        const { oldIndex, newIndex } = evt;
        Promise.resolve().then(() => reorder(oldIndex, newIndex));
      },
    });
    return () => sortable.destroy();
  }, [isAdmin, photos, gridKey]);

  return (
    <div className="page">
      <div className="page-eyebrow">Fotogalerie</div>
      <h1 className="page-title">Bilder der 1. Paartal Open</h1>

      {isAdmin && (
        <div className="gallery-admin-bar">
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            multiple
            style={{ display: 'none' }}
            onChange={(e) => { handleFiles(e.target.files); e.target.value = ''; }}
          />
          <button className="btn" onClick={() => fileInputRef.current && fileInputRef.current.click()}>
            Fotos hochladen
          </button>
          {photos !== null && photos.length > 1 && (
            <span className="gallery-admin-hint">Griff oben links zum Verschieben ziehen</span>
          )}
          {uploadState && uploadState.done < uploadState.total && (
            <span className="gallery-upload-progress">
              {uploadState.done} / {uploadState.total} hochgeladen …
            </span>
          )}
          {uploadState && uploadState.done === uploadState.total && uploadState.errors.length > 0 && (
            <span className="gallery-upload-errors">
              {uploadState.errors.length} Fehler: {uploadState.errors.join(' · ')}
            </span>
          )}
        </div>
      )}

      {photos === null && !loadFailed && <p className="page-sub">Galerie wird geladen …</p>}
      {loadFailed && <p className="page-sub">Galerie konnte nicht geladen werden. Bitte Seite neu laden.</p>}
      {photos !== null && photos.length === 0 && (
        <p className="page-sub">Noch keine Fotos vorhanden. Die Bilder vom Turnier erscheinen hier.</p>
      )}

      {photos !== null && photos.length > 0 && (
        <div className="gallery-grid" ref={gridRef} key={gridKey}>
          {visible.map((p, i) => {
            const globalIdx = start + i;
            return (
              <figure key={p.id} className="gallery-item" onClick={() => setLightboxIdx(globalIdx)}>
                <img
                  className="gallery-thumb"
                  src={galleryPublicUrl(p.thumb_path)}
                  alt={p.caption || 'Turnierfoto'}
                  loading="lazy"
                />
                {isAdmin && (
                  <React.Fragment>
                    <button
                      className="gallery-drag"
                      title="Zum Verschieben ziehen"
                      aria-label="Verschieben"
                      onClick={(e) => e.stopPropagation()}
                    >⠿</button>
                    <div className="gallery-item-admin" onClick={(e) => e.stopPropagation()}>
                      <button className="gallery-item-btn" title="Bildunterschrift" onClick={() => setCaptionPhoto(p)}>✎</button>
                      <button className="gallery-item-btn gallery-item-del" title="Löschen" onClick={() => deletePhoto(p)}>×</button>
                    </div>
                  </React.Fragment>
                )}
              </figure>
            );
          })}
        </div>
      )}

      {paginated && (
        <React.Fragment>
          <nav className="gallery-pager" aria-label="Galerie-Seiten">
            <button
              className="gallery-pager-arrow"
              onClick={() => goToPage(safePage - 1)}
              disabled={safePage === 0}
              aria-label="Vorherige Seite"
            >‹</button>
            {galleryPagerTokens(safePage, pageCount).map((t, i) => (
              t === 'gap'
                ? <span key={'gap' + i} className="gallery-pager-gap">…</span>
                : (
                  <button
                    key={t}
                    className={'gallery-pager-num' + (t === safePage ? ' is-active' : '')}
                    aria-current={t === safePage ? 'page' : undefined}
                    onClick={() => goToPage(t)}
                  >{t + 1}</button>
                )
            ))}
            <button
              className="gallery-pager-arrow"
              onClick={() => goToPage(safePage + 1)}
              disabled={safePage === pageCount - 1}
              aria-label="Nächste Seite"
            >›</button>
          </nav>
          <p className="gallery-pager-info">Seite {safePage + 1} von {pageCount}</p>
        </React.Fragment>
      )}

      {captionPhoto && (
        <CaptionModal
          photo={captionPhoto}
          onSave={(t) => saveCaption(captionPhoto, t)}
          onClose={() => setCaptionPhoto(null)}
        />
      )}

      {lightboxIdx !== null && photos && photos[lightboxIdx] && (
        <GalleryLightbox
          photos={photos}
          idx={lightboxIdx}
          onClose={() => {
            if (paginated) setPage(Math.floor(lightboxIdx / GALLERY_PAGE_SIZE));
            setLightboxIdx(null);
          }}
          onPrev={() => setLightboxIdx((lightboxIdx - 1 + photos.length) % photos.length)}
          onNext={() => setLightboxIdx((lightboxIdx + 1) % photos.length)}
        />
      )}
    </div>
  );
}

Object.assign(window, { GaleriePage });
