// Live data layer — loads the operator + venue catalogue, bookings, and per-
// venue availability from /api/* (which proxies the Bookable production API).
// If the API is unreachable the UI renders a disconnected state.
//
// Loaded after data.jsx so the empty arrays exist as the base shape.

window.__pp_live = { ready: false, error: null, dataLive: false };
window.__pp_slot_cache = window.__pp_slot_cache || new Map();
window.__pp_session = null;
// Auth gate state, consumed by app.jsx to avoid painting the portal chrome
// before we know whether the user is signed in:
//   'pending'  — auth not yet resolved (initial)
//   'authed'   — signed in (or auth disabled server-side)
//   'anon'     — not signed in; a redirect to login is in flight
window.__pp_auth = 'pending';
function ppSetAuth(state) {
  window.__pp_auth = state;
  try { window.dispatchEvent(new CustomEvent('pp:auth', { detail: { state } })); } catch (e) {}
}

function ppSlotKey(compositeId, date, guests) {
  return compositeId + '|' + date + '|' + guests;
}

function ppCurrentPath() {
  try { return window.location.pathname + window.location.search; }
  catch (e) { return '/'; }
}

// Bounce the browser to Auth0 login, preserving where the user was headed.
let ppRedirectingToLogin = false;
function ppRedirectToLogin() {
  if (ppRedirectingToLogin) return;
  ppRedirectingToLogin = true;
  window.location.href = '/api/auth/login?returnTo=' + encodeURIComponent(ppCurrentPath());
}

async function ppFetchJSON(url, opts) {
  const res = await fetch(url, opts);
  // A 401 anywhere means the session is missing or expired (e.g. it lapsed
  // mid-session) — send the user back through login rather than surfacing a
  // raw error into the UI.
  if (res.status === 401) {
    ppRedirectToLogin();
    const err = new Error('unauthenticated');
    err.status = 401;
    throw err;
  }
  const text = await res.text();
  let body; try { body = text ? JSON.parse(text) : null; } catch { body = null; }
  if (!res.ok) {
    const err = new Error('http_' + res.status);
    err.status = res.status; err.body = body;
    throw err;
  }
  return body;
}

async function ppLoadCatalogue() {
  const data = await ppFetchJSON('/api/venues');
  if (!data || !Array.isArray(data.operators) || !Array.isArray(data.venues)) return false;
  if (!data.operators.length && !data.venues.length) return false;

  // Mutate the arrays in place so React closures keep working.
  window.OPERATORS.length = 0;
  data.operators.forEach(o => window.OPERATORS.push(o));
  window.VENUES.length = 0;
  data.venues.forEach(v => window.VENUES.push(v));

  // Rebuild CITIES from venue list — alphabetical, with the "All locations"
  // reset option pinned at the top.
  const cities = new Set();
  data.venues.forEach(v => { if (v.city) cities.add(v.city); });
  window.CITIES.length = 0;
  window.CITIES.push('All locations');
  [...cities].sort((a, b) => a.localeCompare(b, 'en-GB')).forEach(c => window.CITIES.push(c));

  return true;
}

async function ppLoadBookings() {
  const data = await ppFetchJSON('/api/bookings');
  if (!data || !Array.isArray(data.bookings)) return null;
  return data.bookings;
}

async function ppLoadAvailability(compositeId, date, guests) {
  if (!compositeId) return [];
  const key = ppSlotKey(compositeId, date, guests);
  if (window.__pp_slot_cache.has(key)) return window.__pp_slot_cache.get(key);

  const url = `/api/availability?compositeId=${encodeURIComponent(compositeId)}&date=${date}&partySize=${guests}`;
  try {
    const data = await ppFetchJSON(url);
    const slots = (data && Array.isArray(data.slots)) ? data.slots : [];
    window.__pp_slot_cache.set(key, slots);
    return slots;
  } catch (e) {
    window.__pp_slot_cache.set(key, []);
    return [];
  }
}

async function ppCreateBooking(compositeId, payload) {
  return ppFetchJSON('/api/bookings', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ compositeId, ...payload }),
  });
}

async function ppUpdateBooking(bookingId, patchOps) {
  return ppFetchJSON('/api/booking?id=' + encodeURIComponent(bookingId), {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(patchOps),
  });
}

