/* global React, ReactDOM */
// ============================================================
//  The Gamekeeper's Cottage — shared site chrome
//  Header, Footer, Photo, Booking modal, Lightbox, calendar.
//  Composes design-system components from the bundle namespace.
// ============================================================
const NS = window.GamekeepersCottageDesignSystem_070bc7;
const { Button, Input, Badge, Rating, Eyebrow, Card } = NS;
const { useState, useEffect, useRef, createContext, useContext, useCallback } = React;
const IMG = window.GK_IMG;

const PHONE = '01767 261407';
const PHONE2 = '07915 615207';
const EMAIL = 'nickbomayne@tiscali.co.uk';

const NAV = [
  ['/', 'The Cottage'],
  ['cottage', 'Cottage Details'],
  ['gallery', 'Gallery'],
  ['explore', 'Explore Suffolk'],
  ['reviews', 'Reviews'],
  ['offers', 'Offers'],
  ['blog', 'Blog'],
  ['contact', 'Contact'],
];
const EXPLORE_SUB = [
  ['explore', 'All things to do'],
  ['explore?filter=' + encodeURIComponent('Food and Drink'), 'Food and Drink'],
  ['explore?filter=' + encodeURIComponent('Attractions'), 'Attractions'],
  ['explore?filter=' + encodeURIComponent('Museums and Galleries'), 'Museums and Galleries'],
  ['explore?filter=' + encodeURIComponent('Tours'), 'Tours'],
  ['explore?filter=' + encodeURIComponent('Theatres'), 'Theatres'],
];

// ---------- Shell context ----------
const ShellCtx = createContext({ openBooking: () => {}, openLightbox: () => {} });
const useShell = () => useContext(ShellCtx);

// ---------- Photo ----------
const PHOTO_BG = {
  wood: 'radial-gradient(120% 100% at 25% 15%, #7d9a6b 0%, #4f7146 42%, #2f5d3a 100%)',
  canopy: 'linear-gradient(160deg, #6f8f5f 0%, #3c6340 60%, #24492d 100%)',
  interior: 'linear-gradient(155deg, #cdbb9c 0%, #a98a68 55%, #5b4636 100%)',
  beach: 'linear-gradient(165deg, #e8ddc6 0%, #c9b58e 50%, #8a6a4f 100%)',
  bird: 'radial-gradient(110% 100% at 70% 20%, #a8b99a 0%, #6f8f5f 55%, #3c6340 100%)',
};
function Photo({ src, alt = '', label, ratio = '4 / 3', position = 'center', radius = 0, variant = 'wood', className = '', style = {}, onClick }) {
  const wrap = {
    position: 'relative', width: '100%', aspectRatio: ratio,
    borderRadius: radius, overflow: 'hidden', background: src ? '#3c6340' : (PHOTO_BG[variant] || PHOTO_BG.wood),
    cursor: onClick ? 'zoom-in' : undefined, ...style,
  };
  const interactive = onClick ? {
    role: 'button', tabIndex: 0, 'aria-label': (label || alt) ? `View photo: ${label || alt}` : 'View photo',
    onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(e); } },
  } : {};
  return (
    <div className={className} style={wrap} onClick={onClick} {...interactive}>
      {src && <img src={src} alt={alt || label || ''} loading="lazy"
        style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: position }} />}
      {label && (
        <span style={src ? {
          position: 'absolute', left: 12, bottom: 11, fontSize: 11, letterSpacing: '0.06em',
          textTransform: 'uppercase', color: 'rgba(255,255,255,0.94)', fontWeight: 600,
          background: 'rgba(43,42,36,0.40)', padding: '4px 10px', borderRadius: 999, backdropFilter: 'blur(2px)',
        } : {
          position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center', padding: 16,
          fontSize: 13, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.92)', fontWeight: 600,
        }}>{label}</span>
      )}
    </div>
  );
}

// ---------- Logo ----------
function Feather({ size = 38 }) {
  return (
    <svg width={size * 0.72} height={size} viewBox="0 0 72 96" aria-hidden="true" style={{ display: 'block', flex: 'none' }}>
      <path d="M36 92 C 35 70, 35 40, 36 16" stroke="#2f5d3a" strokeWidth="2.6" fill="none" strokeLinecap="round" />
      <path d="M36 18 C 52 30, 53 56, 39 74 C 37 64, 36 40, 36 26 Z" fill="#6f8f5f" />
      <path d="M36 18 C 20 30, 19 56, 33 74 C 35 64, 36 40, 36 26 Z" fill="#a8b99a" />
      <g stroke="#24492d" strokeWidth="2.6" strokeLinecap="round">
        <line x1="29" y1="34" x2="43" y2="34" /><line x1="28" y1="44" x2="44" y2="44" /><line x1="30" y1="54" x2="42" y2="54" />
      </g>
    </svg>
  );
}
function Wordmark() {
  return (
    <span className="brand__word"><b>The Gamekeeper's</b><span>COTTAGE</span></span>
  );
}

// Explore categories derived from the places data, so the nav dropdown is dynamic.
function gkUniqueCats(places) { const seen = []; (places || []).forEach(p => { if (p && p.category && seen.indexOf(p.category) === -1) seen.push(p.category); }); return seen; }
let _exCatsCache = null, _exCatsPromise = null;
function loadExploreCategories() {
  if (_exCatsCache) return Promise.resolve(_exCatsCache);
  if (window.GK_ATTRACTIONS && Array.isArray(window.GK_ATTRACTIONS.places)) { _exCatsCache = gkUniqueCats(window.GK_ATTRACTIONS.places); return Promise.resolve(_exCatsCache); }
  if (_exCatsPromise) return _exCatsPromise;
  _exCatsPromise = (async () => {
    try { const r = await fetch('assets/attractions.json', { cache: 'no-store' }); if (r.ok) { const d = await r.json(); if (d && Array.isArray(d.places)) { _exCatsCache = gkUniqueCats(d.places); return _exCatsCache; } } } catch (e) { /* keep fallback */ }
    return null;
  })();
  return _exCatsPromise;
}
function useExploreCategories() {
  const [c, setC] = useState(_exCatsCache);
  useEffect(() => { let live = true; loadExploreCategories().then(x => { if (live && x) setC(x); }); return () => { live = false; }; }, []);
  return c;
}

