// Single-profile client store. Backs to /api/account on the server.
// Exposes window.QW_account with a useAccount() React hook.
const { useEffect, useState } = React;

// Hard ceiling on per-visitor favourites. 50 is generously above any
// realistic single-project selection and exists only to stop the
// unbounded-growth case (bots, accidental clicks, scripts). When a
// visitor tries to add past this limit, toggleFavourite() refuses
// and surfaces a `notice` that the App-level <Toast> renders.
const MAX_FAVOURITES = 50;

const listeners = new Set();
let state = {
  profile: null,
  rooms: [],
  favourites: [],
  cart: [],
  visualisations: [],
  loaded: false,
  // Latest user-facing message (e.g. "selection is full"). Cleared
  // automatically after a few seconds by a setTimeout in setNotice().
  notice: '',
};

function emit() { for (const fn of listeners) fn(); }
function setState(patch) { state = { ...state, ...patch }; emit(); }

// Push a transient message into state. The Toast component re-renders
// to show it; this timer wipes it 4s later so the next interaction
// doesn't see a stale notice.
let noticeTimer = null;
function setNotice(text) {
  setState({ notice: text });
  if (noticeTimer) clearTimeout(noticeTimer);
  if (text) noticeTimer = setTimeout(() => setState({ notice: '' }), 4500);
}

async function refresh() {
  try {
    const r = await fetch('/api/account');
    const d = await r.json();
    setState({
      profile: d.profile || null,
      rooms: d.rooms || [],
      favourites: d.favourites || [],
      cart: d.cart || [],
      visualisations: d.visualisations || [],
      loaded: true,
    });
  } catch {}
}

async function uploadRoom(file, label) {
  const fd = new FormData();
  fd.append('image', file);
  if (label) fd.append('label', label);
  const r = await fetch('/api/account/rooms', { method: 'POST', body: fd });
  const d = await r.json();
  if (d.room) setState({ rooms: [...state.rooms, d.room] });
  return d.room;
}

async function deleteRoom(id) {
  await fetch(`/api/account/rooms/${id}`, { method: 'DELETE' });
  setState({ rooms: state.rooms.filter(r => r.id !== id) });
}

async function toggleFavourite(tileId) {
  // Cap check — only applies to ADDS. Removes are always allowed
  // so a visitor at the limit can still un-heart to make room.
  const original = state.favourites || [];
  const alreadyFaved = original.includes(tileId);
  if (!alreadyFaved && original.length >= MAX_FAVOURITES) {
    setNotice(`You've hit the limit (${MAX_FAVOURITES} tiles). Submit a quote or remove some to add more.`);
    return { ok: false, error: 'cap' };
  }

  // OPTIMISTIC UPDATE — flip local state IMMEDIATELY so the UI
  // (nav heart badge, drawer list, every tile card's heart icon)
  // reacts on the same animation frame as the click. Without this,
  // the user has to wait for the network round-trip before anything
  // visibly changes — which on a slow connection looks broken (the
  // badge appears to "stick").
  //
  // We hold onto `original` so we can ROLL BACK if the server call
  // fails. The server's response is the source of truth; the
  // optimistic state is overwritten with whatever the server
  // returns on success.
  const optimisticFavs = alreadyFaved
    ? original.filter(id => id !== tileId)
    : [...original, tileId];
  setState({ favourites: optimisticFavs });

  try {
    const r = await fetch('/api/account/favourites/toggle', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tileId }),
    });
    if (!r.ok) throw new Error('toggle HTTP ' + r.status);
    const d = await r.json();
    // Reconcile with the server's authoritative list (in case it
    // ordered, deduped, or capped differently to us).
    setState({ favourites: d.favourites || [] });
    return { ok: true };
  } catch (err) {
    // Network failure / server error — roll back to what the user
    // had before they clicked, and surface a notice so they know
    // their action didn't stick.
    console.error('[favourites] toggle failed, rolling back:', err);
    setState({ favourites: original });
    setNotice(`Couldn't update favourites. Check your connection and try again.`);
    return { ok: false, error: 'network' };
  }
}

// Debounced cart save — every keystroke in the favourites drawer's
// size/area/qty inputs updates local state instantly (so the UI feels
// responsive), but the network PUT only fires after the visitor has
// been idle for 450ms. Result: typing "12.5" sends ONE request
// instead of four; mass-editing 10 tiles sends ~10 instead of ~50.
// Also flushes on page visibility change so a tab-switch doesn't
// strand the latest edit unsaved.
let cartSaveTimer = null;
let cartPending = null;
const CART_SAVE_DEBOUNCE_MS = 450;

