// Root app component — owns navigation, search state, bookings collection.

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#1c47ff",
  "density": "regular",
  "glow": true,
  "groupBy": "flat"
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Auth gate: don't paint the portal chrome until we know the user is signed
  // in. While 'pending' (auth check in flight) or 'anon' (redirecting to
  // login) we render a neutral full-screen splash instead of the sidebar/topbar.
  const [authState, setAuthState] = React.useState(() => (window.__pp_auth) || 'pending');
  React.useEffect(() => {
    const onAuth = (e) => setAuthState((e && e.detail && e.detail.state) || window.__pp_auth || 'pending');
    window.addEventListener('pp:auth', onAuth);
    // Auth may have resolved before this listener attached.
    if (window.__pp_auth && window.__pp_auth !== 'pending') setAuthState(window.__pp_auth);
    return () => window.removeEventListener('pp:auth', onAuth);
  }, []);

  const [route, setRoute] = React.useState({ screen: 'search' });
  const [query, setQuery] = React.useState({
    text: '',
    location: 'All locations',
    productType: null,
    operatorId: null,
    date: new Date().toISOString().slice(0, 10),
    guests: 2,
    near: null,
    vibe: null,
    // Whether availability has been explicitly requested. Browsing into the
    // listings (by product / operator / venue) shows the matching venues but
    // leaves this false, so no times are fetched. Pressing the search icon
    // commits a real search and flips it true. See useAllProductsAvailability.
    searched: false,
  });
  const [searching, setSearching] = React.useState(false);
  const [searchError, setSearchError] = React.useState(null);
  const [drawerSlot, setDrawerSlot] = React.useState(null);
  const [bookings, setBookings] = React.useState(SEED_BOOKINGS);
  const [bookingsInitialText, setBookingsInitialText] = React.useState('');
  const [viewingBookingId, setViewingBookingId] = React.useState(null);
  const [walkinOpen, setWalkinOpen] = React.useState(false);
  const [railExpanded, setRailExpanded] = React.useState(() => {
    try { return localStorage.getItem('pp_rail_expanded') === '1'; } catch (e) { return false; }
  });
  const [dataVersion, setDataVersion] = React.useState(0);
  const [liveStatus, setLiveStatus] = React.useState(() => ({
    ready: !!window.__pp_live?.ready,
    live: !!window.__pp_live?.dataLive,
    error: window.__pp_live?.error || null,
  }));
  React.useEffect(() => {
    try { localStorage.setItem('pp_rail_expanded', railExpanded ? '1' : '0'); } catch (e) {}
  }, [railExpanded]);
  React.useEffect(() => {
    const onLoaded = (e) => {
      setLiveStatus({
        ready: true,
        live: !!(e?.detail?.live ?? window.__pp_live?.dataLive),
        error: window.__pp_live?.error || null,
      });
      // Re-seed bookings from live data once the catalogue has resolved.
      if (window.SEED_BOOKINGS) setBookings(window.SEED_BOOKINGS.slice());
      setDataVersion(v => v + 1);
    };
    window.addEventListener('pp:data-loaded', onLoaded);
    if (window.__pp_live?.ready) onLoaded({ detail: { live: window.__pp_live.dataLive } });
    return () => window.removeEventListener('pp:data-loaded', onLoaded);
  }, []);

  // Mirror route + overlay state into the browser history so swipe-back /
  // back-button match the in-app back behavior instead of leaving the SPA.
  // Each route change or overlay open pushes a history entry; close handlers
  // call history.back() to consume it; popstate restores from the snapshot.
  const popInFlight = React.useRef(false);
  const queryRef = React.useRef(query);
  React.useEffect(() => { queryRef.current = query; }, [query]);
  const fp = `${route.screen}|${drawerSlot ? 'd' : '0'}|${viewingBookingId || ''}|${walkinOpen ? '1' : '0'}`;
  const fpRef = React.useRef(fp);
  const snapFp = (s) => `${s.route?.screen || 'search'}|${s.drawerSlot ? 'd' : '0'}|${s.viewingBookingId || ''}|${s.walkinOpen ? '1' : '0'}`;
  React.useEffect(() => {
    window.history.replaceState({
      ppSnapshot: { route: { screen: 'search' }, drawerSlot: null, viewingBookingId: null, walkinOpen: false },
      ppQuery: queryRef.current,
    }, '');
    const onPop = (e) => {
      const snap = e.state?.ppSnapshot;
      if (!snap) return;
      popInFlight.current = true;
      fpRef.current = snapFp(snap);
      setRoute(snap.route || { screen: 'search' });
      if (e.state.ppQuery) setQuery(e.state.ppQuery);
      setDrawerSlot(snap.drawerSlot || null);
      setViewingBookingId(snap.viewingBookingId || null);
      setWalkinOpen(!!snap.walkinOpen);
    };
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);
  React.useEffect(() => {
    if (popInFlight.current) { popInFlight.current = false; fpRef.current = fp; return; }
    if (fp === fpRef.current) return;
    window.history.pushState({
      ppSnapshot: { route, drawerSlot, viewingBookingId, walkinOpen },
      ppQuery: queryRef.current,
    }, '');
    fpRef.current = fp;
  }, [route, drawerSlot, viewingBookingId, walkinOpen]);
  // Keep the CURRENT history entry's query snapshot in sync with live filters.
  // The pushState above only captures `query` at navigation time, so a filter
  // changed in place (date, guests, location…) would otherwise leave a stale
  // snapshot — and a later history.back() (e.g. closing the booking drawer)
  // would restore the old value, resetting the date to today. Patch the
  // current entry in place whenever query changes.
  React.useEffect(() => {
    const st = window.history.state;
    if (!st || !st.ppSnapshot) return;
    window.history.replaceState({ ...st, ppQuery: query }, '');
  }, [query]);

  const handleNewWalkin = (booking) => setBookings(bs => [booking, ...bs]);

  const handleOpenBooking = (booking) => setViewingBookingId(booking.id);
  const handleCancelBooking = async (id) => {
    setBookings(bs => bs.map(b => b.id === id ? { ...b, status: 'cancelled' } : b));
    if (!liveStatus.live) return;
    try { await ppCancelBooking(id); }
    catch (e) { console.warn('[Bookable] cancel failed', e); }
  };

  // Amend an existing booking. Optimistically patch local state, then mirror
  // the change to Bookable as a JSON Patch (only the fields that actually
  // changed). The drawer has already re-checked availability for any
  // date/time/party-size change before calling this.
  // Returns true on success, false if the live PATCH failed (the drawer relies
  // on this to decide whether to show the "Booking updated" confirmation — we
  // must not claim the customer was notified when the amend never landed).
  const handleSaveBooking = async (updated) => {
    const orig = bookings.find(b => b.id === updated.id);
    setBookings(bs => bs.map(b => b.id === updated.id ? updated : b));
    if (!liveStatus.live || !orig) return true;
    const ops = [];
    const rep = (path, value) => ops.push({ op: 'replace', path, value });
    if (updated.date  !== orig.date)  rep('/date', updated.date);
    if (updated.time  !== orig.time)  rep('/time', updated.time);
    if (updated.guests !== orig.guests) rep('/partySize', updated.guests);
    if ((updated.email || '') !== (orig.email || '')) rep('/email', updated.email || '');
    if ((updated.phone || '') !== (orig.phone || '')) rep('/phone', updated.phone || '');
    if (!ops.length) return true;
    try {
      await ppUpdateBooking(updated.id, ops);
      return true;
    } catch (e) {
      console.warn('[Bookable] amend failed', e);
      // Roll the optimistic update back so the list reflects reality.
      setBookings(bs => bs.map(b => b.id === updated.id ? orig : b));
      return false;
    }
  };

  const viewingBooking = bookings.find(b => b.id === viewingBookingId) || null;

  // `baseQuery` lets a caller commit an explicit filter set (the results
  // screen edits a local draft and only commits it on submit, so search never
  // re-runs as the partner tweaks date/guests/product/operator). Defaults to
  // the live `query` for the search screen, which edits it in place.
  const handleSearch = async (baseQuery) => {
    const q = { ...(baseQuery || query) };
    // Pressing the search icon is the explicit commit that fetches availability.
    q.searched = true;

    const text = (q.text || '').trim();
    const isNatural = text.split(/\s+/).length >= 2;
    if (isNatural) {
      setSearching(true);
      setSearchError(null);
      try {
        const ai = await ppAiSearch(text);
        const f = ai.filters || {};
        if (f.intent === 'existing-bookings') {
          setSearching(false);
          const lookup = f.bookingId || f.customerName || text;
          goToBookings(lookup);
          return;
        }
        if (f.productType) q.productType = f.productType;
        if (f.partySize) q.guests = Number(f.partySize) || q.guests;
        if (f.date) q.date = f.date;
        if (f.city) q.location = f.city;
        q.area = f.area || null;
        if (ai.near && Number.isFinite(ai.near.lat) && Number.isFinite(ai.near.lng)) {
          q.near = { lat: ai.near.lat, lng: ai.near.lng, radiusKm: ai.near.radiusKm || 5, label: ai.near.label || f.postCode };
        } else {
          q.near = null;
        }
        if (f.operatorName) {
          const op = (window.OPERATORS || []).find(o => o.name.toLowerCase().includes(f.operatorName.toLowerCase()));
          if (op) q.operatorId = op.id;
        }
        q.vibe = f.vibe || null;
      } catch (e) {
        setSearchError(e?.message || String(e));
      } finally {
        setSearching(false);
      }
    }

    setQuery(q);
    setRoute({ screen: 'results' });
  };

  const goToBookings = (textPrefill) => {
    if (textPrefill !== undefined) setBookingsInitialText(textPrefill);
    setRoute({ screen: 'bookings' });
  };

  // Route omni-suggest picks. Each kind has a different navigation target.
  // Whenever a pick is a specific entity (venue / operator / product) we
  // strip prior context-sensitive filters (location, area, vibe, near,
  // text, operator, productType) so the dropdown choice is authoritative
  // rather than being intersected with whatever the previous search left
  // behind. Date + guests + sort-style fields are preserved.
  const clearedContext = () => ({
    ...query,
    text: '',
    location: 'All locations',
    operatorId: null,
    productType: null,
    venueId: null,
    area: null,
    vibe: null,
    near: null,
    // Browsing into a filtered listing is not a search — defer availability
    // until the partner explicitly searches. run-search overrides this.
    searched: false,
  });
  const handlePickSuggestion = (pick) => {
    if (!pick) return;
    switch (pick.kind) {
      case 'booking': {
        // Open the read-only details drawer for that booking.
        setViewingBookingId(pick.booking.id);
        return;
      }
      case 'venue': {
        const v = pick.venue;
        setQuery({ ...clearedContext(), venueId: v.id, text: v.name, location: v.city || 'All locations' });
        setRoute({ screen: 'results' });
        return;
      }
      case 'operator': {
        const o = pick.operator;
        setQuery({ ...clearedContext(), operatorId: o.id });
        setRoute({ screen: 'results' });
        return;
      }
      case 'product': {
        const p = pick.product;
        setQuery({ ...clearedContext(), productType: p.id });
        setRoute({ screen: 'results' });
        return;
      }
      case 'run-search': {
        // An explicit natural-language search — fetch availability on arrival.
        if (typeof pick.text === 'string' && pick.text.length) {
          setQuery({ ...clearedContext(), text: pick.text, searched: true });
        } else {
          setQuery({ ...clearedContext(), searched: true });
        }
        setRoute({ screen: 'results' });
        return;
      }
      case 'search-bookings':
      case 'bookings-search': {
        goToBookings(pick.text || query.text);
        return;
      }
      case 'screen': {
        if (pick.screen === 'bookings') goToBookings('');
        else setRoute({ screen: pick.screen });
        return;
      }
      default: return;
    }
  };

  const handleNav = (id) => {
    if (id === 'search') setRoute({ screen: 'search' });
    if (id === 'bookings') setRoute({ screen: 'bookings' });
    if (id === 'apikeys') setRoute({ screen: 'apikeys' });
  };

  const handleBook = (slot) => setDrawerSlot(slot);

  const handleConfirm = async (full) => {
    const venue = venueById(full.venueId) || {};
    // Prefer the composite the slot was sourced from (multiple Bookable
    // products can collapse into one portal bucket — book against the one
    // whose availability surfaced this time).
    const compositeId = full.compositeId || (venue.composites && venue.composites[full.productId]);
    const localId = 'b-' + (3000 + bookings.length);
    const localBooking = {
      id: localId,
      source: 'Portal',
      operator: venue.operator,
      venue: venue.name,
      city: venue.city,
      product: full.productId,
      productName: (venue.productNames && venue.productNames[full.productId]) || full.productId,
      customer: full.customer,
      email: full.email,
      phone: full.phone,
      notes: full.notes,
      date: full.date,
      time: full.time,
      rcvd: new Date().toISOString().slice(0, 10),
      guests: full.guests,
      status: full.type === 'request' ? 'pending' : 'confirmed',
      compositeId,
    };
    setBookings(bs => [localBooking, ...bs]);

    if (!compositeId || !liveStatus.live) return;
    const [firstName, ...rest] = (full.customer || '').split(' ');
    const lastName = rest.join(' ') || '-';
    try {
      const data = await ppCreateBooking(compositeId, {
        firstName,
        lastName,
        email: full.email,
        phone: full.phone,
        partySize: full.guests,
        date: full.date,
        time: full.time,
        type: full.type || 'book',
        notes: full.notes,
        preOrder: full.preOrder || null,
      });
      const live = data && (data.booking || data.raw);
      if (live && live.id) {
        setBookings(bs => bs.map(b => b.id === localId ? { ...localBooking, ...data.booking, id: data.booking.id || live.id } : b));
      }
    } catch (e) {
      setBookings(bs => bs.map(b => b.id === localId ? { ...b, status: 'pending', _error: String(e?.message || e) } : b));
    }
  };

  const crumbs = route.screen === 'search'   ? [{ label:'Partner' }, { label:'Search',   icon: IconSearch }]
              : route.screen === 'results'  ? [{ label:'Partner' }, { label:'Search',   icon: IconSearch }, { label:'Results' }]
              : route.screen === 'bookings' ? [{ label:'Partner' }, { label:'Bookings', icon: IconCal    }]
              : route.screen === 'apikeys'  ? [{ label:'Partner' }, { label:'API Keys', icon: IconCog   }]
              : [{ label:'Partner' }];

  const accent = t.accent || '#1c47ff';

  // Pre-auth: neutral splash, no portal chrome. 'anon' means a login redirect
  // is already in flight (see ppCheckAuth) so this is shown only momentarily.
  if (authState !== 'authed') {
    return <PortalBootSplash redirecting={authState === 'anon'} />;
  }

  return (
    <div className={"pp-app pp-density-" + (t.density || 'regular') + (railExpanded ? " is-rail-expanded" : "")}
         style={{ '--pp-accent': accent }}
         data-glow={t.glow === false ? 'off' : 'on'}
         data-screen-label={
           route.screen === 'search'   ? '01 Search landing'
         : route.screen === 'results'  ? '02 Search results'
         : route.screen === 'bookings' ? '03 Bookings list'
         : route.screen
         }>
      <Sidebar route={route} onNav={handleNav} expanded={railExpanded} onToggle={() => setRailExpanded(e => !e)}/>
      <div className="pp-main">
        <TopBar crumbs={crumbs} liveStatus={liveStatus}/>
        <div className="pp-main-body">
          {!liveStatus.ready ? (
            <LoadingSplash/>
          ) : !liveStatus.live ? (
            <DisconnectedSplash error={liveStatus.error}/>
          ) : (
            <React.Fragment>
              {route.screen === 'search'   && <SearchScreen   query={query} setQuery={setQuery} onSearch={handleSearch} onPickSuggestion={handlePickSuggestion} searching={searching}/>}
              {route.screen === 'results'  && <ResultsScreen  query={query} setQuery={setQuery} onSearch={handleSearch} onBook={handleBook} onBack={() => setRoute({ screen: 'search' })} onPickSuggestion={handlePickSuggestion} searching={searching} searchError={searchError}/>}
              {route.screen === 'bookings' && <BookingsScreen bookings={bookings} groupBy={t.groupBy} setGroupBy={(g) => setTweak('groupBy', g)} initialText={bookingsInitialText} onOpen={handleOpenBooking} onNewWalkin={() => setWalkinOpen(true)}/>}
              {route.screen === 'apikeys'  && <ApiKeysScreen keysEnabled={!!window.__pp_keys_enabled} userEmail={window.__pp_session?.email}/>}
            </React.Fragment>
          )}
        </div>
      </div>

      <BookingDrawer slot={drawerSlot}
                     onClose={() => window.history.back()}
                     onConfirm={handleConfirm}/>

      <EditBookingDrawer booking={viewingBooking}
                         onClose={() => window.history.back()}
                         onSave={handleSaveBooking}
                         onCancelBooking={handleCancelBooking}/>

      <WalkinDrawer open={walkinOpen}
                    onClose={() => window.history.back()}
                    onCreate={handleNewWalkin}/>

      <LangPicker/>

      <TweaksPanel>
        <TweakSection label="Appearance"/>
        <TweakColor  label="Accent"
                     value={t.accent}
                     options={['#1c47ff','#0b1530','#1f8a5b','#c14a3a']}
                     onChange={(v) => setTweak('accent', v)}/>
        <TweakRadio  label="Density"
                     value={t.density}
                     options={['compact','regular','comfy']}
                     onChange={(v) => setTweak('density', v)}/>
        <TweakToggle label="Search glow"
                     value={t.glow}
                     onChange={(v) => setTweak('glow', v)}/>
        <TweakSection label="Bookings list"/>
        <TweakRadio  label="Group bookings by"
                     value={t.groupBy}
                     options={[
                       { value: 'flat',     label: 'List' },
                       { value: 'operator', label: 'Operator' },
                       { value: 'product',  label: 'Product' },
                       { value: 'date',     label: 'Date' },
                     ]}
                     onChange={(v) => setTweak('groupBy', v)}/>
        <TweakSection label="Quick jump"/>
        <div style={{ display:'flex', gap:6 }}>
          <TweakButton label="Search"   onClick={() => setRoute({ screen: 'search' })}/>
          <TweakButton label="Results"  onClick={() => setRoute({ screen: 'results' })}/>
          <TweakButton label="Bookings" onClick={() => setRoute({ screen: 'bookings' })}/>
        </div>
      </TweaksPanel>
    </div>
  );
}