// ---------- Header ----------
function Header({ page }) {
  const { openBooking } = useShell();
  const [open, setOpen] = useState(false);
  const exCats = useExploreCategories();
  const exItems = (exCats && exCats.length)
    ? [['explore', 'All things to do'], ...exCats.map(c => ['explore?filter=' + encodeURIComponent(c), c])]
    : EXPLORE_SUB;
  return (
    <React.Fragment>
      <header className="site-header">
        <a className="brand" href="/"><Feather size={40} /><Wordmark /></a>
        <nav className="nav">
          {NAV.map(([href, label]) =>
            label === 'Explore Suffolk' ? (
              <span className="nav__group" key={label}>
                <a className={'navlink' + (page === href ? ' is-active' : '')} href={href}>
                  {label} <i data-lucide="chevron-down" style={{ width: 15, height: 15, verticalAlign: '-2px' }}></i>
                </a>
                <span className="nav__menu">
                  {exItems.map(([h, l], i) => <a href={h} key={i}>{l}</a>)}
                </span>
              </span>
            ) : (
              <a key={label} className={'navlink' + (page === href ? ' is-active' : '')} href={href}>{label}</a>
            )
          )}
          <Button variant="primary" size="sm" onClick={openBooking}>Book Now</Button>
        </nav>
        <button className="nav-toggle" aria-label="Menu" onClick={() => setOpen(o => !o)}>
          <i data-lucide={open ? 'x' : 'menu'} style={{ width: 26, height: 26 }}></i>
        </button>
      </header>
      <div className={'mobile-nav' + (open ? ' is-open' : '')}>
        {NAV.map(([href, label]) => <a key={label} href={href}>{label}</a>)}
        <div style={{ marginTop: 20 }}>
          <Button variant="primary" size="lg" fullWidth onClick={() => { setOpen(false); openBooking(); }}>Book Now</Button>
        </div>
      </div>
    </React.Fragment>
  );
}

// ---------- Footer ----------
function Footer() {
  const { openBooking } = useShell();
  const [sent, setSent] = useState(false);
  const [nlEmail, setNlEmail] = useState('');
  const [nlBusy, setNlBusy] = useState(false);
  const [nlErr, setNlErr] = useState('');
  const submitNewsletter = async (e) => {
    e.preventDefault();
    if (nlBusy) return;
    setNlErr(''); setNlBusy(true);
    try { await submitEnquiry({ type: 'newsletter', email: nlEmail }); setSent(true); }
    catch (err) { setNlErr('Sorry — that didn’t go through. Please try again in a moment.'); }
    finally { setNlBusy(false); }
  };
  return (
    <footer className="site-footer">
      <div className="container">
        <div className="footer-grid">
          <div>
            <a className="brand" href="/" aria-label="The Gamekeeper's Cottage — home" style={{ marginBottom: 16 }}><Feather size={44} /></a>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, color: '#fff', fontWeight: 600, marginTop: 14 }}>The Gamekeeper's Cottage</div>
            <p style={{ color: '#a9c2a2', fontSize: 15, marginTop: 10, maxWidth: 280 }}>A cosy self-catering cottage in a hundred-acre Suffolk wood, near Southwold. Looked after by Nick and family.</p>
          </div>
          <div>
            <h4 className="footer-h">Explore</h4>
            <div className="footer-list">
              <a href="cottage">Cottage details</a>
              <a href="gallery">Gallery</a>
              <a href="explore">Things to do</a>
              <a href="reviews">Reviews</a>
              <a href="offers">Offers</a>
              <a href="blog">Blog</a>
            </div>
          </div>
          <div>
            <h4 className="footer-h">Contact us</h4>
            <div className="footer-list">
              <a href={'tel:' + PHONE.replace(/\s/g, '')}><i data-lucide="phone" style={{ width: 15, height: 15, verticalAlign: '-2px', marginRight: 8 }}></i>{PHONE} · {PHONE2}</a>
              <a href={'mailto:' + EMAIL}><i data-lucide="mail" style={{ width: 15, height: 15, verticalAlign: '-2px', marginRight: 8 }}></i>{EMAIL}</a>
              <span style={{ color: '#a9c2a2', display: 'flex', gap: 8 }}><i data-lucide="map-pin" style={{ width: 15, height: 15, marginTop: 3, flex: 'none' }}></i>The Wood, Southwold Rd, Brampton, Beccles NR34 8DW</span>
              <a href="https://www.facebook.com" aria-label="Facebook" style={{ display: 'inline-flex', width: 38, height: 38, borderRadius: 999, background: 'rgba(255,255,255,0.1)', alignItems: 'center', justifyContent: 'center', marginTop: 6, color: '#b9cdb4' }}><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22 12.06C22 6.5 17.52 2 12 2S2 6.5 2 12.06c0 5 3.66 9.15 8.44 9.94v-7.03H7.9v-2.9h2.54V9.85c0-2.51 1.49-3.9 3.78-3.9 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56v1.88h2.78l-.44 2.9h-2.34V22c4.78-.79 8.44-4.94 8.44-9.94Z"/></svg></a>
            </div>
          </div>
          <div>
            <h4 className="footer-h">Plan your stay</h4>
            {sent ? (
              <p style={{ color: '#d7e6d3', fontSize: 15 }}>Thank you — we'll be in touch with seasonal news and availability.</p>
            ) : (
              <form onSubmit={submitNewsletter} style={{ display: 'grid', gap: 10, maxWidth: 320 }}>
                <input className="gk-input" type="email" required placeholder="Your email for our newsletter" value={nlEmail} onChange={(e) => setNlEmail(e.target.value)} />
                <input type="text" name="company" tabIndex={-1} aria-hidden="true" autoComplete="off" style={{ position: 'absolute', left: '-9999px', width: 1, height: 1, opacity: 0 }} />
                {nlErr && <span style={{ color: '#f3c9c0', fontSize: 13 }}>{nlErr}</span>}
                <Button variant="on-dark" size="md" type="submit">{nlBusy ? 'Sending…' : 'Keep me posted'}</Button>
              </form>
            )}
            <div style={{ marginTop: 18 }}>
              <Button variant="primary" size="md" onClick={openBooking}>Check rates &amp; reserve</Button>
            </div>
          </div>
        </div>
      </div>
      <div className="footer-bar" style={{ display: 'flex', gap: 18, justifyContent: 'center', flexWrap: 'wrap' }}>
        <span>© {new Date().getFullYear()} The Gamekeeper's Cottage — all rights reserved</span>
        <a href="privacy" style={{ color: '#eaf2e8' }}>Privacy</a>
        <a href="terms" style={{ color: '#eaf2e8' }}>Terms</a>
        <a href="accessibility" style={{ color: '#eaf2e8' }}>Accessibility</a>
      </div>
    </footer>
  );
}

