// Pathway — Bookings Activity page
// On-brand NovaStory aesthetic — reuses .pw-* tokens
const { useState: useStB, useMemo: useMeB, useEffect: useEfB, useRef: useRfB } = React;

// ──────────────────────────────────────────────────────────────
// Mock data — 50 bookings across the Demo Restaurant Group
// ──────────────────────────────────────────────────────────────
const B_SITES = [
  { id: 'kx',  name: "Hoppers — King's Cross" },
  { id: 'soho',name: 'Hoppers — Soho' },
  { id: 'shor',name: 'Hoppers — Shoreditch' },
  { id: 'may', name: 'Ambassadors — Mayfair' },
  { id: 'mar', name: 'Trishna — Marylebone' },
];
// Reuse the brand channel palette (map to Pathway's existing channels)
const B_CHANNELS = [
  { id: 'google',  name: 'Google Ads',    color: 'var(--pw-chart-2)', brandId: 'paid_search' },
  { id: 'meta',    name: 'Meta',          color: 'var(--pw-chart-1)', brandId: 'paid_social' },
  { id: 'email',   name: 'Email',         color: 'var(--pw-chart-3)', brandId: 'email' },
  { id: 'organic', name: 'Organic',       color: 'var(--pw-chart-4)', brandId: 'organic_search' },
  { id: 'direct',  name: 'Direct',        color: 'var(--pw-chart-5)', brandId: 'direct' },
  { id: 'loyalty', name: 'Loyalty',       color: 'var(--pw-chart-6)', brandId: 'email' },
];
const B_PLATFORMS = ['SevenRooms', 'OpenTable', 'ResDiary', 'Collins'];
const B_CAMPAIGNS = {
  google:  ['Brand — Core Search', 'Generic — Near Me', 'PMax — Group'],
  meta:    ['Lookalike — Diners', 'Interest — Foodies', 'Retargeting'],
  email:   ['Weekly Pick', 'Reactivation 90d', 'Loyalty — Members'],
  organic: ['—'],
  direct:  ['—'],
  loyalty: ['Gold Tier', 'Birthday Reward'],
};
const B_KEYWORDS = {
  google:  ['hoppers shoreditch', 'sri lankan london', 'best curry soho', 'restaurants near liverpool st', 'trishna marylebone'],
  meta:    [null],
  email:   [null],
  organic: ['hoppers restaurant', 'ambassadors clubhouse mayfair', 'sri lankan food london', 'trishna review'],
  direct:  [null],
  loyalty: [null],
};
const B_LANDING = {
  google:  ['/hoppers-shoreditch', '/reservations', '/menu', '/private-dining'],
  meta:    ['/weekend-covers', '/new-menu', '/reservations'],
  email:   ['/offers/loyalty-pick', '/reactivate'],
  organic: ['/', '/locations/mayfair', '/menu'],
  direct:  ['/'],
  loyalty: ['/loyalty'],
};
const B_REFERRERS = {
  google:  ['google.com/search', 'google.com/maps'],
  meta:    ['instagram.com', 'facebook.com', 'l.instagram.com'],
  email:   ['mailchi.mp', 'email.pathway.app'],
  organic: ['google.com', 'bing.com', 'duckduckgo.com'],
  direct:  [null],
  loyalty: ['loyalty.demo-group.com'],
};
const B_DEVICES = ['iPhone 15 · iOS 17.4', 'Pixel 8 · Android 14', 'MacBook Pro · Safari 17', 'Windows · Chrome 122'];
const B_FIRST_NAMES = ['Olivia','Finn','Amara','Sanjay','Marcus','Priya','Hamish','Léa','Theo','Sofia','Jonas','Nadia','Arjun','Isla','Idris','Juno','Ravi','Mei','Callum','Zara','Hugo','Noor','Leo','Anya','Freddie','Kwame','Mia','Ezra','Bea','Ivan','Yuki','Sam','Elena','Oscar','Maya','Jamal','Cara','Luca','Saskia','Tobi','Ines','Rowan','Devi','Asha','Nia','Omari','Pia','Hari','Quinn','Vera'];
const B_LAST_NAMES  = ['Holloway','McNair','Okafor','Patel','Reyes','Singh','Campbell','Laurent','Winters','Romano','Brekke','Haidari','Shah','MacLeod','Ahmed','Sato','Iyer','Chen','Walsh','Ford','Garcia','Khan','Mendoza','Novak','Obi','Kovač','Abadi','Tanaka','Wyn','Pham','Shaw','Foster','Brooks','Okeke','Holt','Rahman','Vega','Dupont','Bjørn','Osei','Bennett','Chopra','Ellis','Greene','Haile','Iqbal','Jenkins','Klein','Laing','Moreno'];
const B_TAGS = ['VIP', 'First visit', 'Repeat guest', 'Birthday', 'Private dining', 'Anniversary', 'Press', 'High spender', 'Allergy noted', 'Walk-in converted'];

function bRand(seed) { let x = seed; return () => { x = (x * 9301 + 49297) % 233280; return x / 233280; }; }