async function flushCart() {
  if (!cartPending) return;
  const cart = cartPending;
  cartPending = null;
  try {
    await fetch('/api/account/cart', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cart }),
    });
  } catch {}
}
function saveCart(cart) {
  setState({ cart });             // mirror to local state right away
  cartPending = cart;              // remember the latest desired value
  if (cartSaveTimer) clearTimeout(cartSaveTimer);
  cartSaveTimer = setTimeout(flushCart, CART_SAVE_DEBOUNCE_MS);
}
// Belt-and-braces: flush any pending cart save when the tab goes
// to background or unloads, so a tab-switch doesn't lose the last
// few keystrokes.
if (typeof window !== 'undefined') {
  window.addEventListener('beforeunload', flushCart);
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') flushCart();
  });
}

function addToCart(tileId, sqm = 5) {
  const existing = state.cart.find(i => i.tileId === tileId);
  let next;
  if (existing) {
    next = state.cart.map(i => i.tileId === tileId ? { ...i, sqm: (i.sqm || 0) + sqm } : i);
  } else {
    next = [...state.cart, { tileId, sqm, addedAt: new Date().toISOString() }];
  }
  saveCart(next);
}

function updateCartItem(tileId, patchOrSqm) {
  // Backwards-compat: callers used to pass a raw sqm number. Newer
  // callers pass a partial patch — { size, sqm, qty } — so the
  // favourites drawer's three-field input can update any one of
  // them without clobbering the others.
  const patch = (patchOrSqm && typeof patchOrSqm === 'object')
    ? patchOrSqm
    : { sqm: patchOrSqm };

  // Normalise individual fields. Missing keys keep the existing value.
  const norm = {};
  if ('sqm'  in patch) norm.sqm  = Math.max(0, Number(patch.sqm)  || 0);
  if ('qty'  in patch) norm.qty  = Math.max(0, Math.floor(Number(patch.qty) || 0));
  if ('size' in patch) norm.size = patch.size ? String(patch.size).trim() : '';

  const exists = state.cart.some(i => i.tileId === tileId);
  let next;
  if (exists) {
    next = state.cart.map(i =>
      i.tileId === tileId ? { ...i, ...norm } : i
    );
  } else {
    // First time we see this tile — create a row from the patch
    // alone. Defaults fill the unsupplied fields.
    next = [...state.cart, {
      tileId,
      size: norm.size || '',
      sqm:  norm.sqm  || 0,
      qty:  norm.qty  || 0,
      addedAt: new Date().toISOString(),
    }];
  }
  // Drop entries that are now empty across all three fields — the
  // user has effectively cleared the row. Server applies the same
  // filter, but we drop client-side too so the UI updates instantly.
  next = next.filter(i => (i.sqm > 0) || (i.qty > 0) || i.size);
  saveCart(next);
}

function removeFromCart(tileId) {
  saveCart(state.cart.filter(i => i.tileId !== tileId));
}

async function saveVisualisation(payload) {
  try {
    const r = await fetch('/api/account/visualisations', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    const d = await r.json();
    if (d.visualisation) setState({ visualisations: [d.visualisation, ...state.visualisations] });
    return d.visualisation;
  } catch { return null; }
}

function useAccount() {
  const [, tick] = useState(0);
  useEffect(() => {
    const fn = () => tick(n => n + 1);
    listeners.add(fn);
    if (!state.loaded) refresh();
    return () => { listeners.delete(fn); };
  }, []);
  return {
    ...state,
    MAX_FAVOURITES,                                      // 50; UI shows "X / 50"
    isCartFull: (state.favourites || []).length >= MAX_FAVOURITES,
    refresh,
    uploadRoom,
    deleteRoom,
    toggleFavourite,
    addToCart,
    updateCartItem,
    removeFromCart,
    saveVisualisation,
    setNotice,
  };
}

Object.assign(window, {
  QW_account: { state, refresh, uploadRoom, deleteRoom, toggleFavourite, addToCart, updateCartItem, removeFromCart, saveVisualisation, setNotice, MAX_FAVOURITES },
  useAccount,
});