// ---------- Availability calendar ----------
// Live booked dates come from RentalBell (the same feed as the old site), served
// as JSON from /api/availability.php on the Hostinger host (same domain, no CORS).
// Falls back to the bundled snapshot in assets/availability.json so the calendar
// still shows real data when previewed locally (file://) or if a fetch fails.
const AVAIL_ENDPOINTS = ['api/availability.php', 'assets/availability.json'];
let _availCache = null;      // { booked: Set<string>, validTo: Date|null, updated: string|null }
let _availPromise = null;

function loadAvailability() {
  if (_availCache) return Promise.resolve(_availCache);
  if (_availPromise) return _availPromise;
  const toCache = (data) => ({
    booked: new Set(data.booked),                 // solid / unavailable
    checkin: new Set(data.checkin || []),         // half: free -> booked
    checkout: new Set(data.checkout || []),       // half: booked -> free
    validTo: data.valid_to ? new Date(data.valid_to + 'T23:59:59') : null,
    updated: data.updated || null,
  });
  _availPromise = (async () => {
    // 1. Live / served JSON (works on a real host such as Hostinger).
    for (const url of AVAIL_ENDPOINTS) {
      try {
        const res = await fetch(url, { cache: 'no-store' });
        if (!res.ok) continue;
        const data = await res.json();
        if (data && Array.isArray(data.booked)) {
          _availCache = toCache(data);
          return _availCache;
        }
      } catch (e) { /* try next source */ }
    }
    // 2. Embedded snapshot — works under file:// where fetch() is blocked.
    if (window.GK_AVAILABILITY && Array.isArray(window.GK_AVAILABILITY.booked)) {
      _availCache = toCache(window.GK_AVAILABILITY);
      return _availCache;
    }
    // 3. Nothing available — show everything bookable rather than blocking.
    _availCache = { booked: new Set(), checkin: new Set(), checkout: new Set(), validTo: null, updated: null };
    return _availCache;
  })();
  return _availPromise;
}

function useAvailability() {
  const [avail, setAvail] = useState(_availCache);
  useEffect(() => {
    let live = true;
    loadAvailability().then(a => { if (live) setAvail(a); });
    return () => { live = false; };
  }, []);
  return avail;
}