async function ppCancelBooking(bookingId) {
  return ppFetchJSON('/api/booking?id=' + encodeURIComponent(bookingId), { method: 'DELETE' });
}

// ── Integration-partner API keys (production) ───────────────────────────────
// Sandbox keys are demo-only (generated client-side) until a real sandbox API
// exists; these three only ever talk to the production keys endpoint.
async function ppLoadPartnerKeys() {
  const data = await ppFetchJSON('/api/partner-keys');
  return (data && Array.isArray(data.keys)) ? data.keys : [];
}

async function ppCreatePartnerKey(payload, rotateOf) {
  const qs = rotateOf ? '?rotate=' + encodeURIComponent(rotateOf) : '';
  return ppFetchJSON('/api/partner-keys' + qs, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload || {}),
  });
}

async function ppDeletePartnerKey(clientId) {
  return ppFetchJSON('/api/partner-keys?clientId=' + encodeURIComponent(clientId), { method: 'DELETE' });
}

// Natural-language search — POSTs the agent's query to /api/search which
// passes it through Claude and returns structured filters (+ resolved
// near-by coordinates for any UK postcode it found).
async function ppAiSearch(query) {
  const today = new Date().toISOString().slice(0, 10);
  return ppFetchJSON('/api/search', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, today }),
  });
}

// React hook — pulls live slots from /api/availability. Returns null while the
// request is in flight so the caller can render a loading state; returns [] if
// the venue × product has no availability for the date.
//
// Multiple Bookable products can collapse into a single portal bucket (e.g.
// "BOTTOMLESS BEFORE 2PM" + "BOTTOMLESS AFTER 2PM" → bottomless-brunch).
// Fetch every composite in the bucket in parallel and merge results so the
// card surfaces the union of available times, with each slot carrying the
// compositeId it came from for use when booking.
function usePpAvailability(venue, productId, date, guests) {
  const compositesList = (venue && venue.compositesByProduct && venue.compositesByProduct[productId]) || null;
  const fallbackComposite = venue && venue.composites && venue.composites[productId];
  const composites = compositesList && compositesList.length ? compositesList
                   : fallbackComposite ? [fallbackComposite]
                   : [];
  const key = composites.join('|');
  const [slots, setSlots] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    if (!composites.length) { setSlots([]); return; }
    setSlots(null);
    Promise.all(composites.map(cid => ppLoadAvailability(cid, date, guests)))
      .then(results => {
        if (cancelled) return;
        const byTime = new Map();
        for (let i = 0; i < results.length; i++) {
          const cid = composites[i];
          for (const slot of (results[i] || [])) {
            const stamped = slot.compositeId ? slot : { ...slot, compositeId: cid };
            const existing = byTime.get(stamped.time);
            // Prefer instant-book over request; otherwise keep first seen.
            if (!existing || (existing.type === 'request' && stamped.type !== 'request')) {
              byTime.set(stamped.time, stamped);
            }
          }
        }
        setSlots([...byTime.values()].sort((a, b) => a.time.localeCompare(b.time)));
      });
    return () => { cancelled = true; };
  }, [key, productId, date, guests, venue?.id]);
  return slots;
}

