import React, { useCallback, useEffect, useRef, useState } from 'react';
import { GeoJSON, MapContainer, TileLayer, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import './Travel.css';

/* ─────────────────────────────────────────────
   Constants
───────────────────────────────────────────── */
const PAGE_SIZE = 20;
const THUMB_STRIP_LIMIT = 36;

/* ─────────────────────────────────────────────
   Region accent palette — each destination
   gets its own identity color
───────────────────────────────────────────── */
const REGION_PALETTE = {
    japan:        '#e05c4b',   // vermillion
    korea:        '#d64f6e',   // rose
    china:        '#c84b3a',   // crimson
    europe:       '#5b8fc4',   // cobalt
    france:       '#6f8fc4',   // slate blue
    italy:        '#78a46e',   // olive
    spain:        '#c4844a',   // terracotta
    sea:          '#4aad8b',   // jade
    thailand:     '#4aad8b',
    vietnam:      '#5faa78',
    bali:         '#7aac5a',
    indonesia:    '#8aaa4a',
    america:      '#b4885c',   // desert
    usa:          '#b4885c',
    canada:       '#6a9890',   // glacier
    africa:       '#c47c3c',   // ochre
    middle:       '#c4a24a',   // sand gold
    dubai:        '#c4a24a',
    default:      '#c8905e',   // warm amber
};

const getRegionAccent = (regionId = '') => {
    const id = regionId.toLowerCase();
    for (const [key, color] of Object.entries(REGION_PALETTE)) {
        if (key !== 'default' && id.includes(key)) return color;
    }
    return REGION_PALETTE.default;
};

/* ─────────────────────────────────────────────
   Editorial card layout pattern
   4-column grid, repeating every 7 cards
───────────────────────────────────────────── */
const LAYOUT_SEQ = ['hero', '', 'tall', '', '', 'wide', ''];
const getCardLayout = (index) => LAYOUT_SEQ[index % LAYOUT_SEQ.length];

/* ─────────────────────────────────────────────
   Utility functions
───────────────────────────────────────────── */
const normalizePhotos = (items = []) =>
    items
        .map((item) => {
            if (typeof item === 'string') return { src: item, title: '' };
            if (!item) return null;
            return {
                src: item.thumb || item.url || item.path || item.src || '',
                title: item.title || item.name || item.file || '',
                original: item.url || item.path || item.src || '',
                file: item.file || '',
                album: item.album || '',
            };
        })
        .filter((item) => item && item.src);

const hasSummaryInfo = (payload) =>
    payload &&
    (Object.prototype.hasOwnProperty.call(payload, 'total') ||
        Object.prototype.hasOwnProperty.call(payload, 'matched_albums'));

const getPhotoLabel = (photo) => {
    if (!photo) return '';
    if (photo.title) return photo.title;
    if (photo.file) return photo.file;
    if (!photo.src) return '';
    const parts = photo.src.split('/');
    return parts[parts.length - 1];
};

const getStripRange = (length, center) => {
    if (length <= THUMB_STRIP_LIMIT) return [0, length];
    const half = Math.floor(THUMB_STRIP_LIMIT / 2);
    let start = Math.max(0, center - half);
    let end = start + THUMB_STRIP_LIMIT;
    if (end > length) {
        end = length;
        start = end - THUMB_STRIP_LIMIT;
    }
    return [start, end];
};

/* ─────────────────────────────────────────────
   PhotoCard — single photo tile
───────────────────────────────────────────── */
const PhotoCard = ({ photo, index, onSelect, regionAccent }) => {
    const label = getPhotoLabel(photo);
    const layout = getCardLayout(index);
    const isEager = index < 4;

    return (
        <article
            className={`photo-card ${layout ? `photo-card--${layout}` : ''}`}
            style={{ '--reveal-delay': `${Math.min(index, 12) * 50}ms` }}
            onClick={(e) => onSelect(index, e)}
            role="button"
            tabIndex={0}
            onKeyDown={(e) => e.key === 'Enter' && onSelect(index, e)}
            aria-label={label || `Photo ${index + 1}`}
        >
            {/* Image */}
            <img
                src={photo.src}
                alt={label}
                loading={isEager ? 'eager' : 'lazy'}
                decoding="async"
                fetchpriority={isEager ? 'high' : 'auto'}
                onError={(e) => {
                    if (photo.original && e.currentTarget.src !== photo.original) {
                        e.currentTarget.src = photo.original;
                    }
                }}
            />

            {/* Hover overlay */}
            <div className="photo-card__overlay">
                <div className="photo-card__overlay-inner">
                    <span
                        className="photo-card__index"
                        style={{ '--accent': regionAccent }}
                    >
                        {String(index + 1).padStart(2, '0')}
                    </span>
                    {label && (
                        <p className="photo-card__label">{label}</p>
                    )}
                </div>
            </div>

            {/* Print border effect */}
            <div className="photo-card__frame" aria-hidden />
        </article>
    );
};

/* ─────────────────────────────────────────────
   PhotoMosaic — grid with IntersectionObserver
   lazy reveal + infinite scroll sentinel
───────────────────────────────────────────── */
const PhotoMosaic = ({
    photos,
    regionLabel,
    regionAccent,
    onSelectPhoto,
    onLoadMore,
    hasNext,
    isLoadingMore,
}) => {
    const sentinelRef = useRef(null);
    const gridRef = useRef(null);
    const revealObserverRef = useRef(null);

    /* Infinite scroll sentinel */
    useEffect(() => {
        const sentinel = sentinelRef.current;
        if (!sentinel || !onLoadMore) return;
        const io = new IntersectionObserver(
            ([entry]) => {
                if (entry.isIntersecting && !isLoadingMore && hasNext) onLoadMore();
            },
            { rootMargin: '300px' }
        );
        io.observe(sentinel);
        return () => io.disconnect();
    }, [hasNext, isLoadingMore, onLoadMore]);

    /* Scroll-reveal observer */
    useEffect(() => {
        revealObserverRef.current = new IntersectionObserver(
            (entries) => {
                entries.forEach((entry) => {
                    if (!entry.isIntersecting) return;
                    entry.target.dataset.revealed = 'true';
                    revealObserverRef.current?.unobserve(entry.target);
                });
            },
            { rootMargin: '120px', threshold: 0.05 }
        );
        return () => revealObserverRef.current?.disconnect();
    }, []);

    useEffect(() => {
        const observer = revealObserverRef.current;
        const grid = gridRef.current;
        if (!observer || !grid) return;
        const cards = grid.querySelectorAll('.photo-card:not([data-revealed="true"])');
        cards.forEach((c) => observer.observe(c));
        const fallback = setTimeout(() => {
            grid.querySelectorAll('.photo-card:not([data-revealed="true"])')
                .forEach((c) => (c.dataset.revealed = 'true'));
        }, 600);
        return () => {
            clearTimeout(fallback);
            cards.forEach((c) => observer.unobserve(c));
        };
    }, [photos.length]);

    return (
        <>
            <div className="photo-mosaic" ref={gridRef}>
                {photos.map((photo, index) => (
                    <PhotoCard
                        key={`${regionLabel}-${photo.src}-${index}`}
                        photo={photo}
                        index={index}
                        onSelect={onSelectPhoto}
                        regionAccent={regionAccent}
                    />
                ))}
            </div>

            <div className="mosaic-footer" ref={sentinelRef}>
                {isLoadingMore && (
                    <div className="mosaic-loading">
                        <span className="mosaic-loading__dot" />
                        <span className="mosaic-loading__dot" />
                        <span className="mosaic-loading__dot" />
                    </div>
                )}
                {!hasNext && photos.length > 0 && (
                    <p className="mosaic-end">
                        <span>—</span>
                        &nbsp;{photos.length} frames developed&nbsp;
                        <span>—</span>
                    </p>
                )}
            </div>
        </>
    );
};

/* ─────────────────────────────────────────────
   MapLayer — GeoJSON region polygons
───────────────────────────────────────────── */
const MapLayer = ({ geojson, selectedRegionId, onSelectRegion }) => {
    const map = useMap();
    if (!geojson) return null;

    return (
        <GeoJSON
            key={selectedRegionId || 'none'}
            data={geojson}
            style={(feature) => {
                const isSelected = feature?.properties?.id === selectedRegionId;
                const accent = getRegionAccent(feature?.properties?.id || '');
                return {
                    color: isSelected ? accent : 'rgba(200,160,100,0.4)',
                    weight: isSelected ? 2 : 1,
                    fillColor: isSelected ? accent : 'rgba(200,144,94,0.15)',
                    fillOpacity: isSelected ? 0.25 : 0.12,
                };
            }}
            onEachFeature={(feature, layer) => {
                const name = feature?.properties?.name || feature?.properties?.id || '';
                if (name) layer.bindTooltip(name, { sticky: true, className: 'map-tooltip' });
                layer.on('click', () => {
                    if (!feature?.properties?.id) return;
                    map.fitBounds(layer.getBounds(), { padding: [40, 40], animate: true });
                    onSelectRegion({
                        id: feature.properties.id,
                        name: feature.properties.name || feature.properties.id,
                    });
                });
            }}
        />
    );
};

/* ─────────────────────────────────────────────
   FilmStrip — horizontal thumbnail rail
───────────────────────────────────────────── */
const FilmStrip = ({
    photos,
    selectedIndex,
    stripStart,
    stripEnd,
    thumbStripRef,
    onSelect,
    onScrollPrev,
    onScrollNext,
}) => (
    <div className="filmstrip">
        <button
            type="button"
            className="filmstrip__nav is-prev"
            onClick={onScrollPrev}
            aria-label="이전 썸네일"
        >
            ‹
        </button>

        <div className="filmstrip__rail" ref={thumbStripRef} role="list">
            {/* Film perforations — decorative */}
            <div className="filmstrip__holes" aria-hidden>
                {Array.from({ length: 20 }).map((_, i) => (
                    <span key={i} className="filmstrip__hole" />
                ))}
            </div>

            <div className="filmstrip__frames">
                {photos.slice(stripStart, stripEnd).map((photo, idx) => {
                    const realIndex = stripStart + idx;
                    const isActive = realIndex === selectedIndex;
                    return (
                        <button
                            key={`${photo.src}-${realIndex}`}
                            type="button"
                            className={`filmstrip__frame ${isActive ? 'is-active' : ''}`}
                            data-thumb-index={realIndex}
                            onClick={() => onSelect(realIndex)}
                            aria-label={getPhotoLabel(photo)}
                            role="listitem"
                        >
                            <img
                                src={photo.src}
                                alt={getPhotoLabel(photo)}
                                loading="lazy"
                                decoding="async"
                                onError={(e) => {
                                    if (photo.original && e.currentTarget.src !== photo.original)
                                        e.currentTarget.src = photo.original;
                                }}
                            />
                            <span className="filmstrip__frame-num">
                                {String(realIndex + 1).padStart(2, '0')}
                            </span>
                        </button>
                    );
                })}
            </div>
        </div>

        <button
            type="button"
            className="filmstrip__nav is-next"
            onClick={onScrollNext}
            aria-label="다음 썸네일"
        >
            ›
        </button>
    </div>
);

/* ─────────────────────────────────────────────
   Lightbox — cinematic full-screen viewer
───────────────────────────────────────────── */
const Lightbox = ({
    photos,
    selectedIndex,
    slideToken,
    slideDirection,
    loadingMore,
    hasNext,
    photoSummary,
    selectedRegion,
    backdropBlur,
    setBackdropBlur,
    thumbScrollDuration,
    setThumbScrollDuration,
    toastMessage,
    stripStart,
    stripEnd,
    thumbStripRef,
    onClose,
    onPrev,
    onNext,
    onSelectThumb,
    onScrollThumbPrev,
    onScrollThumbNext,
}) => {
    const photo = photos[selectedIndex];
    const regionAccent = getRegionAccent(selectedRegion?.id || '');
    const totalCount = photoSummary?.total ?? photos.length;

    return (
        <div
            className="lightbox"
            role="dialog"
            aria-modal="true"
            style={{
                '--lb-blur': `${backdropBlur}px`,
                '--lb-accent': regionAccent,
            }}
            onClick={onClose}
        >
            <div className="lightbox__inner" onClick={(e) => e.stopPropagation()}>

                {/* ── Top bar ──────────────────────────── */}
                <header className="lightbox__topbar">
                    <div className="lightbox__counter">
                        <span className="lightbox__counter-current" style={{ color: regionAccent }}>
                            {String(selectedIndex + 1).padStart(2, '0')}
                        </span>
                        <span className="lightbox__counter-sep">/</span>
                        <span className="lightbox__counter-total">
                            {String(totalCount).padStart(2, '0')}
                        </span>
                    </div>

                    <div className="lightbox__region" style={{ '--accent': regionAccent }}>
                        <span className="lightbox__region-dot" />
                        <span className="lightbox__region-name">
                            {selectedRegion?.name || 'Archive'}
                        </span>
                        {photoSummary?.albums?.length > 0 && (
                            <span className="lightbox__album">
                                {photoSummary.albums[0].album}
                            </span>
                        )}
                    </div>

                    <div className="lightbox__controls">
                        <label className="lb-control">
                            <span>BLUR</span>
                            <input
                                type="range"
                                min={0}
                                max={16}
                                step={2}
                                value={backdropBlur}
                                onChange={(e) => setBackdropBlur(Number(e.target.value))}
                                aria-label="Background blur"
                            />
                            <span className="lb-control__val">{backdropBlur}</span>
                        </label>
                    </div>

                    <button
                        type="button"
                        className="lightbox__close"
                        onClick={onClose}
                        aria-label="Close"
                    >
                        <svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden>
                            <path d="M2 2l14 14M16 2L2 16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
                        </svg>
                    </button>
                </header>

                {/* ── Photo stage ──────────────────────── */}
                <div className="lightbox__stage">
                    <button
                        type="button"
                        className="lightbox__arrow is-prev"
                        onClick={onPrev}
                        disabled={selectedIndex === 0}
                        aria-label="Previous"
                    >
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden>
                            <path d="M15 18l-6-6 6-6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
                        </svg>
                    </button>

                    <div className="lightbox__frame">
                        <img
                            key={`${selectedIndex}-${slideToken}`}
                            className={`lightbox__photo slide-${slideDirection}`}
                            src={photo?.original || photo?.src}
                            alt={getPhotoLabel(photo)}
                            onError={(e) => {
                                if (photo?.original && e.currentTarget.src !== photo.original)
                                    e.currentTarget.src = photo.original;
                            }}
                        />
                        {/* Photo frame decoration */}
                        <div className="lightbox__photo-frame" aria-hidden />
                    </div>

                    <button
                        type="button"
                        className={`lightbox__arrow is-next ${loadingMore && hasNext && selectedIndex === photos.length - 1 ? 'is-loading' : ''}`}
                        onClick={onNext}
                        disabled={selectedIndex === photos.length - 1 && !hasNext}
                        aria-label="Next"
                        aria-busy={loadingMore && hasNext && selectedIndex === photos.length - 1}
                    >
                        {loadingMore && hasNext && selectedIndex === photos.length - 1 ? (
                            <span className="lightbox__spinner" aria-hidden />
                        ) : (
                            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden>
                                <path d="M9 18l6-6-6-6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
                            </svg>
                        )}
                    </button>
                </div>

                {/* ── Photo metadata ───────────────────── */}
                {(photo?.album || photo?.file) && (
                    <p className="lightbox__meta">
                        {photo.album}
                        {photo.file ? <span> · {photo.file}</span> : null}
                    </p>
                )}

                {/* ── Film strip ───────────────────────── */}
                <FilmStrip
                    photos={photos}
                    selectedIndex={selectedIndex}
                    stripStart={stripStart}
                    stripEnd={stripEnd}
                    thumbStripRef={thumbStripRef}
                    onSelect={onSelectThumb}
                    onScrollPrev={onScrollThumbPrev}
                    onScrollNext={onScrollThumbNext}
                />

                {/* ── Toast ────────────────────────────── */}
                {toastMessage && (
                    <div className="lightbox__toast">{toastMessage}</div>
                )}
            </div>
        </div>
    );
};

/* ─────────────────────────────────────────────
   Travel — main page component
───────────────────────────────────────────── */
const Travel = () => {
    const [photos, setPhotos] = useState([]);
    const [photoSummary, setPhotoSummary] = useState(null);
    const [loading, setLoading] = useState(false);
    const [loadingMore, setLoadingMore] = useState(false);
    const [error, setError] = useState('');
    const [selectedRegion, setSelectedRegion] = useState(null);
    const [regionsGeojson, setRegionsGeojson] = useState(null);
    const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(null);
    const [slideDirection, setSlideDirection] = useState('next');
    const [slideToken, setSlideToken] = useState(0);
    const [backdropBlur, setBackdropBlur] = useState(6);
    const [thumbScrollDuration, setThumbScrollDuration] = useState(360);
    const [toastMessage, setToastMessage] = useState('');
    const [page, setPage] = useState(1);
    const [hasNext, setHasNext] = useState(true);

    const touchStartXRef = useRef(null);
    const pendingAdvanceRef = useRef(null);
    const toastTimerRef = useRef(null);
    const thumbStripRef = useRef(null);
    const cacheRef = useRef(new Map());
    const cacheTtlMs = 10 * 60 * 1000;
    const travelRef = useRef(null);

    const regionAccent = getRegionAccent(selectedRegion?.id || '');

    /* ── Scroll-reveal for page sections ─── */
    useEffect(() => {
        const root = travelRef.current;
        if (!root) return;
        const io = new IntersectionObserver(
            (entries) => {
                entries.forEach((entry) => {
                    if (!entry.isIntersecting) return;
                    entry.target.dataset.revealed = 'true';
                    io.unobserve(entry.target);
                });
            },
            { rootMargin: '140px' }
        );
        root.querySelectorAll('[data-reveal]').forEach((n) => io.observe(n));
        return () => io.disconnect();
    }, []);

    /* ── Load GeoJSON regions ──────────────── */
    useEffect(() => {
        const controller = new AbortController();
        (async () => {
            try {
                const res = await fetch('/api/travel/regions', { signal: controller.signal });
                if (!res.ok) throw new Error(`지역 정보 로딩 실패 (${res.status})`);
                setRegionsGeojson(await res.json());
            } catch (err) {
                if (err?.name !== 'AbortError') setError(err?.message ?? String(err));
            }
        })();
        return () => controller.abort();
    }, []);

    /* ── Load photos for selected region ──── */
    useEffect(() => {
        if (!selectedRegion) {
            setPhotos([]); setPhotoSummary(null);
            setSelectedPhotoIndex(null); setPage(1); setHasNext(true);
            return;
        }
        const controller = new AbortController();
        (async () => {
            const cached = cacheRef.current.get(selectedRegion.id);
            if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
                setPhotos(cached.items); setPhotoSummary(cached.summary ?? null);
                setPage(cached.page ?? 1); setHasNext(cached.hasNext ?? true);
                setLoading(false); setLoadingMore(false); setError('');
                setSelectedPhotoIndex(cached.items.length > 0 ? 0 : null);
                return;
            }
            setLoading(true); setLoadingMore(false); setError('');
            setPhotos([]); setPhotoSummary(null); setSelectedPhotoIndex(null);
            setPage(1); setHasNext(true);

            try {
                const res = await fetch(
                    `/api/travel/photos?region=${encodeURIComponent(selectedRegion.id)}&page=1&size=${PAGE_SIZE}`,
                    { signal: controller.signal }
                );
                if (!res.ok) throw new Error(`지역 사진 로딩 실패 (${res.status})`);
                const json = await res.json();
                const items = Array.isArray(json) ? json : json.items ?? [];
                const meta = Array.isArray(json) ? {} : json ?? {};
                const normalized = normalizePhotos(items);
                const nextHasNext = typeof meta.has_next === 'boolean'
                    ? meta.has_next
                    : typeof meta.hasNext === 'boolean' ? meta.hasNext : normalized.length >= PAGE_SIZE;
                const summary = hasSummaryInfo(meta)
                    ? { total: meta.total, albums: meta.matched_albums ?? [] }
                    : null;
                setPhotoSummary(summary);
                setPhotos(normalized);
                setHasNext(nextHasNext);
                setPage(2);
                cacheRef.current.set(selectedRegion.id, {
                    timestamp: Date.now(),
                    items: normalized, page: 2, hasNext: nextHasNext, summary,
                });
                setSelectedPhotoIndex(normalized.length > 0 ? 0 : null);
            } catch (err) {
                if (err?.name === 'AbortError') return;
                setError(err?.message ?? String(err));
                setPhotos([]); setPhotoSummary(null);
            } finally {
                setLoading(false);
            }
        })();
        return () => controller.abort();
    }, [selectedRegion]);

    /* ── Load more photos ──────────────────── */
    const loadMorePhotos = useCallback(async () => {
        if (!selectedRegion || loading || loadingMore || !hasNext) return;
        setLoadingMore(true); setError('');
        try {
            const res = await fetch(
                `/api/travel/photos?region=${encodeURIComponent(selectedRegion.id)}&page=${page}&size=${PAGE_SIZE}`
            );
            if (!res.ok) throw new Error(`지역 사진 로딩 실패 (${res.status})`);
            const json = await res.json();
            const items = Array.isArray(json) ? json : json.items ?? [];
            const meta = Array.isArray(json) ? {} : json ?? {};
            const normalized = normalizePhotos(items);
            const nextHasNext = typeof meta.has_next === 'boolean'
                ? meta.has_next
                : typeof meta.hasNext === 'boolean' ? meta.hasNext : normalized.length >= PAGE_SIZE;
            const summary = hasSummaryInfo(meta)
                ? { total: meta.total ?? photoSummary?.total, albums: meta.matched_albums ?? photoSummary?.albums ?? [] }
                : null;
            setPhotos((prev) => {
                const merged = [...prev, ...normalized];
                cacheRef.current.set(selectedRegion.id, {
                    timestamp: Date.now(),
                    items: merged, page: page + 1, hasNext: nextHasNext,
                    summary: photoSummary ?? summary,
                });
                return merged;
            });
            if (!photoSummary && summary) setPhotoSummary(summary);
            setHasNext(nextHasNext);
            setPage((p) => p + 1);
        } catch (err) {
            setError(err?.message ?? String(err));
        } finally {
            setLoadingMore(false);
        }
    }, [hasNext, loading, loadingMore, page, photoSummary, selectedRegion]);

    /* ── Slide helpers ─────────────────────── */
    const bumpSlide = (dir) => {
        setSlideDirection(dir);
        setSlideToken((t) => t + 1);
    };

    const showToast = useCallback((msg) => {
        setToastMessage(msg);
        if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
        toastTimerRef.current = setTimeout(() => setToastMessage(''), 1600);
    }, []);

    useEffect(() => () => { if (toastTimerRef.current) clearTimeout(toastTimerRef.current); }, []);

    /* ── Navigation ────────────────────────── */
    const goPrev = useCallback(() => {
        if (selectedPhotoIndex == null || selectedPhotoIndex <= 0) return;
        bumpSlide('prev');
        setSelectedPhotoIndex((i) => i - 1);
    }, [selectedPhotoIndex]);

    const goNext = useCallback(() => {
        if (selectedPhotoIndex == null) return;
        if (selectedPhotoIndex < photos.length - 1) {
            bumpSlide('next');
            setSelectedPhotoIndex((i) => i + 1);
            return;
        }
        if (hasNext && !loadingMore) {
            pendingAdvanceRef.current = 'next';
            loadMorePhotos();
            return;
        }
        if (!hasNext) showToast('마지막 사진입니다');
    }, [hasNext, loadMorePhotos, loadingMore, photos.length, selectedPhotoIndex, showToast]);

    useEffect(() => {
        if (pendingAdvanceRef.current !== 'next') return;
        if (selectedPhotoIndex == null) { pendingAdvanceRef.current = null; return; }
        if (selectedPhotoIndex < photos.length - 1) {
            bumpSlide('next');
            setSelectedPhotoIndex((i) => (i == null ? i : i + 1));
            pendingAdvanceRef.current = null;
        }
        if (!hasNext && selectedPhotoIndex >= photos.length - 1) pendingAdvanceRef.current = null;
    }, [hasNext, photos.length, selectedPhotoIndex]);

    /* ── Keyboard + swipe ──────────────────── */
    useEffect(() => {
        if (selectedPhotoIndex == null) return;
        const onKey = (e) => {
            if (e.key === 'Escape') setSelectedPhotoIndex(null);
            if (e.key === 'ArrowLeft') goPrev();
            if (e.key === 'ArrowRight') goNext();
        };
        const onTouchStart = (e) => { touchStartXRef.current = e.touches[0].clientX; };
        const onTouchEnd = (e) => {
            if (touchStartXRef.current == null) return;
            const dx = e.changedTouches[0].clientX - touchStartXRef.current;
            if (Math.abs(dx) > 50) { dx > 0 ? goPrev() : goNext(); }
            touchStartXRef.current = null;
        };
        window.addEventListener('keydown', onKey);
        window.addEventListener('touchstart', onTouchStart);
        window.addEventListener('touchend', onTouchEnd);
        return () => {
            window.removeEventListener('keydown', onKey);
            window.removeEventListener('touchstart', onTouchStart);
            window.removeEventListener('touchend', onTouchEnd);
        };
    }, [goNext, goPrev, selectedPhotoIndex]);

    /* ── Body scroll lock ──────────────────── */
    useEffect(() => {
        if (selectedPhotoIndex == null) return;
        const prev = document.body.style.overflow;
        document.body.style.overflow = 'hidden';
        return () => { document.body.style.overflow = prev; };
    }, [selectedPhotoIndex]);

    /* ── Thumbnail strip range ─────────────── */
    const [stripStart, stripEnd] = selectedPhotoIndex == null
        ? [0, 0]
        : getStripRange(photos.length, selectedPhotoIndex);

    /* ── Smooth scroll helper ──────────────── */
    const scrollToX = (el, target, duration) => {
        if (!el) return;
        if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || duration <= 0) {
            el.scrollLeft = target; return;
        }
        const start = el.scrollLeft;
        const diff = target - start;
        if (!diff) return;
        let t0 = null;
        const ease = (t) => 0.5 - Math.cos(Math.PI * t) / 2;
        const step = (ts) => {
            if (!t0) t0 = ts;
            const t = Math.min((ts - t0) / duration, 1);
            el.scrollLeft = start + diff * ease(t);
            if (t < 1) requestAnimationFrame(step);
        };
        requestAnimationFrame(step);
    };

    const scrollThumbs = (dir) => {
        const strip = thumbStripRef.current;
        if (!strip) return;
        scrollToX(strip, strip.scrollLeft + (dir === 'next' ? 1 : -1) * strip.clientWidth * 0.6, thumbScrollDuration);
    };

    /* Auto-center active thumb */
    useEffect(() => {
        if (selectedPhotoIndex == null) return;
        const strip = thumbStripRef.current;
        if (!strip) return;
        const thumb = strip.querySelector(`[data-thumb-index="${selectedPhotoIndex}"]`);
        if (!thumb) return;
        const sr = strip.getBoundingClientRect();
        const tr = thumb.getBoundingClientRect();
        scrollToX(strip, tr.left - sr.left + strip.scrollLeft + tr.width / 2 - sr.width / 2, thumbScrollDuration);
    }, [selectedPhotoIndex, stripStart, stripEnd, thumbScrollDuration]);

    /* ── Photo selection ───────────────────── */
    const handleSelectPhoto = (index) => {
        if (selectedPhotoIndex == null) bumpSlide('next');
        else if (index !== selectedPhotoIndex) bumpSlide(index > selectedPhotoIndex ? 'next' : 'prev');
        setSelectedPhotoIndex(index);
    };

    /* ─────────────────────────────────────────────
       Render
    ───────────────────────────────────────────── */
    return (
        <div
            className="travel"
            ref={travelRef}
            style={{ '--region-accent': regionAccent }}
        >
            {/* ═══════════════════════════════════════
                HEADER — editorial masthead
            ═══════════════════════════════════════ */}
            <header className="tv-header" data-reveal>
                <div className="tv-header__masthead">
                    <div className="tv-header__meta">
                        <span className="tv-header__issue">Visual Diary</span>
                        <span className="tv-header__divider" aria-hidden>·</span>
                        <span className="tv-header__tagline">여행 포토 아카이브</span>
                    </div>
                    <h1 className="tv-header__title">
                        <span className="tv-header__title-main">Travel</span>
                        <span className="tv-header__title-italic">Archive</span>
                    </h1>
                    <p className="tv-header__desc">
                        여행에서 포착한 색, 빛, 장면들을 필름처럼 현상합니다.
                        지도에서 지역을 선택하면 해당 앨범이 펼쳐집니다.
                    </p>
                </div>

                {selectedRegion ? (
                    <div className="tv-header__active-region" style={{ '--accent': regionAccent }}>
                        <div className="tv-header__region-indicator" />
                        <div>
                            <p className="tv-header__region-label">Currently viewing</p>
                            <p className="tv-header__region-name">{selectedRegion.name}</p>
                            {photoSummary && (
                                <p className="tv-header__region-count">
                                    {photoSummary.total ?? photos.length} photos
                                </p>
                            )}
                        </div>
                    </div>
                ) : (
                    <div className="tv-header__hint">
                        <div className="tv-header__hint-icon" aria-hidden>
                            <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
                                <circle cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="1" strokeDasharray="3 3"/>
                                <path d="M16 8v8l5 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
                            </svg>
                        </div>
                        <p className="tv-header__hint-text">지도에서 지역을 선택하세요</p>
                    </div>
                )}
            </header>

            {/* ═══════════════════════════════════════
                MAP — world map with region selection
            ═══════════════════════════════════════ */}
            <section
                className={`tv-map-section ${selectedPhotoIndex != null ? 'is-dimmed' : ''}`}
                data-reveal
            >
                <div className="tv-map-wrap">
                    <MapContainer
                        center={[20, 10]}
                        zoom={2}
                        scrollWheelZoom
                        className="tv-map"
                    >
                        <TileLayer
                            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>'
                            url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
                        />
                        <MapLayer
                            geojson={regionsGeojson}
                            selectedRegionId={selectedRegion?.id}
                            onSelectRegion={setSelectedRegion}
                        />
                    </MapContainer>

                    {/* Map overlay hint */}
                    {!selectedRegion && (
                        <div className="tv-map__overlay-hint">
                            <span>CLICK A REGION</span>
                        </div>
                    )}
                </div>

                {/* State messages */}
                {loading && (
                    <div className="tv-state">
                        <div className="tv-state__loader">
                            <span /><span /><span />
                        </div>
                        <p>Developing film…</p>
                    </div>
                )}
                {error && <p className="tv-error">{error}</p>}
                {!loading && !error && selectedRegion && photos.length === 0 && (
                    <p className="tv-state tv-state--empty">
                        이 지역에는 아직 사진이 없습니다.
                    </p>
                )}

                {/* ── Photo Mosaic ─────────────────── */}
                {!loading && !error && selectedRegion && photos.length > 0 && (
                    <>
                        <div className="tv-album-header">
                            <div className="tv-album-header__left">
                                <span
                                    className="tv-album-header__region"
                                    style={{ color: regionAccent }}
                                >
                                    {selectedRegion.name}
                                </span>
                                {photoSummary?.albums?.length > 0 && (
                                    <span className="tv-album-header__albums">
                                        {photoSummary.albums.map((a) => a.album).join('  ·  ')}
                                    </span>
                                )}
                            </div>
                            <span className="tv-album-header__count">
                                {photos.length}
                                {hasNext && '+'}
                            </span>
                        </div>

                        <PhotoMosaic
                            photos={photos}
                            regionLabel={selectedRegion.id}
                            regionAccent={regionAccent}
                            onSelectPhoto={handleSelectPhoto}
                            onLoadMore={loadMorePhotos}
                            hasNext={hasNext}
                            isLoadingMore={loadingMore}
                        />
                    </>
                )}
            </section>

            {/* ═══════════════════════════════════════
                LIGHTBOX — cinematic fullscreen viewer
            ═══════════════════════════════════════ */}
            {selectedPhotoIndex != null && (
                <Lightbox
                    photos={photos}
                    selectedIndex={selectedPhotoIndex}
                    slideToken={slideToken}
                    slideDirection={slideDirection}
                    loadingMore={loadingMore}
                    hasNext={hasNext}
                    photoSummary={photoSummary}
                    selectedRegion={selectedRegion}
                    backdropBlur={backdropBlur}
                    setBackdropBlur={setBackdropBlur}
                    thumbScrollDuration={thumbScrollDuration}
                    setThumbScrollDuration={setThumbScrollDuration}
                    toastMessage={toastMessage}
                    stripStart={stripStart}
                    stripEnd={stripEnd}
                    thumbStripRef={thumbStripRef}
                    onClose={() => setSelectedPhotoIndex(null)}
                    onPrev={goPrev}
                    onNext={goNext}
                    onSelectThumb={setSelectedPhotoIndex}
                    onScrollThumbPrev={() => scrollThumbs('prev')}
                    onScrollThumbNext={() => scrollThumbs('next')}
                />
            )}
        </div>
    );
};

export default Travel;