function isoDate(y, m, d) {
  return y + '-' + String(m + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0');
}

// ---------- Rates ----------
// Read the rate card from assets/rates.json (which Decap CMS edits) on the live
// host; fall back to the embedded window.GK_RATES for local file:// previews.
let _ratesCache = null, _ratesPromise = null;
function loadRates() {
  if (_ratesCache) return Promise.resolve(_ratesCache);
  if (_ratesPromise) return _ratesPromise;
  _ratesPromise = (async () => {
    try {
      const res = await fetch('assets/rates.json', { cache: 'no-store' });
      if (res.ok) {
        const data = await res.json();
        if (data && Array.isArray(data.periods)) { _ratesCache = data; return data; }
      }
    } catch (e) { /* fall through */ }
    if (window.GK_RATES && Array.isArray(window.GK_RATES.periods)) { _ratesCache = window.GK_RATES; return _ratesCache; }
    _ratesCache = { periods: [], shortBreakNightly: null, minNights: 3, currency: '£' };
    return _ratesCache;
  })();
  return _ratesPromise;
}
function useRates() {
  const [rates, setRates] = useState(_ratesCache);
  useEffect(() => { let live = true; loadRates().then(r => { if (live) setRates(r); }); return () => { live = false; }; }, []);
  return rates;
}
function nightsBetween(a, b) { return Math.round((new Date(b) - new Date(a)) / 86400000); }
function findPeriod(iso, periods) { return (periods || []).find(p => iso >= p.start && iso < p.end) || null; }
function money(cur, n) { return (cur || '£') + Number(n).toLocaleString('en-GB'); }

// Send a form (booking enquiry / contact / newsletter) to the server mail handler.
// Resolves on success; throws otherwise so callers can show an error + phone fallback.
// On the local Python preview there's no PHP, so this rejects — expected; use live/staging to test.
async function submitEnquiry(payload) {
  const res = await fetch('api/enquiry.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  let data = null;
  try { data = await res.json(); } catch (e) { /* non-JSON (e.g. PHP not running locally) */ }
  if (!res.ok || !data || !data.ok) {
    throw new Error((data && data.error) || 'send-failed');
  }
  return data;
}

// Guide quote for the selected stay. Always indicative — Nick confirms the exact
// total — and falls back to 'advise' whenever we can't be certain.
function quoteStay(arrive, depart, rates) {
  if (!rates || !arrive || !depart) return { mode: 'none' };
  const cur = rates.currency || '£';
  const nights = nightsBetween(arrive, depart);
  if (nights <= 0) return { mode: 'none' };
  const p = findPeriod(arrive, rates.periods);
  if (!p) return { mode: 'advise', nights };                         // outside known ranges
  if (p.available === false) return { mode: 'unavailable', nights, period: p };
  // Per-period minimum: high-season weeks require a full week (min 7); others use the site minimum.
  const minN = (p.minNights != null) ? p.minNights : (rates.minNights || 3);
  if (nights < minN) return { mode: 'min', nights, minN, period: p };
  if (depart > p.end) return { mode: 'advise', nights, period: p };  // spans a rate change
  if (p.unit === 'stay' && p.price != null) {
    return { mode: 'stay', nights, total: p.price, currency: cur, period: p };
  }
  if (p.price != null) {
    if (nights % 7 === 0) {
      const weeks = nights / 7;
      // Off-season offer: book two consecutive weeks and the second is half price.
      // Only applies to off-season weekly periods, and only when the whole stay
      // sits inside one period (depart <= p.end is already checked above).
      const offerOn = rates.secondWeekHalfPrice && rates.secondWeekHalfPrice.enabled
        && p.season === 'off' && p.unit === 'week';
      if (offerOn && weeks === 2) {
        const total = p.price + Math.round(p.price / 2);
        return { mode: 'week', nights, weeks, total, weekly: p.price, offer: true, currency: cur, period: p };
      }
      return { mode: 'week', nights, weeks, total: p.price * weeks, weekly: p.price, currency: cur, period: p };
    }
    // Nightly short-break rate can vary by period/year (e.g. £130 in 2026, £140 in 2027).
    const shortNightly = (p.shortBreakNightly != null) ? p.shortBreakNightly : rates.shortBreakNightly;
    if (shortNightly) {
      const est = Math.min(nights * shortNightly, p.price);
      return { mode: 'short', nights, nightly: shortNightly, total: est, weekly: p.price, currency: cur, period: p };
    }
    return { mode: 'advise', nights, weekly: p.price, period: p };
  }
  return { mode: 'advise', nights, period: p };
}

// Cross-check the chosen stay against the live availability calendar.
// Returns 'free' | 'taken' | 'unknown' (beyond the data horizon) | null.
function availabilityForStay(arrive, depart, avail) {
  if (!avail || !arrive || !depart) return null;
  const start = new Date(arrive + 'T00:00:00');
  const end = new Date(depart + 'T00:00:00');
  if (isNaN(start) || isNaN(end) || end <= start) return null;
  let taken = false, unknown = false;
  for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
    if (avail.validTo && d > avail.validTo) { unknown = true; continue; }
    const iso = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
    if (avail.booked.has(iso) || (avail.checkin && avail.checkin.has(iso))) taken = true;
  }
  if (taken) return 'taken';
  if (unknown) return 'unknown';
  return 'free';
}

function AvailabilityNote({ status, quote }) {
  if (!status) return null;
  const underMin = quote && quote.mode === 'min';
  const minN = (quote && quote.minN) || 3;
  const map = {
    free:    { icon: 'calendar-check', bg: '#e7f1e3', color: '#2f5d3a', text: 'Good news — your dates look free. Send your enquiry and Nick will confirm and hold them for you.' },
    taken:   { icon: 'calendar-x', bg: '#fbeee0', color: '#8a5a1f', text: 'These dates look already taken — but do send an enquiry: cancellations happen, and Nick can suggest close-by dates.' },
    unknown: { icon: 'calendar', bg: 'var(--surface-soft)', color: 'var(--text-body)', text: "That's a little beyond our published calendar — send an enquiry and Nick will confirm availability." },
  };
  // Dates are free but the stay is under our usual minimum — soften the promise to "hold".
  if (status === 'free' && underMin) {
    map.free = { icon: 'calendar-check', bg: '#e7f1e3', color: '#2f5d3a', text: 'Good news — your dates look free. They’re under our usual ' + minN + '-night minimum, but do send your enquiry: Nick can sometimes take shorter stays close to the date or in quieter seasons.' };
  }
  const m = map[status];
  return (
    <div role="status" style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 13.5, lineHeight: 1.45, borderRadius: 'var(--radius-sm)', padding: '9px 12px', background: m.bg, color: m.color }}>
      <i data-lucide={m.icon} style={{ width: 16, height: 16, flex: '0 0 auto', marginTop: 1 }}></i>
      <span>{m.text}</span>
    </div>
  );
}

function RateGuide({ quote, rates }) {
  const cur = (quote && quote.currency) || (rates && rates.currency) || '£';
  let body;
  if (!quote || quote.mode === 'none') {
    body = <span className="muted">Choose your dates above for a guide price.</span>;
  } else if (quote.mode === 'unavailable') {
    body = <span>Those dates aren't available — please choose others.</span>;
  } else if (quote.mode === 'min') {
    body = <span>Minimum stay is {quote.minN} nights — add a night or two for a guide price.</span>;
  } else if (quote.mode === 'advise') {
    body = (<span><strong style={{ color: 'var(--text-strong)' }}>Owner to advise.</strong> We'll confirm the exact rate for your dates{quote.weekly ? ' (this period is around ' + money(cur, quote.weekly) + '/week)' : ''}.</span>);
  } else {
    const label = quote.mode === 'week' ? (quote.weeks > 1 ? quote.weeks + ' weeks' : 'the week')
      : quote.mode === 'short' ? quote.nights + ' nights'
      : quote.nights + '-night stay';
    body = (<>
      <div style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-strong)', lineHeight: 1.2 }}>
        {money(cur, quote.total)} <span className="muted" style={{ fontSize: 14, fontWeight: 400 }}>guide for {label}</span>
      </div>
      <div className="muted" style={{ fontSize: 12.5, marginTop: 4 }}>
        {quote.offer ? 'Second week half price applied. ' : ''}
        {quote.mode === 'short' ? 'Short break at ' + money(cur, quote.nightly) + '/night. ' : ''}
        Indicative only — Nick confirms the exact total. No payment now.
      </div>
    </>);
  }
  return <div style={{ background: 'var(--surface-soft)', borderRadius: 'var(--radius-sm)', padding: '12px 14px', fontSize: 14 }}>{body}</div>;
}