// Override the search glow via a tweak.
function GlowApplier({ tweak }) { return null; }

// Full-viewport splash shown before auth resolves — deliberately has NO
// sidebar/topbar so a signed-out visitor never sees the portal flash before the
// login redirect.
function PortalBootSplash({ redirecting }) {
  return (
    <div style={{ minHeight: '100vh', display: 'grid', placeItems: 'center', background: 'var(--pp-cream)' }}>
      <div className="pp-empty" style={{ border: 0, background: 'transparent' }}>
        <div className="pp-trace-loader-wrap" aria-hidden="true">
          <svg width="32" height="32" viewBox="0 0 24 24" style={{ animation: 'pp-spin 0.8s linear infinite' }}>
            <circle cx="12" cy="12" r="9" fill="none" stroke="var(--pp-line-strong)" strokeWidth="3"/>
            <path d="M21 12a9 9 0 0 0-9-9" fill="none" stroke="var(--pp-accent)" strokeWidth="3" strokeLinecap="round"/>
          </svg>
        </div>
        <div className="pp-empty-title">{redirecting ? 'Taking you to sign in…' : 'Loading…'}</div>
      </div>
    </div>
  );
}

function LoadingSplash() {
  const canvasRef = React.useRef(null);
  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const off = document.createElement('canvas');
    off.width = 64; off.height = 64;
    const offCtx = off.getContext('2d');
    const favLink = document.getElementById('pp-favicon');
    const originalFav = favLink ? favLink.getAttribute('href') : null;
    const originalTitle = document.title;
    document.title = 'Loading — Bookable';

    const img = new Image();
    let ready = false, progress = 0, raf = 0, last = performance.now(), cancelled = false;
    const ease = (t) => t * t * (3 - 2 * t);

    function drawFill(target, size) {
      target.clearRect(0, 0, size, size);
      const d = size * 0.92;
      const o = (size - d) / 2;
      target.save();
      target.globalAlpha = 0.16;
      target.drawImage(img, o, o, d, d);
      target.restore();
      const p = ease(progress);
      const revealH = size * p;
      target.save();
      target.beginPath();
      target.rect(0, size - revealH, size, revealH);
      target.clip();
      target.globalAlpha = 1;
      target.drawImage(img, o, o, d, d);
      target.restore();
    }

    function frame(now) {
      if (cancelled) return;
      const dt = Math.min((now - last) / 1000, 0.1);
      last = now;
      if (ready) {
        progress += dt * 0.5; // ~2s per fill
        if (progress >= 1) progress = 0;
        drawFill(ctx, canvas.width);
        drawFill(offCtx, off.width);
        if (favLink) favLink.href = off.toDataURL('image/png');
      }
      raf = requestAnimationFrame(frame);
    }

    img.onload = () => { ready = true; };
    img.src = '/assets/bookable-icon.png';
    raf = requestAnimationFrame((t) => { last = t; frame(t); });

    return () => {
      cancelled = true;
      cancelAnimationFrame(raf);
      document.title = originalTitle;
      if (favLink && originalFav) favLink.href = originalFav;
    };
  }, []);

  return (
    <div className="pp-empty pp-empty--loading" style={{ marginTop: 64 }}>
      <div className="pp-trace-loader-wrap" aria-hidden="true">
        <canvas ref={canvasRef} className="pp-cs-loader" width="192" height="192"/>
      </div>
      <div className="pp-empty-title">Loading Bookable…</div>
      <div className="pp-empty-sub">Pulling your live operator catalogue and bookings.</div>
    </div>
  );
}

function DisconnectedSplash({ error }) {
  return (
    <div className="pp-empty" style={{ marginTop: 64 }}>
      <div className="pp-empty-glyph">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
          <path d="M12 9v4M12 17v.01M4.93 19.07l14.14-14.14M3 12a9 9 0 1 0 18 0 9 9 0 0 0-18 0Z"/>
        </svg>
      </div>
      <div className="pp-empty-title">Can't reach Bookable</div>
      <div className="pp-empty-sub">The Bookable API isn't responding. {error ? <span className="pp-mono">{error}</span> : null}</div>
      <button className="pp-btn pp-btn--ghost" onClick={() => window.location.reload()}>Retry</button>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