function buildBookings() {
  const out = [];
  const rnd = bRand(42);
  const now = new Date('2026-04-20T12:00:00Z');
  for (let i = 0; i < 50; i++) {
    const site = B_SITES[Math.floor(rnd() * B_SITES.length)];
    const ch = B_CHANNELS[Math.floor(rnd() * B_CHANNELS.length)];
    const camp = B_CAMPAIGNS[ch.id][Math.floor(rnd() * B_CAMPAIGNS[ch.id].length)];
    const kw = B_KEYWORDS[ch.id][Math.floor(rnd() * B_KEYWORDS[ch.id].length)];
    const land = B_LANDING[ch.id][Math.floor(rnd() * B_LANDING[ch.id].length)];
    const ref = B_REFERRERS[ch.id][Math.floor(rnd() * B_REFERRERS[ch.id].length)];
    const device = B_DEVICES[Math.floor(rnd() * B_DEVICES.length)];
    const covers = [2,2,2,3,3,4,4,5,6,8][Math.floor(rnd() * 10)];
    const dayOffset = Math.floor(rnd() * 28);
    const hourOffset = 12 + Math.floor(rnd() * 10);
    const minOffset = Math.floor(rnd() * 4) * 15;
    const resDate = new Date(now.getTime() - (Math.floor(rnd()*2) ? dayOffset : -dayOffset) * 86400000);
    resDate.setHours(hourOffset, minOffset, 0, 0);
    const baseSpend = { google: 22, meta: 18, email: 1.2, organic: 0, direct: 0, loyalty: 0.8 }[ch.id];
    const adSpend = ch.id === 'organic' || ch.id === 'direct' ? 0 : (baseSpend + rnd() * baseSpend * 0.8);
    const cpc = covers > 0 ? adSpend / covers : 0;
    const attrRev = covers * (58 + rnd() * 42);
    const platform = B_PLATFORMS[Math.floor(rnd() * B_PLATFORMS.length)];
    // Conversion status — where the guest ended up in the funnel.
    //   'reserved'  : booking in the future, outcome still pending
    //   'no_show'   : past booking, never sat down (no POS match)
    //   'cancelled' : guest cancelled before arrival
    //   'seated'    : arrived & seated, POS transaction matched (paying customer)
    // Only 'seated' bookings contribute revenue — the rest are £0.
    const isFuture = resDate.getTime() > now.getTime();
    let conversionStatus;
    if (isFuture) {
      conversionStatus = 'reserved';
    } else {
      const roll = rnd();
      if (roll < 0.68)      conversionStatus = 'seated';      // ~68% sit & pay
      else if (roll < 0.88) conversionStatus = 'no_show';     // ~20% no-show
      else                  conversionStatus = 'cancelled';   // ~12% cancelled
    }
    const posVerified = conversionStatus === 'seated';
    const tagCount = Math.floor(rnd() * 3);
    const tags = [];
    for (let t = 0; t < tagCount; t++) {
      const tag = B_TAGS[Math.floor(rnd() * B_TAGS.length)];
      if (!tags.includes(tag)) tags.push(tag);
    }
    const first = B_FIRST_NAMES[Math.floor(rnd() * B_FIRST_NAMES.length)];
    const last = B_LAST_NAMES[Math.floor(rnd() * B_LAST_NAMES.length)];
    const ref_code = 'PW-' + ((1000 + Math.floor(rnd() * 8999)).toString());

    const sessions = 1 + Math.floor(rnd() * 3);
    const firstClickDate = new Date(resDate.getTime() - (1 + Math.floor(rnd() * 6)) * 86400000);
    firstClickDate.setHours(Math.floor(rnd() * 22), Math.floor(rnd() * 60));
    const events = [];
    events.push({
      type: 'first_click', ts: firstClickDate.toISOString(),
      channel: ch.name, campaign: camp, keyword: kw, referrer: ref, device, landingPage: land,
      ip: '82.' + Math.floor(rnd()*240) + '.' + Math.floor(rnd()*240) + '.' + Math.floor(rnd()*240),
      utm: { source: ch.id, medium: ch.id === 'google' ? 'cpc' : (ch.id === 'meta' ? 'paid_social' : 'organic'), campaign: camp !== '—' ? camp.toLowerCase().replace(/[^a-z]+/g,'_') : null },
    });
    for (let s = 1; s < sessions; s++) {
      const sessTs = new Date(firstClickDate.getTime() + s * (12 + rnd() * 36) * 3600000);
      if (sessTs < resDate) {
        // Build a plausible navigation path — always entry + 1..N deeper pages ending on reservations/menu
        const navPool = ['/', '/menu', '/menu/dinner', '/menu/lunch', '/menu/wine', '/our-story', '/private-hire', '/locations', '/press', '/reservations', '/reservations/book', '/gallery', '/gift-cards', '/contact'];
        const pagesViewed = 2 + Math.floor(rnd() * 6);
        const navigated = [];
        const entry = s % 2 ? '/' : navPool[Math.floor(rnd() * 6) + 1]; // direct returns land on home; channel returns deep-link
        navigated.push(entry);
        for (let p = 1; p < pagesViewed; p++) {
          const next = navPool[Math.floor(rnd() * navPool.length)];
          if (next !== navigated[navigated.length - 1]) navigated.push(next);
        }
        // Weight the last page toward reservation/menu for realism
        if (rnd() > 0.3) navigated[navigated.length - 1] = rnd() > 0.5 ? '/reservations' : '/menu';
        events.push({
          type: 'return_session', ts: sessTs.toISOString(),
          channel: s % 2 ? 'Direct' : ch.name,
          pagesViewed: navigated.length,
          timeOnSite: 45 + Math.floor(rnd() * 300),
          device,
          navigated,
        });
      }
    }
    const resSubmitTs = new Date(resDate.getTime() - (10 + rnd() * 30) * 60000);
    events.push({
      type: 'reservation_submitted', ts: resSubmitTs.toISOString(),
      platform, covers, partyNotes: tags.includes('Birthday') ? 'Birthday celebration — cake requested' : (tags.includes('Allergy noted') ? 'Gluten allergy in party' : null),
      reservationFor: resDate.toISOString(), guestPhone: '+44 7' + Math.floor(1000000 + rnd() * 8999999),
    });
    const confirmTs = new Date(resSubmitTs.getTime() + (1 + rnd() * 3) * 60000);
    // confirmation_sent event removed — hard to track reliably
    if (conversionStatus === 'cancelled') {
      const cancelTs = new Date(resDate.getTime() - (2 + rnd() * 20) * 3600000);
      events.push({ type: 'cancelled', ts: cancelTs.toISOString(), reason: ['Guest cancelled via email','Guest cancelled via phone','No reason given'][Math.floor(rnd()*3)] });
    } else if (conversionStatus === 'no_show') {
      const noshowTs = new Date(resDate.getTime() + 30 * 60000);
      events.push({ type: 'no_show', ts: noshowTs.toISOString(), note: 'Table held 30 min — guest did not arrive' });
    } else if (conversionStatus === 'seated') {
      const posTs = new Date(resDate.getTime() + (45 + rnd() * 90) * 60000);
      const actualCovers = covers + (rnd() > 0.75 ? 1 : 0);
      events.push({
        type: 'pos_match', ts: posTs.toISOString(),
        tableRevenue: Math.round(attrRev * (0.85 + rnd() * 0.3)),
        actualCovers, table: 'T' + (1 + Math.floor(rnd() * 28)),
        duration: 80 + Math.floor(rnd() * 90),
      });
    }
    // Revenue only counts for seated/paid guests — everything else is £0.
    const realisedRevenue = conversionStatus === 'seated' ? attrRev : 0;
    out.push({
      id: ref_code, guestName: first + ' ' + last, site,
      dateTime: resDate,      // reservation date (kept for back-compat / sorting)
      bookingDate: resDate,   // same thing, clearer name for the table column
      enquiryDate: resSubmitTs, // when the guest actually submitted the booking
      covers,
      channel: ch, campaign: camp, adSpend, costPerCover: cpc,
      attributedRevenue: realisedRevenue,
      potentialRevenue: attrRev,
      platform, posVerified, conversionStatus, tags, referrer: ref, landingPage: land, keyword: kw, device,
      journeyEvents: events.sort((a, b) => new Date(a.ts) - new Date(b.ts)),
      sessionsBeforeBooking: sessions,
      daysToBook: Math.round((resSubmitTs - firstClickDate) / 86400000 * 10) / 10,
    });
  }
  return out.sort((a, b) => b.dateTime - a.dateTime);
}

// Formatters
const fmtBGbp = (n) => n == null || !isFinite(n) ? '—' : '£' + Math.round(n).toLocaleString('en-GB');
const fmtBGbp2 = (n) => n == null || !isFinite(n) ? '—' : '£' + n.toFixed(2);
const fmtBDT = (d) => d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + ' · ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
const fmtBTS = (iso) => {
  const d = new Date(iso);
  return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + ' · ' + d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
};