// Rate-card visibility: drop periods that have fully ended, and any period whose
// every night is already booked (it reappears automatically once a date frees up,
// since availability comes from the same live RentalBell feed as the calendar).
function periodHasEnded(p, todayISO) { return !!p.end && p.end <= todayISO; }
function periodFullyBooked(p, avail) {
  if (!avail || !avail.booked || !p.start || !p.end) return false;
  const end = new Date(p.end + 'T00:00:00');
  const d = new Date(p.start + 'T00:00:00');
  if (isNaN(end) || isNaN(d) || end <= d) return false;
  for (; d < end; d.setDate(d.getDate() + 1)) {
    if (avail.validTo && d > avail.validTo) return false;   // beyond known data — keep the row
    const iso = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
    const taken = avail.booked.has(iso) || (avail.checkin && avail.checkin.has(iso));
    if (!taken) return false;                                // a free night — keep the row
  }
  return true;                                               // every night booked — hide the row
}
function visibleRatePeriods(rates, avail) {
  if (!rates || !rates.periods) return [];
  const t = new Date();
  const todayISO = t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0') + '-' + String(t.getDate()).padStart(2, '0');
  return rates.periods.filter(p => !periodHasEnded(p, todayISO) && !periodFullyBooked(p, avail));
}

function RateCard({ rates }) {
  const avail = useAvailability();
  if (!rates || !rates.periods || !rates.periods.length) return null;
  const cur = rates.currency || '£';
  const periods = visibleRatePeriods(rates, avail);
  if (!periods.length) return null;
  return (
    <details className="ratecard" style={{ marginTop: 16, fontSize: 14 }}>
      <summary style={{ cursor: 'pointer', fontWeight: 600, color: 'var(--text-strong)' }}>See the full rate card</summary>
      {rates.intro ? <p className="muted" style={{ fontSize: 12.5, margin: '8px 0 10px' }}>{rates.intro}</p> : null}
      <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
        {periods.map((p, i) => (
          <li key={i} style={{ display: 'flex', justifyContent: 'space-between', gap: 12, padding: '5px 0', borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
            <span>{p.label}</span>
            <span style={{ fontWeight: 600, whiteSpace: 'nowrap' }}>
              {p.available === false || p.price == null ? 'On request' : money(cur, p.price) + (p.unit === 'stay' ? '' : '/week')}
            </span>
          </li>
        ))}
      </ul>
    </details>
  );
}

function MiniCalendar({ months = 1, onPick, focus, selStart, selEnd }) {
  const today = new Date();
  const avail = useAvailability();
  // Open on the month of `focus` (e.g. a date carried in from a booking bar), else today.
  const startMonth = (() => {
    if (focus && /^\d{4}-\d{2}-\d{2}$/.test(focus)) {
      const d = new Date(focus + 'T00:00:00');
      if (!isNaN(d)) return { y: d.getFullYear(), m: d.getMonth() };
    }
    return { y: today.getFullYear(), m: today.getMonth() };
  })();
  const [cursor, setCursor] = useState(startMonth);
  // Jump to the focused date's month when it changes (e.g. the date inputs are edited).
  // Manual prev/next navigation doesn't change `focus`, so it's preserved.
  useEffect(() => {
    if (focus && /^\d{4}-\d{2}-\d{2}$/.test(focus)) {
      const d = new Date(focus + 'T00:00:00');
      if (!isNaN(d)) setCursor({ y: d.getFullYear(), m: d.getMonth() });
    }
  }, [focus]);
  const monthName = (y, m) => new Date(y, m, 1).toLocaleString('en-GB', { month: 'long', year: 'numeric' });
  const renderMonth = (y, m) => {
    const first = new Date(y, m, 1).getDay(); // 0 Sun
    const start = (first + 6) % 7; // make Monday-first
    const days = new Date(y, m + 1, 0).getDate();
    const cells = [];
    for (let i = 0; i < start; i++) cells.push(<span key={'e' + i} className="cal__cell cal__cell--out"></span>);
    for (let d = 1; d <= days; d++) {
      const iso = isoDate(y, m, d);
      const past = new Date(y, m, d) < new Date(today.getFullYear(), today.getMonth(), today.getDate());
      // Mirror RentalBell: event-day = solid booked; check-in / checkout = half
      // (turnover) days that can still start or end a stay, so they stay selectable.
      // Dates beyond the data horizon are treated as available — Nick confirms by enquiry.
      const booked   = !!avail && avail.booked.has(iso);
      const checkin  = !!avail && avail.checkin.has(iso);
      const checkout = !!avail && avail.checkout.has(iso);
      const unavailable = past || booked;            // not selectable (incl. same-day turnovers, which are 'booked')
      const mod = unavailable ? 'cal__cell--busy'
        : checkin ? 'cal__cell--checkin'
        : checkout ? 'cal__cell--checkout'
        : 'cal__cell--free';
      const isEnd = (selStart && iso === selStart) || (selEnd && iso === selEnd);
      const inRange = selStart && selEnd && iso > selStart && iso < selEnd;
      const selStyle = isEnd ? { outline: '2px solid var(--color-primary)', outlineOffset: '-2px' }
        : inRange ? { boxShadow: 'inset 0 0 0 2px rgba(47,93,58,0.28)' } : {};
      cells.push(
        <span key={d}
          className={'cal__cell ' + mod}
          style={{ cursor: unavailable ? 'default' : 'pointer', ...selStyle }}
          onClick={() => !unavailable && onPick && onPick(iso)}>{d}</span>
      );
    }
    return (
      <div key={y + '-' + m}>
        <div className="cal__title" style={{ textAlign: 'center', marginBottom: 8 }}>{monthName(y, m)}</div>
        <div className="cal__grid">
          {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((d, i) => <span key={i} className="cal__dow">{d}</span>)}
          {cells}
        </div>
      </div>
    );
  };
  const shift = (n) => setCursor(c => { let m = c.m + n, y = c.y; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } return { y, m }; });
  const list = [];
  for (let i = 0; i < months; i++) { let m = cursor.m + i, y = cursor.y; while (m > 11) { m -= 12; y++; } list.push([y, m]); }
  return (
    <div>
      <div className="cal__head">
        <button className="cal__nav" onClick={() => shift(-1)} aria-label="Previous month"><i data-lucide="chevron-left" style={{ width: 16, height: 16 }}></i></button>
        <span className="cal__title">Availability</span>
        <button className="cal__nav" onClick={() => shift(1)} aria-label="Next month"><i data-lucide="chevron-right" style={{ width: 16, height: 16 }}></i></button>
      </div>
      <div className="cal__months" style={{ display: 'grid', gridTemplateColumns: `repeat(${months}, auto)`, gap: 28, justifyContent: 'center' }}>
        {list.map(([y, m]) => renderMonth(y, m))}
      </div>
      <div className="cal__legend">
        <span><span className="cal__dot" style={{ background: 'var(--accent-fern)' }}></span>Available</span>
        <span><span className="cal__dot" style={{ background: '#f3e3df' }}></span>Booked</span>
        <span><span className="cal__dot" style={{ background: 'linear-gradient(135deg, #eec7bb 0 50%, var(--accent-fern) 50% 100%)' }}></span>Changeover day</span>
      </div>
    </div>
  );
}

// ---------- Booking modal ----------
function BookingModal({ onClose, prefill }) {
  const [sent, setSent] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [sendError, setSendError] = useState('');
  const [form, setForm] = useState({
    arrive: (prefill && prefill.arrive) || '',
    depart: (prefill && prefill.depart) || '',
    guests: (prefill && prefill.guests) || '2',
    name: '', email: '', phone: '', msg: '', company: '',
  });
  const set = (k) => (e) => setForm(f => ({ ...f, [k]: e.target.value }));
  const [focusField, setFocusField] = useState('arrive');
  // Changing arrival drags a now-earlier (or empty) departure with it, so the
  // departure picker doesn't open months in the past. Changing departure leaves arrival.
  const onArrive = (e) => {
    const v = e.target.value;
    setFocusField('arrive');
    setForm(f => { const next = { ...f, arrive: v }; if (v && (!f.depart || f.depart < v)) next.depart = v; return next; });
  };
  const onDepart = (e) => { setFocusField('depart'); setForm(f => ({ ...f, depart: e.target.value })); };
  const calFocus = form[focusField] || form.arrive || form.depart;
  const dateError = (form.arrive && form.depart && form.depart < form.arrive)
    ? 'Your departure date is before your arrival date — please pick a later departure.' : '';
  const rates = useRates();
  const quote = quoteStay(form.arrive, form.depart, rates);
  const avail = useAvailability();
  const stayAvail = dateError ? null : availabilityForStay(form.arrive, form.depart, avail);
  // High-season full-week guidance: if the arrival falls in a high-season period
  // and the stay isn't a clean Saturday-to-Saturday week, gently flag Nick's
  // usual Sat–Sat full-week pattern — without blocking the enquiry.
  const arrPeriod = (rates && form.arrive && !dateError) ? findPeriod(form.arrive, rates.periods) : null;
  const stayNights = (form.arrive && form.depart && !dateError) ? nightsBetween(form.arrive, form.depart) : 0;
  const arrDow = form.arrive ? new Date(form.arrive + 'T00:00:00').getDay() : null; // 6 = Saturday
  const isSatToSatWeek = arrDow === 6 && stayNights > 0 && stayNights % 7 === 0;
  const showSatNote = !!arrPeriod && arrPeriod.season === 'high' && stayNights > 0 && !isSatToSatWeek;
  const onSubmitEnquiry = async (e) => {
    e.preventDefault();
    if (dateError || submitting) return;
    setSendError(''); setSubmitting(true);
    try {
      await submitEnquiry({
        type: 'booking',
        name: form.name, email: form.email, phone: form.phone, guests: form.guests,
        arrive: form.arrive, depart: form.depart,
        guide: (quote && quote.total != null) ? money(quote.currency || (rates && rates.currency) || '£', quote.total) : '',
        message: form.msg, company: form.company,
      });
      setSent(true);
    } catch (err) {
      setSendError('Sorry — we couldn’t send that just now. Please call Nick on ' + PHONE + ', or email ' + EMAIL + '.');
    } finally { setSubmitting(false); }
  };
  const dialogRef = useRef(null);
  useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [stayAvail, dateError, showSatNote, quote.mode]);
  useEffect(() => {
    const prev = document.activeElement;
    if (dialogRef.current) dialogRef.current.focus();
    const onKey = (e) => e.key === 'Escape' && onClose();
    window.addEventListener('keydown', onKey);
    return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); };
  }, [onClose]);
  return (
    <div className="overlay" onClick={onClose}>
      <div className="dialog" ref={dialogRef} tabIndex={-1} role="dialog" aria-modal="true" aria-label="Check rates and reserve your stay" style={{ position: 'relative', maxWidth: 880, outline: 'none' }} onClick={(e) => e.stopPropagation()}>
        <button className="dialog__close" onClick={onClose} aria-label="Close"><i data-lucide="x" style={{ width: 20, height: 20 }}></i></button>
        {sent ? (
          <div style={{ padding: '56px 48px', textAlign: 'center' }}>
            <div style={{ width: 64, height: 64, borderRadius: 999, background: 'var(--accent-fern)', color: 'var(--color-primary)', display: 'grid', placeItems: 'center', margin: '0 auto 20px' }}>
              <i data-lucide="check" style={{ width: 30, height: 30 }}></i>
            </div>
            <h2 className="h2" style={{ marginBottom: 12 }}>Enquiry sent</h2>
            <p className="lead muted" style={{ maxWidth: 460, margin: '0 auto 24px' }}>Thank you, {form.name || 'and'} — Nick will confirm availability and the total for your dates, usually within a day. No deposit is taken yet.</p>
            <Button variant="primary" size="lg" onClick={onClose}>Lovely, thanks</Button>
          </div>
        ) : (
          <div className="gk-stack" style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', minHeight: 0 }}>
            <div style={{ background: 'var(--surface-soft)', padding: '36px 34px' }}>
              <Eyebrow>Check rates &amp; reserve</Eyebrow>
              <h2 className="h2" style={{ margin: '12px 0 8px' }}>Tell us your dates</h2>
              <p className="muted" style={{ fontSize: 15, margin: '0 0 22px' }}>An enquiry, not a payment — we'll reply with the exact price and hold the dates for you.</p>
              <MiniCalendar months={1} focus={calFocus} selStart={form.arrive} selEnd={form.depart} onPick={(iso) => {
                if (!form.arrive || (form.arrive && form.depart)) { setForm(f => ({ ...f, arrive: iso, depart: '' })); setFocusField('arrive'); }
                else if (iso > form.arrive) { setForm(f => ({ ...f, depart: iso })); setFocusField('depart'); }
                else { setForm(f => ({ ...f, arrive: iso })); setFocusField('arrive'); }
              }} />
              <div style={{ marginTop: 18, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                <Badge tone="moss">Sleeps 5</Badge>
                <Badge tone="sage">{rates && rates.shortBreakNightly ? '3 nights from ' + money(rates.currency || '£', rates.shortBreakNightly) + '/night' : '3 nights from £130/night'}</Badge>
              </div>
              <RateCard rates={rates} />
            </div>
            <form style={{ padding: '36px 34px', display: 'grid', gap: 14, alignContent: 'start' }} onSubmit={onSubmitEnquiry}>
              <input type="text" name="company" value={form.company} onChange={set('company')} autoComplete="off" tabIndex={-1} aria-hidden="true" style={{ position: 'absolute', left: '-9999px', width: 1, height: 1, opacity: 0 }} />
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
                <Input label="Arrival" type="date" value={form.arrive} onChange={onArrive} required />
                <Input label="Departure" type="date" value={form.depart} onChange={onDepart} required />
              </div>
              {dateError && <div role="alert" style={{ background: '#fbeae6', color: '#9f3d2e', borderRadius: 'var(--radius-sm)', padding: '9px 12px', fontSize: 13.5 }}>{dateError}</div>}
              <AvailabilityNote status={stayAvail} quote={quote} />
              <RateGuide quote={quote} rates={rates} />
              {showSatNote && (
                <div role="note" style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 13.5, lineHeight: 1.45, borderRadius: 'var(--radius-sm)', padding: '9px 12px', background: '#fbf3e3', color: '#7a5a1f' }}>
                  <i data-lucide="info" style={{ width: 16, height: 16, flex: '0 0 auto', marginTop: 1 }}></i>
                  <span>In our busy season we normally book full weeks, Saturday to Saturday. Do send your enquiry — Nick will confirm what's possible for your dates.</span>
                </div>
              )}
              <Input label="Guests" type="number" min="1" max="5" value={form.guests} onChange={set('guests')} hint="Sleeps up to 5 (+ cot on request)" />
              <Input label="Your name" value={form.name} onChange={set('name')} required />
              <Input label="Email" type="email" value={form.email} onChange={set('email')} required />
              <Input label="Phone (optional)" value={form.phone} onChange={set('phone')} />
              {sendError && <div role="alert" style={{ background: '#fbeae6', color: '#9f3d2e', borderRadius: 'var(--radius-sm)', padding: '9px 12px', fontSize: 13.5 }}>{sendError}</div>}
              <Button variant="primary" size="lg" type="submit" fullWidth disabled={!!dateError || submitting}>{submitting ? 'Sending…' : 'Send enquiry'}</Button>
              <p className="muted" style={{ fontSize: 13, textAlign: 'center', margin: 0 }}>Prefer to talk? Call Nick on {PHONE}</p>
            </form>
          </div>
        )}
      </div>
    </div>
  );
}