// Multi-product variant of usePpAvailability. Resolves slots for every product
// in `productIds` concurrently so a venue card can render a switcher (chip per
// product) without re-fetching when the selection changes. Returns an object
// keyed by productId; values are null while loading, [] when no slots.
function useAllProductsAvailability(venue, productIds, date, guests, enabled = true) {
  const ids = (productIds || []).filter(Boolean);
  const key = (enabled ? '1' : '0') + '|' + (venue && venue.id) + '|' + ids.join(',') + '|' + date + '|' + guests;
  const [byProduct, setByProduct] = React.useState(() =>
    ids.reduce((acc, pid) => { acc[pid] = null; return acc; }, {})
  );
  React.useEffect(() => {
    let cancelled = false;
    // No availability fetch until the partner explicitly searches — browsing a
    // filtered listing should show venues without hitting the slots API.
    if (!enabled) { setByProduct({}); return; }
    if (!venue || !ids.length) { setByProduct({}); return; }
    setByProduct(ids.reduce((acc, pid) => { acc[pid] = null; return acc; }, {}));
    ids.forEach(pid => {
      const compositesList = (venue.compositesByProduct && venue.compositesByProduct[pid]) || null;
      const fallback = venue.composites && venue.composites[pid];
      const composites = compositesList && compositesList.length ? compositesList
                       : fallback ? [fallback]
                       : [];
      if (!composites.length) {
        if (!cancelled) setByProduct(prev => ({ ...prev, [pid]: [] }));
        return;
      }
      Promise.all(composites.map(cid => ppLoadAvailability(cid, date, guests)))
        .then(results => {
          if (cancelled) return;
          const byTime = new Map();
          for (let i = 0; i < results.length; i++) {
            const cid = composites[i];
            for (const slot of (results[i] || [])) {
              const stamped = slot.compositeId ? slot : { ...slot, compositeId: cid };
              const existing = byTime.get(stamped.time);
              if (!existing || (existing.type === 'request' && stamped.type !== 'request')) {
                byTime.set(stamped.time, stamped);
              }
            }
          }
          const sorted = [...byTime.values()].sort((a, b) => a.time.localeCompare(b.time));
          setByProduct(prev => ({ ...prev, [pid]: sorted }));
        });
    });
    return () => { cancelled = true; };
  }, [key]);
  return byProduct;
}

// Confirm the session before loading any data. Returns false if we've kicked
// off a redirect to login (caller should abort boot). When auth is disabled
// server-side, this resolves true with a null session.
async function ppCheckAuth() {
  let res;
  try {
    res = await fetch('/api/auth/me', { headers: { Accept: 'application/json' } });
  } catch (e) {
    // Network blip reaching our own function — let boot continue and surface
    // the usual Disconnected state rather than trapping the user.
    ppSetAuth('authed');
    return true;
  }
  if (res.status === 401) {
    // Not signed in — mark anon so the app stays on the boot splash (no portal
    // chrome) while we redirect to login.
    ppSetAuth('anon');
    ppRedirectToLogin();
    return false;
  }
  try {
    const me = await res.json();
    window.__pp_session = me && me.user ? me.user : null;
    // Whether this user may manage integration-partner API keys.
    window.__pp_keys_enabled = !!(me && me.keysEnabled);
  } catch (e) { window.__pp_session = null; window.__pp_keys_enabled = false; }
  ppSetAuth('authed');
  return true;
}

// Boot — fire and forget. Front-end re-renders when the event fires.
(async function ppBoot() {
  try {
    const authed = await ppCheckAuth();
    if (!authed) return; // redirecting to login; stop here

    const catalogue = await ppLoadCatalogue();
    if (catalogue) window.__pp_live.dataLive = true;

    // Build a compositeId → venue lookup for resolving booking venue names.
    const compToVenue = new Map();
    for (const v of window.VENUES) {
      if (!v.composites) continue;
      for (const cid of Object.values(v.composites)) compToVenue.set(cid, v);
    }

    const bookings = await ppLoadBookings();
    if (Array.isArray(bookings)) {
      const enriched = bookings.map(b => {
        if (!b.compositeId) return b;
        const v = compToVenue.get(b.compositeId);
        if (!v) return b;
        return {
          ...b,
          venue: v.name,
          venueId: v.id,
          venuePhoto: v.photos && v.photos[0],
          operator: v.operator || b.operator,
          city: v.city || b.city,
          area: v.area || b.area,
        };
      });
      window.SEED_BOOKINGS.length = 0;
      enriched.forEach(b => window.SEED_BOOKINGS.push(b));
      window.__pp_live.bookingsLoaded = true;
    }
  } catch (e) {
    window.__pp_live.error = e?.message || String(e);
    console.error('[Bookable] live data unavailable', e);
  } finally {
    window.__pp_live.ready = true;
    window.dispatchEvent(new CustomEvent('pp:data-loaded', { detail: { live: window.__pp_live.dataLive } }));
  }
})();

Object.assign(window, {
  ppLoadAvailability,
  ppCreateBooking,
  ppUpdateBooking,
  ppCancelBooking,
  ppLoadPartnerKeys,
  ppCreatePartnerKey,
  ppDeletePartnerKey,
  ppAiSearch,
  usePpAvailability,
  useAllProductsAvailability,
});