// ──────────────────────────────────────────────────────────────
// Main page
// ──────────────────────────────────────────────────────────────
function BookingsActivityPage({ ctx }) {
  // Trigger re-renders when live bookings arrive
  const [bookingsVersion, setBookingsVersion] = useStB(0);
  useEfB(() => {
    function onReady() { setBookingsVersion(v => v + 1); }
    window.addEventListener('coverstory:bookings-ready', onReady);
    window.addEventListener('coverstory:data-ready', onReady);
    return () => {
      window.removeEventListener('coverstory:bookings-ready', onReady);
      window.removeEventListener('coverstory:data-ready', onReady);
    };
  }, []);
  // Prefer real bookings over mock data; fall back to mock if API hasn't loaded
  const allBookings = useMeB(() => {
    if (window.CoverStory && window.CoverStory.LIVE_BOOKINGS && window.CoverStory.LIVE_BOOKINGS.length) {
      return window.CoverStory.LIVE_BOOKINGS;
    }
    return buildBookings();
  }, [bookingsVersion]);

  // Streamlined filter set. NB: the timeline is owned by the topbar DateRange —
  // the page-local "Timeframe" was removed so there's a single canonical date
  // range across every page. The bookings list is fetched per topbar range, so
  // the table here always reflects whatever range the topbar shows.
  const [filters, setFilters] = useStB({
    channel: [], site: [], landing: [], referrer: [], status: [],
  });
  // Sort by when the booking was MADE (enquiryDate) descending by default so
  // freshly-booked rows surface at the top of the table.
  const [sortKey, setSortKey] = useStB('enquiryDate');
  const [sortDir, setSortDir] = useStB('desc');
  const [page, setPage] = useStB(1);
  const [chartMetric, setChartMetric] = useStB('bookings');
  const [activeDay, setActiveDay] = useStB(null);
  const [selectedBooking, setSelectedBooking] = useStB(null);
  const [activeEvent, setActiveEvent] = useStB(0);

  // No page-local timeframe filter: LIVE_BOOKINGS is already fetched for the
  // topbar's range, so this list reflects whatever range the topbar shows.
  const filtered = useMeB(() => {
    let list = allBookings;
    if (filters.channel.length)  { const s = new Set(filters.channel);  list = list.filter(b => b.channel && s.has(b.channel.id)); }
    if (filters.site.length)     { const s = new Set(filters.site);     list = list.filter(b => s.has(b.site.id)); }
    if (filters.landing.length)  { const s = new Set(filters.landing);  list = list.filter(b => s.has(b.landingPage)); }
    if (filters.referrer.length) { const s = new Set(filters.referrer); list = list.filter(b => s.has(b.referrer)); }
    if (filters.status.length)   { const s = new Set(filters.status);   list = list.filter(b => s.has(b.conversionStatus)); }
    if (activeDay) list = list.filter(b => b.dateTime.toDateString() === activeDay);
    return list;
  }, [allBookings, filters, activeDay]);

  const sorted = useMeB(() => {
    const arr = [...filtered];
    arr.sort((a, b) => {
      let av = a[sortKey], bv = b[sortKey];
      if (sortKey === 'channel') { av = a.channel.name; bv = b.channel.name; }
      if (sortKey === 'site') { av = a.site.name; bv = b.site.name; }
      if (av instanceof Date) { av = av.getTime(); bv = bv.getTime(); }
      if (typeof av === 'string') return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
      return sortDir === 'asc' ? av - bv : bv - av;
    });
    return arr;
  }, [filtered, sortKey, sortDir]);

  const totalPages = Math.max(1, Math.ceil(sorted.length / 20));
  const paged = sorted.slice((page - 1) * 20, page * 20);

  const totals = useMeB(() => {
    const T = filtered.reduce((a, b) => ({
      bookings: a.bookings + 1,
      covers: a.covers + b.covers,
      revenue: a.revenue + b.attributedRevenue,
      spend: a.spend + b.adSpend,
      reserved:  a.reserved  + (b.conversionStatus === 'reserved'  ? 1 : 0),
      seated:    a.seated    + (b.conversionStatus === 'seated'    ? 1 : 0),
      noShow:    a.noShow    + (b.conversionStatus === 'no_show'   ? 1 : 0),
      cancelled: a.cancelled + (b.conversionStatus === 'cancelled' ? 1 : 0),
      seatedCovers: a.seatedCovers + (b.conversionStatus === 'seated' ? b.covers : 0),
    }), { bookings: 0, covers: 0, revenue: 0, spend: 0, reserved: 0, seated: 0, noShow: 0, cancelled: 0, seatedCovers: 0 });
    const cpc = T.covers > 0 ? T.spend / T.covers : 0;
    // Past bookings only — exclude future 'reserved' from funnel conversion %
    const decided = T.seated + T.noShow + T.cancelled;
    const seatedRate = decided > 0 ? T.seated / decided : 0;
    return { ...T, cpc, decided, seatedRate, dBookings: +7.2, dCovers: +9.8, dRevenue: +12.1, dCpc: -4.6 };
  }, [filtered]);

  const chartData = useMeB(() => {
    const byDay = {};
    filtered.forEach(b => {
      const k = b.dateTime.toDateString();
      if (!byDay[k]) byDay[k] = { day: k, bookings: 0, covers: 0, revenue: 0, spend: 0 };
      byDay[k].bookings += 1;
      byDay[k].covers += b.covers;
      byDay[k].revenue += b.attributedRevenue;
      byDay[k].spend += b.adSpend;
    });
    return Object.values(byDay).sort((a, b) => new Date(a.day) - new Date(b.day));
  }, [filtered]);

  function setFilter(k, v) { setFilters(prev => ({ ...prev, [k]: v })); setPage(1); }
  function clearFilter(k) {
    setFilters(prev => ({ ...prev, [k]: [] }));
    setPage(1);
  }
  function clearAll() {
    setFilters({ channel: [], site: [], landing: [], referrer: [], status: [] });
    setActiveDay(null); setPage(1);
  }
  // Dropdown options come from the full topbar-range booking set (no further
  // local timeframe narrowing) so the operator sees every channel / venue /
  // landing page / referrer that was active during the selected range.
  const inTimeframe = allBookings;

  // Dynamic option lists. All derived from the timeframe-filtered set so the
  // dropdowns reflect what's actually bookable in the selected window.
  const channelOptions = useMeB(() => {
    const byId = new Map();
    inTimeframe.forEach(b => { if (b.channel && b.channel.id) byId.set(b.channel.id, b.channel); });
    return [...byId.values()];
  }, [inTimeframe]);
  const siteOptions = useMeB(() => {
    const set = new Set();
    inTimeframe.forEach(b => { if (b.site && b.site.id) set.add(b.site.id); });
    return [...set].map(id => ({ id, name: id }));
  }, [inTimeframe]);
  const landingOptions = useMeB(() => {
    const set = new Set();
    inTimeframe.forEach(b => { if (b.landingPage) set.add(b.landingPage); });
    return [...set].sort();
  }, [inTimeframe]);
  const referrerOptions = useMeB(() => {
    const set = new Set();
    inTimeframe.forEach(b => { if (b.referrer) set.add(b.referrer); });
    return [...set].sort();
  }, [inTimeframe]);

  // Truncate long URLs for the dropdown label while preserving the full
  // string as the option value (so filter matching is exact).
  function truncUrl(u, n) {
    if (!u) return '';
    return u.length > n ? u.slice(0, n - 1) + '…' : u;
  }

  const STATUS_LABELS = { reserved: 'Reserved', seated: 'Seated & paid', no_show: 'No-show', cancelled: 'Cancelled' };

  function summariseMulti(ids, lookup, n) {
    if (!ids.length) return '';
    if (ids.length === 1) return truncUrl(lookup(ids[0]), n || 40);
    return ids.length + ' selected';
  }
  const activeFilters = [];
  if (filters.channel.length)  activeFilters.push({ k: 'channel',  label: 'Channel: '  + summariseMulti(filters.channel,  id => (channelOptions.find(c => c.id === id) || { name: id }).name) });
  if (filters.site.length)     activeFilters.push({ k: 'site',     label: 'Location: ' + summariseMulti(filters.site,     id => id) });
  if (filters.landing.length)  activeFilters.push({ k: 'landing',  label: 'Landing: '  + summariseMulti(filters.landing,  u => u) });
  if (filters.referrer.length) activeFilters.push({ k: 'referrer', label: 'Referrer: ' + summariseMulti(filters.referrer, u => u) });
  if (filters.status.length)   activeFilters.push({ k: 'status',   label: 'Outcome: '  + summariseMulti(filters.status,   id => STATUS_LABELS[id] || id) });
  if (activeDay) activeFilters.push({ k: 'day', label: 'Day: ' + new Date(activeDay).toLocaleDateString('en-GB', { day:'2-digit', month:'short' }) });

  function toggleSort(k) {
    if (sortKey === k) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortKey(k); setSortDir('desc'); }
  }

  if (selectedBooking) {
    return (
      <BookingJourney
        booking={selectedBooking}
        onBack={() => { setSelectedBooking(null); setActiveEvent(0); }}
        activeEvent={activeEvent}
        setActiveEvent={setActiveEvent}
      />
    );
  }

  return (
    <div>
      {/* Filter bar — timeline is owned by the topbar DateRange (single source
          of truth across pages). All filters below are multi-select with ticks. */}
      <div className="bk-filterbar">
        <BkDropdown label="Channel" multi value={filters.channel} onChange={v => setFilter('channel', v)}
          options={[['__all','All channels'], ...channelOptions.map(c => [c.id, c.name])]} />
        <BkDropdown label="Location" multi value={filters.site} onChange={v => setFilter('site', v)}
          options={[['__all','All locations'], ...siteOptions.map(s => [s.id, s.name])]} />
        <BkDropdown label="Landing page" multi value={filters.landing} onChange={v => setFilter('landing', v)}
          options={[['__all','All landing pages'], ...landingOptions.map(u => [u, truncUrl(u, 60)])]} />
        <BkDropdown label="Referrer" multi value={filters.referrer} onChange={v => setFilter('referrer', v)}
          options={[['__all','All referrers'], ...referrerOptions.map(u => [u, truncUrl(u, 60)])]} />
        <BkDropdown label="Outcome" multi value={filters.status} onChange={v => setFilter('status', v)}
          options={[['__all','All outcomes'],['reserved','Reserved (upcoming)'],['seated','Seated & paid'],['no_show','No-show'],['cancelled','Cancelled']]} />
      </div>

      {activeFilters.length > 0 && (
        <div className="bk-pills">
          {activeFilters.map(f => (
            <button key={f.k} className="bk-pill" onClick={() => { if (f.k === 'day') setActiveDay(null); else clearFilter(f.k); }}>
              {f.label}
              <svg width="10" height="10" viewBox="0 0 10 10"><path d="M1 1 L9 9 M9 1 L1 9" stroke="currentColor" strokeWidth="1.4"/></svg>
            </button>
          ))}
          <button className="bk-pill bk-pill-clear" onClick={clearAll}>Clear all</button>
        </div>
      )}

      {/* KPI cards — use brand Kpi component */}
      <div className="pw-grid pw-grid-4" style={{ marginBottom: 20 }}>
        <Kpi label="Total bookings"      value={totals.bookings.toLocaleString('en-GB')} delta={totals.dBookings} sub="vs prior period" />
        <Kpi label="Total covers"        value={totals.covers.toLocaleString('en-GB')}   delta={totals.dCovers}   sub="vs prior period" />
        <Kpi label="Revenue (POS-matched)" value={fmtBGbp(totals.revenue)}                 delta={totals.dRevenue}  sub={`Seated guests only · ${(totals.seatedRate*100).toFixed(0)}% seated`} />
        <Kpi label="Avg cost per cover"  value={fmtBGbp2(totals.cpc)}                       delta={totals.dCpc}      sub="vs prior period" invertDelta />
      </div>



      {/* Chart */}
      <Card
        title={(chartMetric === 'bookings' ? 'Bookings' : chartMetric === 'covers' ? 'Covers' : 'Attributed revenue') + ' over time'}
        sub="Performance"
        right={
          <div className="pw-toggle">
            {[['bookings','Bookings'],['covers','Covers'],['revenue','Revenue']].map(([k,l]) => (
              <button key={k} className={chartMetric === k ? 'on' : ''} onClick={() => setChartMetric(k)}>{l}</button>
            ))}
          </div>
        }
        style={{ marginBottom: 16 }}
      >
        <BkLineChart data={chartData} metric={chartMetric} onDayClick={(d) => setActiveDay(d === activeDay ? null : d)} activeDay={activeDay} />
      </Card>

      {/* Bookings table */}
      <Card
        title={filtered.length.toLocaleString('en-GB') + ' bookings' + (activeFilters.length > 0 ? ' · filtered' : '')}
        sub="Activity"
        right={
          <button className="pw-btn">
            <Icon.download /> Export CSV
          </button>
        }
      >
        <div style={{ overflowX: 'auto', margin: '0 -20px -20px' }}>
          <table className="pw-table bk-table">
            <thead>
              <tr>
                <BkTh label="Booking ref"    sortKey="id"            curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Guest"          sortKey="guestName"     curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Site"           sortKey="site"          curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Booking date"   sortKey="bookingDate"   curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Covers" right   sortKey="covers"        curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Channel"        sortKey="channel"       curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Revenue" right  sortKey="attributedRevenue" curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Platform"       sortKey="platform"      curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Outcome"        sortKey="conversionStatus" curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Enquiry date"   sortKey="enquiryDate"   curKey={sortKey} dir={sortDir} onSort={toggleSort} />
                <BkTh label="Tags" />
              </tr>
            </thead>
            <tbody>
              {paged.map(b => (
                <tr key={b.id} className="clickable" onClick={() => setSelectedBooking(b)}>
                  <td><span className="bk-ref">{b.id}</span></td>
                  <td style={{ fontWeight: 500 }}>{b.guestName}</td>
                  <td style={{ color: 'var(--pw-fg-muted)' }}>{b.site.name}</td>
                  <td className="bk-mono" style={{ color: 'var(--pw-fg-muted)' }}>{fmtBDT(b.bookingDate)}</td>
                  <td className="num bk-mono">{b.covers}</td>
                  <td>
                    <span className="pw-chip" style={{ cursor: 'default' }}>
                      <span className="pw-chip-dot" style={{ background: b.channel.color }} />
                      {b.channel.name}
                    </span>
                  </td>
                  <td className="num bk-mono" style={{ fontWeight: 500 }}>
                    {b.conversionStatus === 'seated'
                      ? fmtBGbp(b.attributedRevenue)
                      : <span style={{ color: 'var(--pw-fg-subtle)' }} title={b.conversionStatus === 'reserved' ? 'Upcoming — revenue pending POS match' : 'No revenue — guest did not sit down'}>—</span>}
                  </td>
                  <td><span className="pw-badge">{b.platform}</span></td>
                  <td><BkStatusPill status={b.conversionStatus} /></td>
                  <td className="bk-mono" style={{ color: 'var(--pw-fg-muted)' }}>{fmtBDT(b.enquiryDate)}</td>
                  <td>{b.tags.length === 0 ? <span style={{ color: 'var(--pw-fg-subtle)' }}>—</span> : b.tags.map(t => <span key={t} className="bk-tag">{t}</span>)}</td>
                </tr>
              ))}
              {paged.length === 0 && (
                <tr><td colSpan="11" style={{ padding: 0 }}>
                  <EmptyState
                    title="No bookings match the current filters"
                    hint={activeFilters.length > 0 ? 'Try removing a filter or widening the timeframe in the topbar.' : 'No bookings in this timeframe yet.'}
                    onClear={activeFilters.length > 0 ? clearAll : null}
                  />
                </td></tr>
              )}
            </tbody>
          </table>
        </div>
        {sorted.length > 20 && (
          <div className="bk-pager">
            <span style={{ color: 'var(--pw-fg-muted)', fontSize: 12 }}>Page {page} of {totalPages} · {sorted.length.toLocaleString()} bookings</span>
            <div style={{ display:'flex', gap: 6 }}>
              <button className="pw-btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>← Previous</button>
              <button className="pw-btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>Next →</button>
            </div>
          </div>
        )}
      </Card>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Sub-components
// ──────────────────────────────────────────────────────────────
// BkDropdown supports two modes:
//   • single-select (multi=false, default): value is a string. The first
//     option ('all', etc.) clears the filter. Clicking an item closes the
//     popover.
//   • multi-select (multi=true): value is an array of selected ids. Empty
//     array = "all" / no filter. Each item shows a ✓ when selected. The first
//     "All …" option acts as a Clear-all/Reset. Popover stays open so users
//     can toggle multiple choices in one session.
function BkDropdown({ label, value, onChange, options, multi }) {
  const [open, setOpen] = useStB(false);
  const ref = useRfB(null);
  useEfB(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener('click', onDoc);
    return () => document.removeEventListener('click', onDoc);
  }, []);

  const isMulti = !!multi;
  const selectedSet = isMulti ? new Set(value || []) : null;

  // Button label
  let displayLabel;
  if (isMulti) {
    if (!value || value.length === 0) {
      displayLabel = (options[0] && options[0][1]) || 'All';
    } else if (value.length === 1) {
      const opt = options.find(o => o[0] === value[0]);
      displayLabel = opt ? opt[1] : value[0];
    } else {
      displayLabel = value.length + ' selected';
    }
  } else {
    const current = options.find(o => o[0] === value);
    displayLabel = current ? current[1] : value;
  }
  const isDefault = isMulti ? (!value || value.length === 0) : (value === 'all' || value === 'month');

  function handleClick(k) {
    if (!isMulti) {
      onChange(k);
      setOpen(false);
      return;
    }
    // multi: first option clears all
    if (k === options[0][0]) {
      onChange([]);
      return;
    }
    const next = new Set(selectedSet);
    if (next.has(k)) next.delete(k); else next.add(k);
    onChange([...next]);
  }

  return (
    <div ref={ref} className={'bk-dd' + (isDefault ? '' : ' active')}>
      <button className="bk-dd-btn" onClick={() => setOpen(o => !o)}>
        <span className="bk-dd-label">{label}</span>
        <span className="bk-dd-value">{displayLabel}</span>
        <svg width="9" height="9" viewBox="0 0 10 10"><path d="M1 3 L5 7 L9 3" stroke="currentColor" fill="none" strokeWidth="1.4"/></svg>
      </button>
      {open && (
        <div className="bk-dd-menu">
          {options.map(([k, l], idx) => {
            const isFirst = idx === 0;
            const on = isMulti
              ? (isFirst ? (!value || value.length === 0) : selectedSet.has(k))
              : value === k;
            return (
              <button key={k} className={on ? 'on' : ''} onClick={() => handleClick(k)}>
                {isMulti && (
                  <span style={{ display: 'inline-block', width: 14, marginRight: 6, color: 'var(--pw-accent)', textAlign: 'center' }}>
                    {on ? '✓' : ''}
                  </span>
                )}
                {l}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

function BkSearch({ label, value, onChange, placeholder }) {
  return (
    <div className={'bk-dd bk-dd-search' + (value ? ' active' : '')}>
      <div className="bk-dd-btn">
        <span className="bk-dd-label">{label}</span>
        <input
          className="bk-search"
          value={value}
          onChange={e => onChange(e.target.value)}
          placeholder={placeholder}
          onClick={e => e.stopPropagation()}
        />
      </div>
    </div>
  );
}

function BkTh({ label, sortKey, curKey, dir, onSort, right }) {
  const active = curKey === sortKey;
  return (
    <th className={right ? 'num' : ''} onClick={() => sortKey && onSort && onSort(sortKey)} style={{ cursor: sortKey ? 'pointer' : 'default', userSelect: 'none' }}>
      <span style={{ display:'inline-flex', alignItems:'center', gap: 4 }}>
        {label}
        {sortKey && (
          <svg width="8" height="10" viewBox="0 0 8 10" style={{ opacity: active ? 1 : 0.3 }}>
            <path d={active && dir === 'asc' ? 'M4 2 L7 6 L1 6 Z' : 'M4 8 L7 4 L1 4 Z'} fill="currentColor"/>
          </svg>
        )}
      </span>
    </th>
  );
}

// Conversion status pill — shown in the bookings table.
const BK_STATUS_META = {
  reserved:  { label: 'Reserved',     tone: 'info',    dot: '#8FB3FF' },
  seated:    { label: 'Seated & paid', tone: 'success', dot: '#7BD3A3' },
  no_show:   { label: 'No-show',      tone: 'warn',    dot: '#E8B36E' },
  cancelled: { label: 'Cancelled',    tone: 'danger',  dot: '#E8836E' },
};
function BkStatusPill({ status }) {
  const m = BK_STATUS_META[status] || BK_STATUS_META.reserved;
  return (
    <span className={'bk-status bk-status-' + m.tone}>
      <span className="bk-status-dot" style={{ background: m.dot }} />
      {m.label}
    </span>
  );
}

// Conversion strip — compact horizontal stacked bar showing outcome breakdown.
// Each segment is proportional to its share of total bookings.
// Click to filter; click the active one again to clear.
function BkConversionStrip({ totals, activeStatus, onPick }) {
  const total = totals.bookings || 1;
  const segments = [
    { key: 'seated',    label: 'Seated & paid', count: totals.seated,    color: '#7BD3A3' },
    { key: 'reserved',  label: 'Upcoming',      count: totals.reserved,  color: '#8FB3FF' },
    { key: 'no_show',   label: 'No-show',       count: totals.noShow,    color: '#E8B36E' },
    { key: 'cancelled', label: 'Cancelled',     count: totals.cancelled, color: '#E8836E' },
  ].filter(s => s.count > 0);
  const pct = (n) => (n / total) * 100;

  return (
    <div className="bk-strip">
      <div className="bk-strip-hd">
        <div>
          <div className="bk-strip-title">Where bookings end up</div>
          <div className="bk-strip-sub">
            {totals.bookings.toLocaleString('en-GB')} bookings · {totals.seated.toLocaleString('en-GB')} became paying customers ({(totals.seatedRate*100).toFixed(0)}% of decided)
          </div>
        </div>
        <div className="bk-strip-hint">Click a segment to filter</div>
      </div>
      <div className="bk-strip-bar" role="group" aria-label="Conversion outcome breakdown">
        {segments.map(s => {
          const w = pct(s.count);
          const active = activeStatus === s.key;
          const dimmed = activeStatus !== 'all' && !active;
          return (
            <button
              key={s.key}
              type="button"
              className={'bk-strip-seg' + (active ? ' is-active' : '') + (dimmed ? ' is-dim' : '')}
              style={{ width: w + '%', background: s.color }}
              onClick={() => onPick(s.key)}
              title={`${s.label}: ${s.count.toLocaleString('en-GB')} (${w.toFixed(1)}%)`}
            >
              {w > 8 && <span className="bk-strip-seg-label">{w.toFixed(0)}%</span>}
            </button>
          );
        })}
      </div>
      <div className="bk-strip-legend">
        {segments.map(s => {
          const active = activeStatus === s.key;
          return (
            <button
              key={s.key}
              type="button"
              className={'bk-strip-legend-item' + (active ? ' is-active' : '')}
              onClick={() => onPick(s.key)}
            >
              <span className="bk-strip-legend-dot" style={{ background: s.color }} />
              <span className="bk-strip-legend-label">{s.label}</span>
              <span className="bk-strip-legend-count">{s.count.toLocaleString('en-GB')}</span>
              <span className="bk-strip-legend-pct">({pct(s.count).toFixed(0)}%)</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Line chart (SVG, brand colours)
// ──────────────────────────────────────────────────────────────
function BkLineChart({ data, metric, onDayClick, activeDay }) {
  const W = 1100, H = 240, pad = { l: 60, r: 20, t: 16, b: 32 };
  const [hover, setHover] = useStB(null);
  if (!data.length) return <div className="pw-empty">No data in the selected period</div>;

  const key = metric === 'cpc' ? null : metric;
  const values = data.map(d => key ? d[key] : (d.covers > 0 ? d.spend / d.covers : 0));
  const maxV = Math.max(...values, 1);
  const minV = 0;
  const innerW = W - pad.l - pad.r;
  const innerH = H - pad.t - pad.b;
  const x = (i) => pad.l + (data.length === 1 ? innerW / 2 : (i / (data.length - 1)) * innerW);
  const y = (v) => pad.t + innerH - ((v - minV) / (maxV - minV || 1)) * innerH;

  const pathD = values.map((v, i) => (i === 0 ? 'M' : 'L') + x(i) + ',' + y(v)).join(' ');
  const areaD = pathD + ' L' + x(values.length - 1) + ',' + (pad.t + innerH) + ' L' + x(0) + ',' + (pad.t + innerH) + ' Z';
  const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => minV + t * (maxV - minV));

  const fmtY = (v) => metric === 'revenue' ? (v >= 1000 ? '£' + (v/1000).toFixed(0) + 'k' : '£' + Math.round(v)) :
                       metric === 'cpc' ? '£' + v.toFixed(0) :
                       Math.round(v).toLocaleString('en-GB');
  const fmtTip = (v) => metric === 'revenue' ? fmtBGbp(v) : metric === 'cpc' ? fmtBGbp2(v) : Math.round(v).toLocaleString('en-GB');

  return (
    <div style={{ position: 'relative' }}>
      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display:'block' }} preserveAspectRatio="none">
        <defs>
          <linearGradient id="bkGrad" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--pw-accent)" stopOpacity="0.22"/>
            <stop offset="100%" stopColor="var(--pw-accent)" stopOpacity="0"/>
          </linearGradient>
        </defs>
        {ticks.map((t, i) => (
          <g key={i}>
            <line x1={pad.l} y1={y(t)} x2={W - pad.r} y2={y(t)} stroke="var(--pw-hair)" strokeWidth="1"/>
            <text x={pad.l - 10} y={y(t) + 4} fill="var(--pw-fg-subtle)" fontSize="10" textAnchor="end" style={{ fontFamily: 'ui-monospace, monospace', letterSpacing: '0.04em' }}>{fmtY(t)}</text>
          </g>
        ))}
        {data.map((d, i) => {
          if (data.length > 14 && i % Math.ceil(data.length / 10) !== 0 && i !== data.length - 1) return null;
          return (
            <text key={i} x={x(i)} y={H - pad.b + 18} fill="var(--pw-fg-subtle)" fontSize="10" textAnchor="middle" style={{ fontFamily: 'ui-monospace, monospace', letterSpacing: '0.04em' }}>
              {new Date(d.day).toLocaleDateString('en-GB', { day:'2-digit', month:'short' })}
            </text>
          );
        })}
        <path d={areaD} fill="url(#bkGrad)"/>
        <path d={pathD} fill="none" stroke="var(--pw-accent)" strokeWidth="1.75"/>
        {values.map((v, i) => (
          <g key={i}>
            <circle cx={x(i)} cy={y(v)} r={hover === i ? 5 : (activeDay === data[i].day ? 4 : 2.5)}
              fill={activeDay === data[i].day ? 'var(--pw-chart-3)' : 'var(--pw-accent)'}
              stroke="var(--pw-surface)" strokeWidth="2"
              style={{ cursor:'pointer' }}
              onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}
              onClick={() => onDayClick(data[i].day)}
            />
            <rect x={x(i) - (innerW / data.length / 2)} y={pad.t} width={innerW / data.length} height={innerH} fill="transparent"
              onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}
              onClick={() => onDayClick(data[i].day)}
              style={{ cursor:'pointer' }}/>
          </g>
        ))}
      </svg>
      {hover !== null && (
        <div className="pw-tt" style={{ left: `${(x(hover) / W) * 100}%`, top: `${(y(values[hover]) / H) * 100}%` }}>
          <div className="tt-k">{new Date(data[hover].day).toLocaleDateString('en-GB', { weekday:'short', day:'2-digit', month:'short' })}</div>
          <div style={{ fontWeight: 500, marginTop: 2 }}>{fmtTip(values[hover])}</div>
          <div className="tt-k" style={{ marginTop: 4 }}>Click to filter</div>
        </div>
      )}
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Booking Journey view
// ──────────────────────────────────────────────────────────────
function BookingJourney({ booking, onBack, activeEvent, setActiveEvent }) {
  // For real bookings (have _realRef and empty journeyEvents), lazy-fetch the
  // full touchpoint journey from the API on first render.
  const [liveJourney, setLiveJourney] = useStB(null);
  // For real bookings, fetch the full journey lazily on first render.
  useEfB(() => {
    if (booking._realRef && (!booking.journeyEvents || booking.journeyEvents.length === 0) && window.CoverStory.fetchJourney) {
      window.CoverStory.fetchJourney(booking._realRef).then(function (events) {
        setLiveJourney(events);
      });
    }
  }, [booking._realRef]);
  // Resolved journey: live-fetched events take priority, then any pre-baked
  // events on the booking (mock data path), then an empty array.
  const journeyEvents = liveJourney || booking.journeyEvents || [];

  const firstTouch = journeyEvents.find(e => e.type === 'first_click');
  // "Last touch" attribution should reflect what marketing channel converted
  // the guest — i.e. the last touchpoint on the CLIENT'S site (mildreds.com),
  // not the SevenRooms widget where they completed the booking flow.
  // Find the last touchpoint whose landingPage is on a non-sevenrooms domain.
  // The data layer (fetchJourney) has already filtered SevenRooms widget pages
  // out of the timeline and synthesised client-domain touchpoints from SR
  // referrers when needed. So here we just take first/last touchpoint events
  // as the timeline presents them.
  const touchpointEvents = journeyEvents.filter(e => e.type === 'first_click' || e.type === 'return_session');
  const lastTouch = touchpointEvents.length ? touchpointEvents[touchpointEvents.length - 1] : firstTouch;
  const firstTouchDisplayUrl = firstTouch ? firstTouch.landingPage : null;
  const lastTouchDisplayUrl = lastTouch ? lastTouch.landingPage : null;
  const posEvent = journeyEvents.find(e => e.type === 'pos_match');
  const reservationEvent = journeyEvents.find(e => e.type === 'reservation_submitted');
  const roas = booking.adSpend > 0 ? (booking.attributedRevenue / booking.adSpend) : null;

  return (
    <div>
      <div className="pw-subnav" style={{ marginBottom: 18 }}>
        <a onClick={onBack}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" style={{ marginRight: 4, verticalAlign: '-2px' }}><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
          All bookings
        </a>
        <span className="sep">/</span>
        <span className="cur bk-mono">{booking.id}</span>
      </div>

      <div className="bk-journey-hero">
        <div>
          <div className="pw-eyebrow">Booking journey</div>
          <div style={{ display:'flex', alignItems:'baseline', gap: 12, marginTop: 8 }}>
            <span className="bk-ref" style={{ fontSize: 16 }}>{booking.id}</span>
            <h2 style={{ margin: 0, fontWeight: 500, fontSize: 30, letterSpacing: '-0.02em' }}>{booking.guestName}</h2>
          </div>
          <div style={{ color: 'var(--pw-fg-muted)', fontSize: 14, marginTop: 6 }}>
            {booking.site.name} <span style={{ color: 'var(--pw-fg-subtle)' }}>·</span> <span className="bk-mono">{fmtBDT(booking.dateTime)}</span> <span style={{ color: 'var(--pw-fg-subtle)' }}>·</span> {booking.covers} covers
          </div>
        </div>
        <div>
          <BkStatusPill status={booking.conversionStatus} />
        </div>
      </div>

      <div className="bk-journey-body">
        <div>
          <Card title="Event timeline" sub={journeyEvents.length + ' events'} style={{ marginBottom: 16 }}>
            {journeyEvents.length === 0
              ? <div style={{ padding: 40, textAlign: 'center', color: 'var(--pw-fg-muted)', fontSize: 13 }}>
                  {booking._realRef ? 'Loading touchpoints…' : 'No touchpoint events captured for this booking.'}
                </div>
              : <BkTimeline events={journeyEvents} active={activeEvent} onSelect={setActiveEvent}/>}
          </Card>
          {journeyEvents.length > 0 && journeyEvents[activeEvent] && EVENT_META[journeyEvents[activeEvent].type] && (
            <Card
              title={EVENT_META[journeyEvents[activeEvent].type].title}
              sub={'Event ' + String(activeEvent + 1).padStart(2, '0') + ' · ' + fmtBTS(journeyEvents[activeEvent].ts)}
            >
              <BkEventDetail event={journeyEvents[activeEvent]} />
            </Card>
          )}
        </div>

        <div>
          <Card title="Attribution summary" sub="RAM">
            <BkSummaryRow k="Guest" v={booking.guestName} />
            <BkSummaryRow k="Booking ref" v={booking.id} mono />
            <BkSummaryRow k="Site" v={booking.site.name} />

            <div className="bk-divider"/>

            <div className="bk-summary-k" style={{ marginBottom: 6 }}>First touch</div>
            <div style={{ marginBottom: 4 }}>
              <span className="pw-chip" style={{ cursor: 'default' }}>
                <span className="pw-chip-dot" style={{ background: booking.channel.color }}/>
                {firstTouch ? firstTouch.channel : booking.channel.name}
              </span>
            </div>
            <div style={{ color: 'var(--pw-fg-muted)', fontSize: 12, marginBottom: 6 }}>
              {firstTouch && firstTouch.campaign && firstTouch.campaign !== '—' ? firstTouch.campaign : <span style={{ color: 'var(--pw-fg-subtle)' }}>No campaign</span>}
            </div>
            {firstTouchDisplayUrl && (
              <div style={{ fontSize: 11, color: 'var(--pw-fg-muted)', marginBottom: 14, wordBreak: 'break-all', lineHeight: 1.4 }}>
                {firstTouchDisplayUrl}
              </div>
            )}

            <div className="bk-summary-k" style={{ marginBottom: 6 }}>Last touch <span style={{ fontSize: 10, color: 'var(--pw-fg-subtle)', letterSpacing: '0.04em', textTransform: 'uppercase', marginLeft: 6 }}>Conversion page</span></div>
            <div style={{ marginBottom: 4 }}>
              <span className="pw-chip" style={{ cursor: 'default' }}>
                <span className="pw-chip-dot" style={{ background: lastTouch === firstTouch ? booking.channel.color : 'var(--pw-chart-5)' }}/>
                {lastTouch ? lastTouch.channel : booking.channel.name}
              </span>
            </div>
            <div style={{ color: 'var(--pw-fg-muted)', fontSize: 12, marginBottom: 6 }}>
              {!firstTouch ? 'Touchpoints loading…' : (lastTouch === firstTouch ? 'Single-session journey' : 'After ' + Math.max(0, booking.sessionsBeforeBooking - 1) + ' return sessions')}
            </div>
            {lastTouchDisplayUrl && (
              <div style={{ fontSize: 11, color: 'var(--pw-fg-muted)', marginBottom: 14, wordBreak: 'break-all', lineHeight: 1.4 }}>
                {lastTouchDisplayUrl}
              </div>
            )}

            <div className="bk-divider"/>

            <BkSummaryRow k="Sessions" v={booking.sessionsBeforeBooking} mono />
            <BkSummaryRow k="Days in advance" v={booking.daysToBook} mono />

            <div className="bk-divider"/>

            <BkSummaryRow k="Ad spend attributed" v={booking.adSpend ? fmtBGbp2(booking.adSpend) : '—'} mono />

            <div className="bk-divider"/>

            <div className="bk-summary-k" style={{ marginBottom: 8 }}>Outcome</div>
            <div style={{ marginBottom: 10 }}>
              <BkStatusPill status={booking.conversionStatus} />
            </div>
            <BkSummaryRow
              k="Realised revenue"
              v={booking.conversionStatus === 'seated'
                  ? fmtBGbp(booking.attributedRevenue)
                  : (booking.conversionStatus === 'reserved' ? 'Pending POS match' : '£0')}
              mono
              bold={booking.conversionStatus === 'seated'}
            />
            {booking.conversionStatus !== 'seated' && (
              <BkSummaryRow
                k="Potential revenue"
                v={fmtBGbp(booking.potentialRevenue)}
                mono
              />
            )}

            <div className="bk-divider"/>

            <div className="bk-summary-k" style={{ marginBottom: 8 }}>Source detected via CoverStory pixel</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 10 }}>
              <span className="pw-chip" style={{ cursor: 'default' }}>
                <span className="pw-chip-dot" style={{ background: booking.channel.color }}/>
                {firstTouch ? firstTouch.channel : booking.channel.name}
              </span>
            </div>

            <div style={{ padding: '10px 12px', border: '1px dashed var(--pw-hair-strong)', marginBottom: 4, cursor: 'pointer' }}
                 onClick={() => {}}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
                <div style={{ fontSize: 13, fontWeight: 500 }}>+ Add a source</div>
                <span style={{ fontSize: 11, color: 'var(--pw-fg-subtle)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>Pixel</span>
              </div>
              <div style={{ fontSize: 12, color: 'var(--pw-fg-muted)', marginTop: 4 }}>
                Attribute this booking to a source not yet detected
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 8 }}>
                {['Meta Ads', 'Google Ads', 'Google Organic', 'TikTok', 'Email', 'Referral'].map(s => (
                  <span key={s} style={{ fontSize: 11, padding: '3px 8px', border: '1px solid var(--pw-hair-strong)', color: 'var(--pw-fg-muted)', background: 'var(--pw-bg)' }}>
                    {s}
                  </span>
                ))}
              </div>
            </div>

            <div className="bk-divider"/>

            <BkSummaryRow k="Covers booked" v={booking.covers} mono />
            <BkSummaryRow k="Covers at table" v={posEvent ? posEvent.actualCovers : '—'} mono />

            {booking.tags.length > 0 && <>
              <div className="bk-divider"/>
              <div className="bk-summary-k" style={{ marginBottom: 8 }}>Tags</div>
              <div style={{ display:'flex', flexWrap:'wrap', gap: 4 }}>
                {booking.tags.map(t => <span key={t} className="bk-tag">{t}</span>)}
              </div>
            </>}
          </Card>
        </div>
      </div>
    </div>
  );
}

function BkSummaryRow({ k, v, mono, bold }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', padding: '6px 0', fontSize: 13 }}>
      <span style={{ color: 'var(--pw-fg-muted)' }}>{k}</span>
      <span className={mono ? 'bk-mono' : ''} style={{ fontWeight: bold ? 500 : 400, color: v === '—' ? 'var(--pw-fg-subtle)' : 'var(--pw-fg)' }}>{v}</span>
    </div>
  );
}

const EVENT_META = {
  first_click:            { title: 'First click',            icon: 'click' },
  return_session:         { title: 'Return session',         icon: 'refresh' },
  reservation_submitted:  { title: 'Reservation submitted',  icon: 'check' },
  pos_match:              { title: 'POS transaction matched',icon: 'pos' },
  no_show:                { title: 'No-show',                icon: 'warn' },
  cancelled:              { title: 'Booking cancelled',      icon: 'x' },
};

function BkTimeline({ events, active, onSelect }) {
  return (
    <div className="bk-timeline">
      {events.map((e, i) => {
        const meta = EVENT_META[e.type];
        const prev = events[i - 1];
        const gap = prev ? ((new Date(e.ts) - new Date(prev.ts)) / 60000) : 0;
        const gapLabel = !prev ? null : (
          gap < 60 ? Math.round(gap) + 'm' :
          gap < 60 * 24 ? (gap / 60).toFixed(1) + 'h' :
          (gap / (60 * 24)).toFixed(1) + 'd'
        );
        return (
          <React.Fragment key={i}>
            {prev && (
              <div className="bk-timeline-connector">
                <div className="bk-timeline-line"/>
                <div className="bk-timeline-gap">{gapLabel}</div>
              </div>
            )}
            <button
              className={'bk-timeline-node' + (active === i ? ' active' : '')}
              onClick={() => onSelect(i)}
            >
              <div className="bk-node-step bk-mono">{String(i + 1).padStart(2, '0')}</div>
              <div className="bk-node-icon">
                <NodeIcon type={meta.icon}/>
              </div>
              <div className="bk-node-title">{meta.title}</div>
              <div className="bk-node-ts bk-mono">{fmtBTS(e.ts)}</div>
            </button>
          </React.Fragment>
        );
      })}
    </div>
  );
}

function NodeIcon({ type }) {
  const props = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' };
  if (type === 'click')   return <svg {...props}><path d="M9 9l6 3-3 6-3-9z"/><path d="M3 3l3 3M3 9h3M9 3v3"/></svg>;
  if (type === 'refresh') return <svg {...props}><path d="M20 12a8 8 0 1 1-3-6.2"/><path d="M20 4v5h-5"/></svg>;
  if (type === 'check')   return <svg {...props}><rect x="4" y="5" width="16" height="15"/><path d="M9 12l2 2 4-4"/></svg>;
  if (type === 'mail')    return <svg {...props}><rect x="3" y="6" width="18" height="13"/><path d="M3 7l9 7 9-7"/></svg>;
  if (type === 'pos')     return <svg {...props}><rect x="3" y="5" width="18" height="14"/><path d="M3 10h18M8 14h3"/></svg>;
  if (type === 'warn')    return <svg {...props}><path d="M12 3l10 18H2z"/><path d="M12 10v5M12 18h.01"/></svg>;
  if (type === 'x')       return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/></svg>;
  return null;
}

function BkPathList({ paths }) {
  return (
    <div className="bk-path">
      {paths.map((p, i) => (
        <React.Fragment key={i}>
          <span className="bk-path-step bk-mono">{p}</span>
          {i < paths.length - 1 && (
            <svg className="bk-path-arrow" width="10" height="10" viewBox="0 0 10 10" aria-hidden>
              <path d="M2 2 L6 5 L2 8" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

function BkEventDetail({ event }) {
  const rows = [];
  // First click + return sessions share the same rich touchpoint shape (real
  // BQ data). Render the same columns for both so the timeline reads as
  // "what they saw on each visit" rather than just "where they were once".
  if (event.type === 'first_click' || event.type === 'return_session') {
    rows.push(['Channel', event.channel]);
    if (event.campaign) rows.push(['Campaign', event.campaign]);
    if (event.keyword) rows.push(['Keyword', event.keyword, true]);
    rows.push(['Referrer', event.referrer || 'Direct', true]);
    rows.push(['Landing page', event.landingPage || '—', true]);
    if (event.device) rows.push(['Device', event.device]);
    if (event.ip) rows.push(['IP', event.ip, true]);
    if (event.utm) {
      if (event.utm.source)   rows.push(['UTM source', event.utm.source, true]);
      if (event.utm.medium)   rows.push(['UTM medium', event.utm.medium, true]);
      if (event.utm.campaign) rows.push(['UTM campaign', event.utm.campaign, true]);
    }
    // Mock-data legacy fields (only present for buildBookings() data, not real)
    if (event.type === 'return_session' && event.pagesViewed) rows.push(['Pages viewed', event.pagesViewed]);
    if (event.type === 'return_session' && event.timeOnSite)  rows.push(['Time on site', event.timeOnSite + 's']);
    if (event.navigated && event.navigated.length) {
      rows.push(['Path navigated', <BkPathList key="p" paths={event.navigated} />, false, true]);
    }
  } else if (event.type === 'reservation_submitted') {
    rows.push(['Platform', event.platform]);
    rows.push(['Covers', event.covers]);
    rows.push(['Submitted at', new Date(event.ts).toLocaleString('en-GB')]);
    if (event.reservationFor) rows.push(['Reservation for', new Date(event.reservationFor).toLocaleString('en-GB')]);
    if (event.leadTimeDays != null) rows.push(['Days in advance', event.leadTimeDays]);
    if (event.guestPhone) rows.push(['Guest phone', event.guestPhone, true]);
    if (event.partyNotes) rows.push(['Party notes', event.partyNotes]);
  } else if (event.type === 'confirmation_sent') {
    rows.push(['Email platform', event.emailPlatform]);
    rows.push(['Sent at', fmtBTS(event.ts)]);
    rows.push(['Opened at', event.openedAt ? fmtBTS(event.openedAt) : 'Not yet opened']);
  } else if (event.type === 'pos_match') {
    rows.push(['Table', event.table, true]);
    rows.push(['Actual covers', event.actualCovers]);
    rows.push(['Table revenue', fmtBGbp(event.tableRevenue), true]);
    rows.push(['Duration', event.duration + ' min']);
  } else if (event.type === 'no_show') {
    rows.push(['Status', 'Guest did not arrive']);
    rows.push(['Note', event.note]);
    rows.push(['Realised revenue', '£0 — table held but empty', true]);
  } else if (event.type === 'cancelled') {
    rows.push(['Status', 'Cancelled before arrival']);
    rows.push(['Reason', event.reason]);
    rows.push(['Realised revenue', '£0 — no table seated', true]);
  }
  return (
    <div className="bk-event-grid">
      {rows.map(([k, v, mono, full], i) => (
        <div key={i} className={'bk-event-row' + (full ? ' bk-event-row-full' : '')}>
          <div className="bk-event-k">{k}</div>
          <div className={'bk-event-v' + (mono ? ' bk-mono' : '')}>{v}</div>
        </div>
      ))}
    </div>
  );
}

Object.assign(window, { BookingsActivityPage });