// ---------- Lightbox ----------
function Lightbox({ items, index, onClose, onIndex }) {
  const boxRef = useRef(null);
  useEffect(() => {
    const prev = document.activeElement;
    if (boxRef.current) boxRef.current.focus();
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight') onIndex((index + 1) % items.length);
      if (e.key === 'ArrowLeft') onIndex((index - 1 + items.length) % items.length);
    };
    window.addEventListener('keydown', onKey);
    return () => { window.removeEventListener('keydown', onKey); if (prev && prev.focus) prev.focus(); };
  }, [index, items, onClose, onIndex]);
  const cur = items[index];
  return (
    <div className="lightbox" ref={boxRef} tabIndex={-1} role="dialog" aria-modal="true" aria-label="Photo viewer" style={{ outline: 'none' }} onClick={onClose}>
      <button className="lightbox__close" onClick={onClose} aria-label="Close"><i data-lucide="x" style={{ width: 30, height: 30 }}></i></button>
      <div className="lightbox__stage" onClick={(e) => e.stopPropagation()}>
        <img src={cur.src} alt={cur.label || ''} />
      </div>
      <div className="lightbox__bar" onClick={(e) => e.stopPropagation()}>
        <button className="lightbox__btn" onClick={() => onIndex((index - 1 + items.length) % items.length)} aria-label="Previous"><i data-lucide="arrow-left" style={{ width: 22, height: 22 }}></i></button>
        <span style={{ minWidth: 220, textAlign: 'center', fontSize: 15 }}>{cur.label} · {index + 1} / {items.length}</span>
        <button className="lightbox__btn" onClick={() => onIndex((index + 1) % items.length)} aria-label="Next"><i data-lucide="arrow-right" style={{ width: 22, height: 22 }}></i></button>
      </div>
    </div>
  );
}

