'use client';

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

import { MAP_TILES } from '../../constants';
import { groupByTime } from '../../lib/grouping';
import { Icon } from '../../icons';
import { useGallery, useGalleryStoreApi } from '../../store/context';
import {
  clusterByLocation,
  type LocationCluster,
  liveMedia,
  locatedMedia,
  searchMedia,
} from '../../store/selectors';
import type { MediaItem } from '../../types';
import { MediaGrid } from '../MediaGrid';
import { MosaicGrid } from './MosaicGrid';

/** Thumbnails shown in a multi-photo pin's hover strip before the "+N" chip. */
const TIP_STRIP_MAX = 5;
/** How long each frame of the hover mini-slider stays up. */
const TIP_FRAME_MS = 900;

/** Parse a `<input type="date">` value into a local-midnight epoch, or null. */
function parseDateInput(value: string, endOfDay: boolean): number | null {
  if (!value) return null;
  const [y, m, d] = value.split('-').map(Number);
  if (!y || !m || !d) return null;
  return endOfDay
    ? new Date(y, m - 1, d, 23, 59, 59, 999).getTime()
    : new Date(y, m - 1, d, 0, 0, 0, 0).getTime();
}

/** Format a Date to a `<input type="date">` value (local `YYYY-MM-DD`). */
function toInputDate(d: Date): string {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

/** Short human label for one `YYYY-MM-DD` value. */
function labelDate(value: string): string {
  const [y, m, d] = value.split('-').map(Number);
  if (!y || !m || !d) return '';
  return new Date(y, m - 1, d).toLocaleDateString(undefined, {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
  });
}

/** Button label for the current range ("All dates" when unset). */
function rangeLabel(from: string, to: string): string {
  if (!from && !to) return 'All dates';
  if (from && to) return `${labelDate(from)} – ${labelDate(to)}`;
  if (from) return `From ${labelDate(from)}`;
  return `Until ${labelDate(to)}`;
}

/**
 * A single, polished "date range" control: a button showing the current range
 * that opens a popover of quick presets + a custom From/To pair. It drives the
 * SAME from/to strings the MapView already filters by — so search/object chips,
 * the live count, and Clear all keep working unchanged.
 */
function DateRangeControl({
  from,
  to,
  onChange,
}: {
  from: string;
  to: string;
  onChange: (from: string, to: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const onDown = (e: PointerEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    };
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setOpen(false);
    };
    window.addEventListener('pointerdown', onDown, true);
    window.addEventListener('keydown', onKey);
    return () => {
      window.removeEventListener('pointerdown', onDown, true);
      window.removeEventListener('keydown', onKey);
    };
  }, [open]);

  const today = new Date();
  const daysAgo = (n: number) => {
    const d = new Date();
    d.setDate(today.getDate() - n);
    return d;
  };
  const presets: Array<{ label: string; from: string; to: string }> = [
    { label: 'All dates', from: '', to: '' },
    { label: 'Last 7 days', from: toInputDate(daysAgo(6)), to: toInputDate(today) },
    { label: 'Last 30 days', from: toInputDate(daysAgo(29)), to: toInputDate(today) },
    {
      label: 'This year',
      from: toInputDate(new Date(today.getFullYear(), 0, 1)),
      to: toInputDate(today),
    },
  ];

  const active = Boolean(from || to);

  return (
    <div className="apg-daterange" ref={ref}>
      <button
        type="button"
        className={['apg-daterange__button', active ? 'apg-daterange__button--active' : '']
          .filter(Boolean)
          .join(' ')}
        aria-haspopup="dialog"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        <Icon name="clock" size={14} />
        <span className="apg-daterange__label">{rangeLabel(from, to)}</span>
        <Icon name="chevron-down" size={14} />
      </button>
      {open ? (
        <div className="apg-daterange__pop" role="dialog" aria-label="Choose a date range">
          <div className="apg-daterange__presets">
            {presets.map((p) => {
              const on = p.from === from && p.to === to;
              return (
                <button
                  key={p.label}
                  type="button"
                  className={['apg-daterange__preset', on ? 'apg-daterange__preset--on' : '']
                    .filter(Boolean)
                    .join(' ')}
                  aria-pressed={on}
                  onClick={() => {
                    onChange(p.from, p.to);
                    setOpen(false);
                  }}
                >
                  {p.label}
                  {on ? <Icon name="check" size={14} /> : null}
                </button>
              );
            })}
          </div>
          <div className="apg-daterange__custom">
            <div className="apg-daterange__custom-label">Custom range</div>
            <div className="apg-daterange__fields">
              <label className="apg-daterange__field">
                <span>From</span>
                <input
                  type="date"
                  value={from}
                  max={to || undefined}
                  onChange={(e) => onChange(e.target.value, to)}
                />
              </label>
              <label className="apg-daterange__field">
                <span>To</span>
                <input
                  type="date"
                  value={to}
                  min={from || undefined}
                  onChange={(e) => onChange(from, e.target.value)}
                />
              </label>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}

export function MapView() {
  const api = useGalleryStoreApi();
  const mapMode = useGallery((s) => s.mapMode);
  const media = useGallery((s) => s.media);
  const mapFocus = useGallery((s) => s.mapFocus);
  const located = locatedMedia(media);

  // The location pin the user tapped → its photos shown date-grouped in a sheet.
  const [cluster, setCluster] = useState<LocationCluster | null>(null);
  // Sheet height as a fraction of the viewport (drag the handle up → toward full).
  const [sheetH, setSheetH] = useState(0.5);
  const dragRef = useRef<{ startY: number; startH: number } | null>(null);

  // ---- sheet filters (search + date range + object chips), AND-combined ----
  const [query, setQuery] = useState('');
  const [fromDate, setFromDate] = useState('');
  const [toDate, setToDate] = useState('');
  const [objectFilter, setObjectFilter] = useState<string | null>(null);

  const resetFilters = () => {
    setQuery('');
    setFromDate('');
    setToDate('');
    setObjectFilter(null);
  };

  const onHandleDown = (e: React.PointerEvent) => {
    dragRef.current = { startY: e.clientY, startH: sheetH };
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
  };
  const onHandleMove = (e: React.PointerEvent) => {
    if (!dragRef.current) return;
    const dy = dragRef.current.startY - e.clientY; // dragging up is positive
    const next = dragRef.current.startH + dy / window.innerHeight;
    setSheetH(Math.max(0.18, Math.min(0.98, next)));
  };
  const onHandleUp = () => {
    if (dragRef.current && sheetH <= 0.2) setCluster(null); // dragged down → dismiss
    dragRef.current = null;
  };

  const containerRef = useRef<HTMLDivElement>(null);
  // Leaflet instances kept in refs (typed loosely to avoid an SSR import).
  const mapRef = useRef<any>(null);
  const tileRef = useRef<any>(null);
  const markersRef = useRef<any>(null);
  const leafletRef = useRef<any>(null);
  // Every hover-slider interval currently running, so teardown can kill them all
  // (a leaked interval keeps mutating detached DOM forever).
  const tipTimersRef = useRef(new Set<ReturnType<typeof setInterval>>());
  const clearTipTimers = () => {
    for (const t of tipTimersRef.current) clearInterval(t);
    tipTimersRef.current.clear();
  };
  // addMarkers reads the latest `located` + setCluster via refs (Leaflet callbacks
  // are created once, so closing over state directly would go stale).
  const locatedRef = useRef(located);
  locatedRef.current = located;
  // Current sheet height in px, read by the fly-to offset (the sheet covers the
  // bottom of the map, so the pin must end up above it).
  const sheetHRef = useRef(sheetH);
  sheetHRef.current = sheetH;

  /** Zoom to the pin, keeping it clear of the sheet, then open the sheet. */
  const openClusterRef = useRef<(c: LocationCluster) => void>(() => {});
  openClusterRef.current = (c) => {
    const map = mapRef.current;
    if (map) {
      const current = map.getZoom?.() ?? 2;
      // Never zoom OUT: a single photo warrants street level, a cluster stays wide
      // enough that its members remain distinguishable.
      const target = Math.max(current, c.items.length > 1 ? 12 : 14);
      const size = map.getSize?.();
      const mapH: number = size?.y ?? 0;
      // The sheet is about to occupy the bottom `sheetH` of the map, so shift the
      // centre up by half of that — the pin then sits in the visible upper band.
      const sheetPx = mapH * 0.5; // the sheet always (re)opens at half height
      map.flyTo([c.lat, c.lng], target, { animate: true, duration: 0.6 });
      if (sheetPx > 0) {
        // panBy after the fly settles, else Leaflet cancels the in-flight animation.
        // Leaflet's panBy moves the map pane BY the offset, so content shifts the
        // opposite way: a POSITIVE y lifts the pin toward the top of the map,
        // which is the direction that gets it clear of the sheet.
        map.once('moveend', () => map.panBy([0, sheetPx / 2], { animate: true }));
      }
    }
    setCluster(c);
    setSheetH(0.5); // reset to half height each time a pin is opened
    resetFilters(); // a new pin starts with a clean filter
  };

  // Opening one photo from a pin's hover strip.
  const openLightboxRef = useRef<(id: string) => void>(() => {});
  openLightboxRef.current = (id) => api.getState().openLightbox(id);

  // Create the map once when entering a map tile mode.
  useEffect(() => {
    if (mapMode === 'grid' || !containerRef.current || mapRef.current) return;
    let cancelled = false;

    void import('leaflet').then((mod) => {
      const L = (mod as any).default ?? mod;
      if (cancelled || !containerRef.current) return;
      leafletRef.current = L;
      const map = L.map(containerRef.current, {
        zoomControl: false,
        attributionControl: true,
        worldCopyJump: true,
      }).setView([20, 0], 2);
      mapRef.current = map;
      addTiles();
      addMarkers();
      // Re-cluster pins each time the zoom changes (precision is zoom-dependent).
      map.on('zoomend', addMarkers);
      // Center on a focused photo (from the Info mini-map), else fit to all markers.
      if (mapFocus) {
        map.setView([mapFocus.lat, mapFocus.lng], 12, { animate: false });
      } else if (located.length) {
        const bounds = L.latLngBounds(located.map((m) => [m.location!.lat, m.location!.lng]));
        map.fitBounds(bounds.pad(0.3), { animate: false });
      }
    });

    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mapMode]);

  // Tear the map down when leaving map tile modes.
  useEffect(() => {
    if (mapMode === 'grid' && mapRef.current) {
      clearTipTimers();
      mapRef.current.remove();
      mapRef.current = null;
      tileRef.current = null;
      markersRef.current = null;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mapMode]);

  // Destroy on unmount.
  useEffect(() => {
    return () => {
      clearTipTimers();
      if (mapRef.current) {
        mapRef.current.remove();
        mapRef.current = null;
        tileRef.current = null;
        markersRef.current = null;
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const addTiles = () => {
    const L = leafletRef.current;
    const map = mapRef.current;
    if (!L || !map) return;
    if (tileRef.current) {
      map.removeLayer(tileRef.current);
      tileRef.current = null;
    }
    const cfg = mapMode === 'satellite' ? MAP_TILES.satellite : MAP_TILES.map;
    tileRef.current = L.tileLayer(cfg.url, {
      attribution: cfg.attribution,
      maxZoom: cfg.maxZoom,
    }).addTo(map);
  };

  const addMarkers = () => {
    const L = leafletRef.current;
    const map = mapRef.current;
    if (!L || !map) return;
    // Keep markers in a dedicated layer group so they can be re-synced wholesale.
    if (!markersRef.current) markersRef.current = L.layerGroup().addTo(map);
    // Every marker is about to be destroyed — kill their hover timers first.
    clearTipTimers();
    markersRef.current.clearLayers();

    // Tighter clustering as you zoom in, so pins split apart (macOS behaviour).
    const zoom = map.getZoom?.() ?? 2;
    const precision = zoom < 4 ? 0 : zoom < 7 ? 1 : zoom < 11 ? 2 : 3;
    const clusters = clusterByLocation(locatedRef.current, precision);

    for (const c of clusters) {
      const cover = c.items[0]!;
      const multi = c.items.length > 1;
      // Static markup only (no interpolated data) so Leaflet's innerHTML can't be
      // injected; the thumbnail src + count are set via DOM after the marker mounts.
      const icon = L.divIcon({
        className: 'apg-pin-wrap',
        // Static markup only (data set via DOM after mount → XSS-safe). Includes a
        // hover tooltip: one preview for a single photo, an auto-advancing strip
        // of thumbnails for a cluster.
        html:
          '<span class="apg-pin"><img class="apg-pin__img" alt=""/>' +
          '<span class="apg-pin__count"></span>' +
          '<span class="apg-pin__tip"><img class="apg-pin__tip-img" alt=""/>' +
          '<span class="apg-pin__strip"></span>' +
          '<span class="apg-pin__tip-cap"></span></span></span>',
        iconSize: [56, 64],
        iconAnchor: [28, 64],
      });
      const marker = L.marker([c.lat, c.lng], { icon, keyboard: false });
      marker.on('add', () => {
        const el: HTMLElement | null = marker.getElement();
        if (!el) return;
        const thumb = cover.thumbnail ?? cover.src;
        const img = el.querySelector('.apg-pin__img') as HTMLImageElement | null;
        if (img) img.src = thumb;
        const tipImg = el.querySelector('.apg-pin__tip-img') as HTMLImageElement | null;
        const strip = el.querySelector('.apg-pin__strip') as HTMLElement | null;
        const cap = el.querySelector('.apg-pin__tip-cap') as HTMLElement | null;

        // Keep the hover tooltip inside the map: on enter, measure it against the map
        // container and slide it horizontally (--tip-dx) / flip it below (--below) so it
        // never overflows and gets clipped by the gallery's rounded, overflow-hidden shell.
        const pinEl = el.querySelector('.apg-pin') as HTMLElement | null;
        const tipEl = el.querySelector('.apg-pin__tip') as HTMLElement | null;
        const clampTip = () => {
          if (!tipEl) return;
          const mapEl = el.closest('.leaflet-container') as HTMLElement | null;
          if (!mapEl) return;
          tipEl.style.setProperty('--tip-dx', '0px');
          tipEl.classList.remove('apg-pin__tip--below');
          const m = mapEl.getBoundingClientRect();
          const t = tipEl.getBoundingClientRect();
          const pad = 8;
          let dx = 0;
          if (t.left < m.left + pad) dx = m.left + pad - t.left;
          else if (t.right > m.right - pad) dx = m.right - pad - t.right;
          if (dx) tipEl.style.setProperty('--tip-dx', `${Math.round(dx)}px`);
          if (t.top < m.top + pad) tipEl.classList.add('apg-pin__tip--below');
        };
        pinEl?.addEventListener('mouseenter', clampTip);
        if (cap) {
          // textContent (never innerHTML) — place names are attacker-influencable.
          const place = cover.location?.place ?? '';
          const n = c.items.length;
          cap.textContent = place ? `${place}${n > 1 ? ` · ${n} photos` : ''}` : `${n} photo${n === 1 ? '' : 's'}`;
        }
        const badge = el.querySelector('.apg-pin__count') as HTMLElement | null;
        if (badge) {
          // The badge always carries the cluster's TOTAL, and only appears when
          // there is genuinely more than one photo here.
          badge.textContent = String(c.items.length);
          badge.style.display = multi ? '' : 'none';
        }

        if (!multi || !strip) {
          if (tipImg) tipImg.src = thumb;
          return;
        }

        // ---- multi-photo pin: horizontal strip + auto-advancing preview ----
        if (tipImg) tipImg.src = thumb;
        const shown = c.items.slice(0, TIP_STRIP_MAX);
        const cells: HTMLElement[] = [];
        for (const item of shown) {
          const cell = document.createElement('button');
          cell.type = 'button';
          cell.className = 'apg-pin__strip-cell';
          cell.title = item.name;
          const thumbEl = document.createElement('img');
          thumbEl.alt = '';
          thumbEl.src = item.thumbnail ?? item.src;
          cell.appendChild(thumbEl);
          // Opening a specific photo must not also open the cluster sheet.
          cell.addEventListener('click', (ev) => {
            ev.stopPropagation();
            ev.preventDefault();
            openLightboxRef.current(item.id);
          });
          strip.appendChild(cell);
          cells.push(cell);
        }
        if (c.items.length > TIP_STRIP_MAX) {
          const more = document.createElement('span');
          more.className = 'apg-pin__strip-more';
          more.textContent = `+${c.items.length - TIP_STRIP_MAX}`;
          strip.appendChild(more);
        }

        // Auto-advance the big preview (and the highlighted cell) while hovered.
        const pin = el.querySelector('.apg-pin') as HTMLElement | null;
        if (!pin) return;
        let timer: ReturnType<typeof setInterval> | null = null;
        let frame = 0;
        const paint = () => {
          const item = c.items[frame % c.items.length]!;
          if (tipImg) tipImg.src = item.thumbnail ?? item.src;
          cells.forEach((cell, i) =>
            cell.classList.toggle('apg-pin__strip-cell--on', i === frame % c.items.length),
          );
        };
        const stop = () => {
          if (timer === null) return;
          clearInterval(timer);
          tipTimersRef.current.delete(timer);
          timer = null;
          frame = 0;
          if (tipImg) tipImg.src = thumb;
          cells.forEach((cell) => cell.classList.remove('apg-pin__strip-cell--on'));
        };
        const start = () => {
          if (timer !== null) return;
          frame = 0;
          paint();
          timer = setInterval(() => {
            frame += 1;
            paint();
          }, TIP_FRAME_MS);
          tipTimersRef.current.add(timer);
        };
        pin.addEventListener('mouseenter', start);
        pin.addEventListener('mouseleave', stop);
        // Leaflet destroys the element on clearLayers(); `remove` fires first.
        marker.on('remove', stop);
      });
      marker.on('click', () => openClusterRef.current(c));
      markersRef.current.addLayer(marker);
    }
  };

  // Swap tile layer when Map/Satellite toggles.
  useEffect(() => {
    if (mapRef.current) addTiles();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mapMode]);

  // Re-sync markers when the located set changes while the map stays mounted.
  useEffect(() => {
    if (mapRef.current) addMarkers();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [media]);

  // Recenter when a photo's location is focused (from the Info mini-map).
  useEffect(() => {
    if (mapFocus && mapRef.current) {
      mapRef.current.setView([mapFocus.lat, mapFocus.lng], 12, { animate: true });
    }
  }, [mapFocus]);

  // Distinct object labels present in the open cluster, most common first.
  const clusterObjects = useMemo(() => {
    if (!cluster) return [] as Array<{ label: string; count: number }>;
    const counts = new Map<string, number>();
    for (const m of cluster.items) {
      for (const label of new Set(m.objectLabels)) {
        counts.set(label, (counts.get(label) ?? 0) + 1);
      }
    }
    return [...counts.entries()]
      .map(([label, count]) => ({ label, count }))
      .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
  }, [cluster]);

  // Search + date range + object chip, combined with AND.
  const filtered = useMemo<MediaItem[]>(() => {
    if (!cluster) return [];
    let items = cluster.items;
    if (objectFilter) items = items.filter((m) => m.objectLabels.includes(objectFilter));
    const from = parseDateInput(fromDate, false);
    const to = parseDateInput(toDate, true);
    if (from !== null) items = items.filter((m) => m.takenAt >= from);
    if (to !== null) items = items.filter((m) => m.takenAt <= to);
    // Same matcher as the global search bar, so "car" means the same thing here.
    if (query.trim()) items = searchMedia(items, query);
    return items;
  }, [cluster, query, fromDate, toDate, objectFilter]);

  const filterActive = Boolean(query.trim() || fromDate || toDate || objectFilter);

  if (mapMode === 'grid') {
    // The Grid tab shows ALL photos in the macOS Memories-style mosaic (not just
    // located ones) so it's a rich date-grouped collage, per the design.
    const all = liveMedia(media);
    if (all.length === 0) {
      return (
        <div className="apg-empty">
          <div className="apg-empty__card">
            <div className="apg-empty__title" style={{ fontSize: 22 }}>
              No Photos
            </div>
            <div className="apg-empty__subtitle">Imported photos appear here as a mosaic.</div>
          </div>
        </div>
      );
    }
    return <MosaicGrid items={all} groupBy="month" />;
  }

  return (
    <div className="apg-map">
      <div ref={containerRef} className="apg-map__leaflet" />
      <div className="apg-map__controls">
        <button
          type="button"
          className="apg-iconbtn apg-iconbtn--circle"
          aria-label="Zoom in"
          onClick={() => mapRef.current?.zoomIn()}
        >
          <Icon name="plus" />
        </button>
        <button
          type="button"
          className="apg-iconbtn apg-iconbtn--circle"
          aria-label="Zoom out"
          onClick={() => mapRef.current?.zoomOut()}
        >
          <Icon name="minus" />
        </button>
        <button
          type="button"
          className="apg-iconbtn apg-iconbtn--circle"
          aria-label="Reset north"
          onClick={() => mapRef.current?.setView([20, 0], 2)}
        >
          <Icon name="compass" size={22} />
        </button>
      </div>
      <a className="apg-map__legal" href="https://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer noopener">
        Legal
      </a>

      {cluster ? (
        <div
          className="apg-map__sheet"
          role="dialog"
          aria-label="Photos at this location"
          style={{ height: `${Math.round(sheetH * 100)}%` }}
        >
          <div
            className="apg-map__grip"
            onPointerDown={onHandleDown}
            onPointerMove={onHandleMove}
            onPointerUp={onHandleUp}
            onPointerCancel={onHandleUp}
            title="Drag to resize"
            aria-hidden
          >
            <span className="apg-map__grip-bar" />
          </div>
          <div className="apg-map__sheet-head">
            <div>
              <div style={{ fontWeight: 700, fontSize: 16 }}>
                {cluster.items[0]?.location?.place ?? 'This location'}
              </div>
              <div style={{ color: 'var(--apg-text-secondary)', fontSize: 12 }} aria-live="polite">
                {filterActive
                  ? `${filtered.length} of ${cluster.items.length} photo${
                      cluster.items.length === 1 ? '' : 's'
                    }`
                  : `${cluster.items.length} photo${cluster.items.length === 1 ? '' : 's'}`}
              </div>
            </div>
            <button
              type="button"
              className="apg-iconbtn"
              aria-label="Close"
              onClick={() => setCluster(null)}
            >
              <Icon name="close" />
            </button>
          </div>

          {/* Search + date range + object chips — all AND-combined. */}
          <div className="apg-map__sheet-filters">
            <div className="apg-mapfilter__row">
              <div className="apg-mapfilter__search">
                <Icon name="search" size={15} />
                <input
                  type="search"
                  aria-label="Search photos at this location"
                  placeholder="Search photos, objects, text…"
                  value={query}
                  onChange={(e) => setQuery(e.target.value)}
                />
                {filterActive ? (
                  <button
                    type="button"
                    className="apg-mapfilter__clear"
                    aria-label="Clear filters"
                    title="Clear filters"
                    onClick={resetFilters}
                  >
                    <Icon name="close" size={13} />
                  </button>
                ) : null}
              </div>
            </div>
            <div className="apg-mapfilter__row apg-mapfilter__dates">
              <DateRangeControl
                from={fromDate}
                to={toDate}
                onChange={(f, t) => {
                  setFromDate(f);
                  setToDate(t);
                }}
              />
            </div>
            {clusterObjects.length ? (
              <div className="apg-mapfilter__chips" role="group" aria-label="Filter by object">
                {clusterObjects.map(({ label, count }) => {
                  const on = objectFilter === label;
                  return (
                    <button
                      key={label}
                      type="button"
                      className={[
                        'apg-mapfilter__chip',
                        on ? 'apg-mapfilter__chip--on' : '',
                      ]
                        .filter(Boolean)
                        .join(' ')}
                      aria-pressed={on}
                      onClick={() => setObjectFilter(on ? null : label)}
                    >
                      {label}
                      <span className="apg-mapfilter__chip-n">{count}</span>
                    </button>
                  );
                })}
              </div>
            ) : null}
          </div>

          <div className="apg-map__sheet-body apg-scroll">
            {filtered.length === 0 ? (
              <div className="apg-mapfilter__empty">
                <Icon name="search" size={26} />
                <div className="apg-mapfilter__empty-title">No matching photos</div>
                <div className="apg-mapfilter__empty-sub">
                  Try a different search, date range, or object.
                </div>
                <button type="button" className="apg-btn" onClick={resetFilters}>
                  Clear filters
                </button>
              </div>
            ) : (
              groupByTime(filtered, 'day').map((s) => (
                <MediaGrid key={s.key} items={s.items} title={s.title} />
              ))
            )}
          </div>
        </div>
      ) : null}
    </div>
  );
}
