// Travels — /travels (replaces /availability). Covers sit history AND
// upcoming openings: horizontal timeline + minimalist map + collapsible
// year sections.
//
// Interaction model
//   - Hover a timeline bar  → that bar grows + map dot for that city pulses
//   - Hover a map dot        → all timeline bars at that city highlight
//   - Click a timeline bar   → open SitPage (same as feed entry)
//   - Year section headers   → expand / collapse (current year open by default)

const { useState: useTvState, useMemo: useTvMemo, useRef: useTvRef, useEffect: useTvEffect } = React;

// Fraunces wght responds to scroll velocity on the main heading only.
// Fast scroll → denser type (up to 500); idle → settles back to 400.
// Drives a --wght CSS custom property via rAF — no React rerenders.
// Respects prefers-reduced-motion by bailing out entirely.
function useVelocityWeight(ref, { min = 400, max = 500, gain = 33, lerpIn = 0.22, decay = 0.06 } = {}) {
  useTvEffect(() => {
    const el = ref.current;
    if (!el) return;
    const mqReduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
    if (mqReduced && mqReduced.matches) return;

    let lastY = window.scrollY;
    let lastT = performance.now();
    let wght = min;
    let target = min;
    let raf = 0;

    const step = () => {
      wght += (target - wght) * lerpIn;
      target += (min - target) * decay;
      el.style.setProperty('--wght', wght.toFixed(1));
      if (Math.abs(target - min) > 0.4 || Math.abs(wght - min) > 0.4) {
        raf = requestAnimationFrame(step);
      } else {
        raf = 0;
        el.style.setProperty('--wght', String(min));
      }
    };
    const onScroll = () => {
      const now = performance.now();
      const dt = Math.max(1, now - lastT);
      const dy = Math.abs(window.scrollY - lastY);
      const v = dy / dt; // px/ms
      target = Math.min(max, min + v * gain);
      lastY = window.scrollY;
      lastT = now;
      if (!raf) raf = requestAnimationFrame(step);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => {
      window.removeEventListener('scroll', onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [ref]);
}

// Region "palette" — stays inside the paper/ink vocabulary. Home region
// (Colorado) reads as the deepest ink; others step back toward warm mid-
// tones. No rainbow — just value + warmth variations on the same axis.
const REGION_COLORS = {
  CO: '#1a1714',  // home — near-black warm ink
  WA: '#2e3136',  // cool mid-ink
  OR: '#252822',  // muted ink-moss
  CA: '#322d24',  // warm mid-ink
  MA: '#0f0e0d',  // deepest ink
  NM: '#35271c',  // warm ink (rust-adjacent but desaturated)
  DE: '#262430',  // cool ink (violet-adjacent but desaturated)
  FR: '#2d251b',  // warm ink (sienna-adjacent but desaturated)
};
const regionOf = (sit) => (sit.location_short || '').split(',').pop().trim();
const colorForSit = (sit) => REGION_COLORS[regionOf(sit)] || '#888';

const THIS_YEAR = new Date().getUTCFullYear();

const Travels = ({ onOpenSit }) => {
  const sits = window.SITS || [];
  const stats = window.SITE_STATS || {};
  const completed = sits.filter(s => s.status !== 'available');
  const open = sits.filter(s => s.status === 'available');

  const [hovered, setHovered] = useTvState(null);         // active sit slug
  const [hoveredCity, setHoveredCity] = useTvState(null); // active city

  // Collapsible year sections — default: current year expanded.
  const byYear = useTvMemo(() => {
    const grouped = {};
    for (const s of [...sits].sort((a, b) => a.start_date < b.start_date ? 1 : -1)) {
      const y = s.start_date.slice(0, 4);
      (grouped[y] ||= []).push(s);
    }
    return grouped;
  }, [sits]);
  const years = Object.keys(byYear).sort().reverse();
  const [expanded, setExpanded] = useTvState(() =>
    Object.fromEntries(years.map(y => [y, parseInt(y, 10) === THIS_YEAR]))
  );
  const toggleYear = (y) => setExpanded(e => ({ ...e, [y]: !e[y] }));

  const statRef = useTvRef(null);
  useVelocityWeight(statRef);

  return (
    <article className="travels">
      <header className="travels__head">
        <span className="mono-label">/history</span>
        <h1 ref={statRef} className="travels__stat">
          {stats.sits} sits · {stats.regions} regions · {stats.years} years on the road
        </h1>
        <p className="travels__sub">
          A timeline of completed sits and the next open windows.
          Hover a bar to see where; click to go there.
        </p>
      </header>

      <TravelsTimeline
        completed={completed}
        open={open}
        hovered={hovered}
        hoveredCity={hoveredCity}
        setHovered={setHovered}
        setHoveredCity={setHoveredCity}
        onOpenSit={onOpenSit}
      />

      {years.map(y => (
        <YearSection
          key={y}
          year={y}
          sits={byYear[y]}
          isOpen={!!expanded[y]}
          onToggle={() => toggleYear(y)}
          onOpenSit={onOpenSit}
          hovered={hovered}
          setHovered={setHovered}
        />
      ))}
    </article>
  );
};

// ---- Hover preview panel ------------------------------------------
// Follows cursor-ish; fixed to the right-center of the viewport. Shows
// a cover thumbnail + one-line meta + review glyph. Inspired by
// microdot.vision's in-page preview pattern.

const SitHoverPreview = ({ slug, onOpenSit, setHovered }) => {
  if (!slug) return null;
  const sit = window.SIT_BY_SLUG && window.SIT_BY_SLUG[slug];
  if (!sit) return null;
  const hasDetails = !!(sit.blurb || (sit.media && sit.media.length > 0));

  const review = window.REVIEW_BY_SLUG && window.REVIEW_BY_SLUG[slug];
  const cover = sit.media && sit.media.find(m => m.feed) || (sit.media && sit.media[0]);
  const plate = (cover && cover.plate) || 'linear-gradient(180deg, #2a2622 0%, #0e0c0b 100%)';
  const isVideo = cover && cover.type === 'video' && cover.src;
  const hasImage = cover && cover.src && !isVideo;

  const d1 = new Date(sit.start_date + 'T00:00:00');
  const d2 = new Date(sit.end_date + 'T00:00:00');
  const days = Math.round((d2 - d1) / 86400000);
  const fmt = (d) => `${d.toLocaleString('en',{month:'short'}).toLowerCase()} ${d.getUTCDate()}`;

  const handleClick = () => {
    if (hasDetails && onOpenSit) onOpenSit(slug);
  };
  return (
    <aside
      className={`sit-preview${hasDetails ? ' is-clickable' : ''}`}
      onMouseEnter={() => setHovered && setHovered(slug)}
      onMouseLeave={() => setHovered && setHovered(null)}
      onClick={handleClick}
      role={hasDetails ? 'button' : undefined}
      tabIndex={hasDetails ? 0 : undefined}
      onKeyDown={(e) => { if (hasDetails && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); handleClick(); }}}
    >
      <div className="sit-preview__media">
        {/* Plate always rendered behind any video — covers the black flash
            before onCanPlay and the failure case if the video never loads. */}
        <div
          className="sit-preview__plate"
          style={{ backgroundImage: (hasImage || isVideo) && cover.plate ? `url(${cover.plate})`
                                   : hasImage ? `url(${cover.src})`
                                   : plate }}
        />
        {isVideo && (
          <video
            src={cover.src}
            muted loop playsInline autoPlay
            className="sit-preview__video"
            preload="metadata"
            disablePictureInPicture
          />
        )}
      </div>
      <div className="sit-preview__body">
        <span className="sit-preview__kicker mono-caps">{sit.location_short}</span>
        <h3 className="sit-preview__title">
          {sit.location.split(',')[0]}
        </h3>
        <p className="sit-preview__meta mono-caps">
          {fmt(d1)} – {fmt(d2)}, {d2.getUTCFullYear()}
          <span className="sep">·</span>
          {days}d
        </p>
        <p className="sit-preview__pets mono-caps">
          {sit.dog}{sit.host && !sit.private ? ` · ${sit.host}` : ''}
        </p>
        {review && (
          <p className="sit-preview__review">
            <span className="sit-preview__glyph" aria-hidden>&#8220;</span>
            {review.quote.length > 140 ? review.quote.slice(0, 137) + '…' : review.quote}
          </p>
        )}
        {!review && sit.blurb && (
          <p className="sit-preview__blurb">{firstSentence(sit.blurb)}</p>
        )}
        <span className="sit-preview__cta mono-caps">
          {hasDetails ? 'click to open →' : 'no details yet'}
        </span>
      </div>
    </aside>
  );
};

// ---- Timeline ------------------------------------------------------
// Full-viewport interactive timeline. Bars are 80vh tall, widths
// proportional to sit duration (no calendar gaps), oldest first.
// Hovering a bar reveals a detail card inside the bar with location,
// dates, dog/host, and a first-sentence blurb. Clicking opens SitPage.

const firstSentence = (text) => {
  if (!text) return '';
  const m = text.match(/^[^.!?]+[.!?]/);
  return m ? m[0].trim() : text.trim();
};

const TravelsTimeline = ({ completed, open, hovered, hoveredCity, setHovered, setHoveredCity, onOpenSit }) => {
  if (!completed.length && !open.length) return null;
  const parse = (iso) => new Date(iso + 'T00:00:00').getTime();
  const daysOf = (s) => Math.max(1, Math.round((parse(s.end_date) - parse(s.start_date)) / 86400000));
  const cityOf = (s) => (s.location || '').split(',')[0].trim();
  const fmtDate = (iso) => {
    const d = new Date(iso + 'T00:00:00');
    return `${d.toLocaleString('en',{month:'short'}).toLowerCase()} ${d.getUTCDate()}`;
  };

  // Oldest first (left), newest on the right, then open windows.
  const chrono = [...completed].sort((a, b) => a.start_date < b.start_date ? -1 : 1);
  const openSorted = [...open].sort((a, b) => a.start_date < b.start_date ? -1 : 1);

  // Year dividers interleaved with sits. Each sit carries its year for
  // bottom-row labels.
  const items = [];
  let lastYear = null;
  for (const s of chrono) {
    const y = parseInt(s.start_date.slice(0, 4), 10);
    if (y !== lastYear) { items.push({ kind: 'year', year: String(y), key: `y-${y}` }); lastYear = y; }
    items.push({ kind: 'sit', sit: s, days: daysOf(s), year: y, key: s.slug });
  }
  if (openSorted.length) {
    items.push({ kind: 'year', year: 'open', key: 'y-open' });
    for (const s of openSorted) items.push({ kind: 'open', sit: s, days: daysOf(s), key: s.slug });
  }

  // Active sit (if hovered) — drives the ambient overlay typography
  // and the section tint. No popup card; the whole timeline responds.
  const activeSit = hovered ? completed.find(s => s.slug === hovered) : null;
  const activeReview = activeSit && window.REVIEW_BY_SLUG
    ? window.REVIEW_BY_SLUG[activeSit.slug]
    : null;
  const activeDays = activeSit
    ? Math.max(1, Math.round((parse(activeSit.end_date) - parse(activeSit.start_date)) / 86400000))
    : 0;
  const activeColor = activeSit ? colorForSit(activeSit) : null;

  return (
    <section
      className={`travels-tl${activeSit ? ' is-active' : ''}`}
      style={activeColor ? { '--tl-active': activeColor } : undefined}
    >
      <div className="travels-tl__track">
        {items.map(it => {
          if (it.kind === 'year') {
            return <span key={it.key} className="travels-tl__year mono-caps">{it.year}</span>;
          }
          if (it.kind === 'sit') {
            const s = it.sit;
            const active = hovered === s.slug || (hoveredCity && cityOf(s) === hoveredCity);
            const dim = !!hovered && !active;
            const hasDetails = !!(s.blurb || (s.media && s.media.length > 0)) || !!(window.REVIEW_BY_SLUG && window.REVIEW_BY_SLUG[s.slug]);
            return (
              <button
                key={it.key}
                className={`travels-tl__bar${active ? ' is-active' : ''}${dim ? ' is-dim' : ''}${hasDetails ? ' has-details' : ''}`}
                style={{ flexGrow: it.days, background: colorForSit(s) }}
                onMouseEnter={() => { setHovered(s.slug); setHoveredCity(cityOf(s)); }}
                onMouseLeave={() => { setHovered(null); setHoveredCity(null); }}
                onClick={() => hasDetails && onOpenSit && onOpenSit(s.slug)}
                aria-label={`${s.location_short}, ${fmtDate(s.start_date)} to ${fmtDate(s.end_date)}`}
              />
            );
          }
          return (
            <span
              key={it.key}
              className={`travels-tl__bar travels-tl__bar--open${hovered ? ' is-dim' : ''}`}
              style={{ flexGrow: it.days }}
              title={`Open · ${it.sit.location_short}`}
              aria-label={`open ${it.sit.start_date} to ${it.sit.end_date}`}
            />
          );
        })}
      </div>

      {/* Ambient overlay — typography appears over the timeline on hover.
          No popup, no card. The whole section is the response surface. */}
      <div className="travels-tl__overlay" aria-hidden={!activeSit}>
        {activeSit && (
          <div className="travels-tl__overlay-content">
            <span className="travels-tl__overlay-kicker mono-caps">{activeSit.location_short}</span>
            <h2 className="travels-tl__overlay-title">
              {activeSit.location.split(',')[0]}
            </h2>
            <p className="travels-tl__overlay-meta mono-caps">
              {fmtDate(activeSit.start_date)} – {fmtDate(activeSit.end_date)}, {activeSit.end_date.slice(0, 4)}
              <span className="sep">·</span>
              {activeDays} day{activeDays === 1 ? '' : 's'}
              <span className="sep">·</span>
              {activeSit.dog}
            </p>
            {activeReview ? (
              <p className="travels-tl__overlay-quote">
                <span className="travels-tl__overlay-glyph" aria-hidden>&#8220;</span>
                {activeReview.quote.length > 180
                  ? activeReview.quote.slice(0, 177) + '…'
                  : activeReview.quote}
                <span className="travels-tl__overlay-attrib mono-caps">
                  — {activeReview.reviewer_first}, {activeReview.reviewer_city}
                </span>
              </p>
            ) : activeSit.blurb ? (
              <p className="travels-tl__overlay-blurb">{firstSentence(activeSit.blurb)}</p>
            ) : null}
          </div>
        )}
      </div>
    </section>
  );
};

// ---- Map -----------------------------------------------------------
// Two-panel map: North America and Western Europe. State + country
// outlines rendered as simplified SVG paths (see data/map_outlines.js)
// behind the city dots. BBoxes MUST match the projection used when the
// outline paths were pre-projected.

const MAP_INSETS = [
  { label: 'North America', bbox: [30, 50, -126, -66], widthPct: 72,
    getOutlineGroups: () => [
      { paths: window.MAP_OUTLINES?.na_neighbors, cls: 'travels-map__neighbor' },
      { paths: window.MAP_OUTLINES?.na_states,    cls: 'travels-map__state'    },
    ] },
  { label: 'Europe',        bbox: [42, 54, -5, 14],  widthPct: 22,
    getOutlineGroups: () => [
      { paths: window.MAP_OUTLINES?.eu_countries, cls: 'travels-map__country' },
    ] },
];

const TravelsMap = ({ completed, hovered, hoveredCity, setHovered, setHoveredCity }) => {
  // Group by city — a dot = a city, sized by sit count.
  const cityMap = new Map();
  for (const s of completed) {
    const city = (s.location || '').split(',')[0].trim();
    const coords = window.CITY_COORDS?.[city];
    if (!coords) continue;
    if (!cityMap.has(city)) {
      cityMap.set(city, { city, coords, sits: [], region: regionOf(s) });
    }
    cityMap.get(city).sits.push(s);
  }
  const cities = Array.from(cityMap.values());

  const insetFor = (lat, lon) => MAP_INSETS.find(
    i => lat >= i.bbox[0] && lat <= i.bbox[1] && lon >= i.bbox[2] && lon <= i.bbox[3]
  );

  return (
    <section className="travels-map" aria-label="map of sit locations">
      <div className="travels-map__grid">
        {MAP_INSETS.map(inset => {
          const [latMin, latMax, lonMin, lonMax] = inset.bbox;
          const dots = cities.filter(c => insetFor(c.coords[0], c.coords[1])?.label === inset.label);
          return (
            <div
              key={inset.label}
              className="travels-map__panel"
              style={{ flexBasis: `${inset.widthPct}%` }}
            >
              <div className="travels-map__panel-frame" aria-hidden>
                <svg viewBox="0 0 100 60" preserveAspectRatio="none" className="travels-map__svg">
                  {/* State / country outlines from map_outlines.js */}
                  {inset.getOutlineGroups && inset.getOutlineGroups().map((group, gi) => (
                    group.paths ? (
                      <g key={gi} className={`travels-map__outlines ${group.cls}-group`}>
                        {Object.entries(group.paths).map(([name, d]) => (
                          <path key={name} d={d} className={group.cls} />
                        ))}
                      </g>
                    ) : null
                  ))}
                  {dots.map(c => {
                    const [lat, lon] = c.coords;
                    const x = ((lon - lonMin) / (lonMax - lonMin)) * 100;
                    const y = ((latMax - lat) / (latMax - latMin)) * 60;
                    const active = (hoveredCity && c.city === hoveredCity) ||
                                   (hovered && c.sits.some(s => s.slug === hovered));
                    const r = 0.7 + Math.min(2.2, Math.log2(c.sits.length + 1));
                    return (
                      <g
                        key={c.city}
                        className={`travels-map__pin${active ? ' is-active' : ''}`}
                        transform={`translate(${x}, ${y})`}
                        onMouseEnter={() => { setHoveredCity(c.city); setHovered(null); }}
                        onMouseLeave={() => { setHoveredCity(null); }}
                      >
                        {active && <circle r={r * 2.2} className="travels-map__pulse" />}
                        <circle
                          r={r}
                          fill={REGION_COLORS[c.region] || '#888'}
                          className="travels-map__dot"
                        />
                      </g>
                    );
                  })}
                </svg>
              </div>
              <div className="travels-map__panel-label mono-caps">
                {inset.label} — {dots.length} {dots.length === 1 ? 'city' : 'cities'}
              </div>
              {/* Show label when hovered in this inset */}
              {hoveredCity && dots.some(d => d.city === hoveredCity) && (
                <div className="travels-map__hover-label">
                  <span className="mono-caps">{hoveredCity.toUpperCase()}</span>
                  <span className="mono-caps">
                    {cityMap.get(hoveredCity).sits.length} sit
                    {cityMap.get(hoveredCity).sits.length === 1 ? '' : 's'}
                  </span>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </section>
  );
};

// ---- Collapsible year section --------------------------------------

const YearSection = ({ year, sits, isOpen, onToggle, onOpenSit, hovered, setHovered }) => {
  const fmt = (iso) => {
    const d = new Date(iso + 'T00:00:00');
    return `${d.toLocaleString('en',{month:'short'}).toLowerCase()} ${d.getUTCDate()}`;
  };
  const filled = sits.filter(s => s.status !== 'available').length;
  const openCount = sits.filter(s => s.status === 'available').length;

  return (
    <section className="travels-year">
      <button
        className={`travels-year__head${isOpen ? ' is-open' : ''}`}
        onClick={onToggle}
        aria-expanded={isOpen}
      >
        <span className="travels-year__chevron" aria-hidden>
          <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
            <path d="M3 2 L7 5 L3 8" />
          </svg>
        </span>
        <span className="travels-year__num">{year}</span>
        <span className="travels-year__count mono-caps">
          {filled} sit{filled === 1 ? '' : 's'}
          {openCount ? ` · ${openCount} open` : ''}
        </span>
        <span className="travels-year__rule" />
      </button>

      <div className={`travels-year__body${isOpen ? ' is-open' : ''}`}>
        <ol className="travels-year__list">
          {sits.map(s => {
            const isAvail = s.status === 'available';
            const d1 = new Date(s.start_date + 'T00:00:00');
            const d2 = new Date(s.end_date + 'T00:00:00');
            const days = Math.round((d2 - d1) / 86400000);
            const isActive = hovered === s.slug;
            const hasReview = !isAvail && window.REVIEW_BY_SLUG && window.REVIEW_BY_SLUG[s.slug];
            return (
              <li
                key={s.slug}
                className={`travels-year__row${isAvail ? ' is-open' : ' is-filled'}${isActive ? ' is-hover' : ''}`}
                onMouseEnter={() => !isAvail && setHovered(s.slug)}
                onMouseLeave={() => setHovered(null)}
                onClick={() => !isAvail && onOpenSit && onOpenSit(s.slug)}
                onKeyDown={(e) => {
                  if (!isAvail && (e.key === 'Enter' || e.key === ' ')) {
                    e.preventDefault();
                    onOpenSit && onOpenSit(s.slug);
                  }
                }}
                role={!isAvail ? 'button' : undefined}
                tabIndex={!isAvail ? 0 : undefined}
                aria-label={!isAvail ? `${s.location_short || s.location}, ${s.start_date} to ${s.end_date}` : undefined}
              >
                <span
                  className="travels-year__bar"
                  aria-hidden
                  style={!isAvail ? { background: colorForSit(s) } : undefined}
                />
                <span className="travels-year__dates mono-caps">
                  {fmt(s.start_date)} – {fmt(s.end_date)}
                </span>
                <span className="travels-year__loc">{s.location_short}</span>
                <span className="travels-year__days mono-caps">{days}d</span>
                <span className="travels-year__tag mono-caps">
                  {isAvail ? 'AVAILABLE' : s.dog}
                </span>
                {hasReview && (
                  <span className="travels-year__quote" title="verified review">&#8220;</span>
                )}
              </li>
            );
          })}
        </ol>
      </div>
    </section>
  );
};

window.Travels = Travels;