// ---------- Shell ----------
function SiteShell({ page, children }) {
  const [booking, setBooking] = useState(false);
  const [prefill, setPrefill] = useState(null); // { arrive, depart, guests } carried in from a booking bar
  const [lb, setLb] = useState(null); // { items, index }
  const openBooking = useCallback((data) => { setPrefill(data && typeof data === 'object' && data.nativeEvent === undefined ? data : null); setBooking(true); }, []);
  const openLightbox = useCallback((items, index = 0) => setLb({ items, index }), []);
  useEffect(() => {
    if (window.lucide) window.lucide.createIcons();
    // a11y: the DS Rating stars carry aria-label on a roleless span — give it a role
    document.querySelectorAll('.gk-rating__stars[aria-label]:not([role])').forEach(e => e.setAttribute('role', 'img'));
  });
  useEffect(() => {
    document.body.style.overflow = (booking || lb) ? 'hidden' : '';
  }, [booking, lb]);
  // scroll reveal
  useEffect(() => {
    const els = document.querySelectorAll('.reveal');
    const io = new IntersectionObserver((ents) => ents.forEach(en => { if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); } }), { threshold: 0.12 });
    els.forEach(el => io.observe(el)); return () => io.disconnect();
  });
  return (
    <ShellCtx.Provider value={{ openBooking, openLightbox }}>
      <a className="skip-link" href="#main">Skip to content</a>
      <Header page={page} />
      <main id="main">{children}</main>
      <Footer />
      {booking && <BookingModal prefill={prefill} onClose={() => setBooking(false)} />}
      {lb && <Lightbox items={lb.items} index={lb.index} onClose={() => setLb(null)} onIndex={(i) => setLb(s => ({ ...s, index: i }))} />}
    </ShellCtx.Provider>
  );
}

function mount(page, Content) {
  const root = document.getElementById('root');
  ReactDOM.createRoot(root).render(<SiteShell page={page}><Content /></SiteShell>);
}

// ---------- Guest reviews ----------
// Approved reviews live in assets/reviews.json (managed by Decap; the Approve email
// link appends to it via GitHub). Fall back to embedded window.GK_REVIEWS for
// local file:// previews.
let _reviewsCache = null, _reviewsPromise = null;
function loadReviews() {
  if (_reviewsCache) return Promise.resolve(_reviewsCache);
  if (_reviewsPromise) return _reviewsPromise;
  _reviewsPromise = (async () => {
    try {
      const res = await fetch('assets/reviews.json', { cache: 'no-store' });
      if (res.ok) { const d = await res.json(); if (d && Array.isArray(d.reviews)) { _reviewsCache = d.reviews; return _reviewsCache; } }
    } catch (e) { /* fall through to embedded snapshot */ }
    if (window.GK_REVIEWS && Array.isArray(window.GK_REVIEWS.reviews)) { _reviewsCache = window.GK_REVIEWS.reviews; return _reviewsCache; }
    _reviewsCache = [];
    return _reviewsCache;
  })();
  return _reviewsPromise;
}
function useApprovedReviews() {
  const [reviews, setReviews] = useState(_reviewsCache || []);
  useEffect(() => { let live = true; loadReviews().then(r => { if (live) setReviews(r); }); return () => { live = false; }; }, []);
  return reviews;
}
function reviewMonth(iso) {
  if (!iso) return '';
  const d = new Date(iso + 'T00:00:00');
  return isNaN(d) ? '' : d.toLocaleString('en-GB', { month: 'long', year: 'numeric' });
}
// Live count + average from the reviews currently on the site.
function reviewStats(reviews) {
  const list = Array.isArray(reviews) ? reviews : [];
  const count = list.length;
  const rated = list.filter((r) => r && Number(r.rating) > 0);
  const average = rated.length ? rated.reduce((s, r) => s + Number(r.rating), 0) / rated.length : 0;
  return { count, average, averageText: average ? average.toFixed(1) : '—' };
}

Object.assign(window, {
  GKPhoto: Photo, GKHeader: Header, GKFooter: Footer, GKFeather: Feather,
  GKMiniCalendar: MiniCalendar, GKSiteShell: SiteShell, GKuseShell: useShell, GKmount: mount,
  GKContact: { PHONE, PHONE2, EMAIL },
  GKuseReviews: useApprovedReviews, GKreviewMonth: reviewMonth, GKreviewStats: reviewStats,
  GKuseRates: useRates, GKuseAvailability: useAvailability, GKquoteStay: quoteStay,
  GKstayAvailability: availabilityForStay, GKRateGuide: RateGuide, GKAvailabilityNote: AvailabilityNote,
  GKvisibleRatePeriods: visibleRatePeriods, GKsubmitEnquiry: submitEnquiry,
});
