// App.jsx — router + tweaks
const { useState, useEffect } = React;

// ─── Error Boundary ───────────────────────────────────────────────
// React-class component (the only way error boundaries work — hooks
// don't expose componentDidCatch). Wraps the whole app so any thrown
// render error shows a readable diagnostic instead of unmounting the
// tree and leaving the user staring at a blank white page.
// Reload button gives a recovery path without dev-tools.
class AppErrorBoundary extends React.Component {
  constructor(p) { super(p); this.state = { err: null, info: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) {
    this.setState({ info });
    console.error('[Venoraa] React tree crashed:', err);
    console.error('[Venoraa] component stack:', info?.componentStack);
  }
  render() {
    if (this.state.err) {
      return (
        <div style={{
          minHeight: '100vh', display: 'flex', alignItems: 'center',
          justifyContent: 'center', padding: '40px',
          background: 'var(--cream, #f6f3ee)',
          fontFamily: 'var(--sans, system-ui, sans-serif)',
        }}>
          <div style={{
            maxWidth: '560px', width: '100%', background: 'white',
            padding: '40px', border: '1px solid var(--cream-deep, #e0d9cc)',
          }}>
            <p style={{
              fontSize: '11px', letterSpacing: '0.16em', textTransform: 'uppercase',
              color: 'var(--terracotta, #b0552a)', marginBottom: '12px',
            }}>
              Something went wrong
            </p>
            <h1 style={{
              fontFamily: 'var(--serif, Georgia, serif)', fontWeight: 300,
              fontSize: '30px', marginBottom: '14px',
            }}>
              Page failed to load
            </h1>
            <p style={{ fontSize: '14px', lineHeight: 1.55, color: 'var(--dark-mid, #555)', marginBottom: '20px' }}>
              The browser hit an error while rendering this page. Reloading usually fixes it — if you've just received a code update, do a hard refresh (Ctrl + Shift + R).
            </p>
            <pre style={{
              fontFamily: 'ui-monospace, Menlo, monospace', fontSize: '11px',
              background: 'var(--cream, #f6f3ee)', padding: '12px',
              border: '1px solid var(--cream-deep, #e0d9cc)',
              maxHeight: '200px', overflow: 'auto',
              marginBottom: '20px', whiteSpace: 'pre-wrap',
            }}>
              {String(this.state.err?.message || this.state.err || 'Unknown error')}
            </pre>
            <button
              onClick={() => { this.setState({ err: null, info: null }); window.location.reload(); }}
              style={{
                background: 'var(--dark, #111)', color: 'white',
                border: 'none', padding: '12px 24px', cursor: 'pointer',
                fontFamily: 'var(--sans, system-ui, sans-serif)', fontSize: '12px',
                letterSpacing: '0.14em', textTransform: 'uppercase',
              }}
            >
              Reload page
            </button>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

// ─── Toast ────────────────────────────────────────────────────────
// Minimal global notice ribbon. Reads `notice` off the account store
// and renders a dark pill at the bottom-centre of the viewport.
// Auto-dismiss happens server-side (setNotice in AccountStore sets a
// 4.5s timer); this component just renders what's there. CSS animates
// in/out smoothly so a brief hop in/out doesn't feel jarring.
function Toast() {
  const { notice } = useAccount();
  return (
    <div
      aria-live="polite"
      role="status"
      style={{
        position: 'fixed',
        bottom: '24px',
        left: '50%',
        transform: `translateX(-50%) translateY(${notice ? '0' : '24px'})`,
        zIndex: 9999,
        opacity: notice ? 1 : 0,
        transition: 'opacity 0.25s ease, transform 0.25s ease',
        pointerEvents: notice ? 'auto' : 'none',
        maxWidth: 'calc(100vw - 32px)',
      }}
    >
      <div style={{
        background: 'var(--dark)',
        color: 'white',
        padding: '12px 18px',
        fontFamily: 'var(--sans)',
        fontSize: '13px',
        lineHeight: 1.4,
        letterSpacing: '0.01em',
        boxShadow: '0 14px 36px rgba(15,15,15,0.32)',
        borderRadius: 0,
      }}>
        {notice}
      </div>
    </div>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accentColor": "terracotta",
  "heroFont": "serif",
  "density": "comfortable"
}/*EDITMODE-END*/;

const ACCENT_MAP = {
  terracotta: { main: 'oklch(58% 0.13 40)', light: 'oklch(72% 0.09 42)' },
  sage:       { main: 'oklch(55% 0.09 155)', light: 'oklch(68% 0.06 155)' },
  slate:      { main: 'oklch(45% 0.04 240)', light: 'oklch(62% 0.04 240)' },
};

function App() {
  const [tweaks, setTweaks] = useState(() => {
    try { return { ...TWEAK_DEFAULTS, ...JSON.parse(localStorage.getItem('venoraa-tweaks') || '{}') }; } catch { return TWEAK_DEFAULTS; }
  });
  const [tweaksOpen, setTweaksOpen] = useState(false);
  // Initial page is derived from (in priority order):
  //   1. The URL hash (e.g. /#collections, /#admin) — supports deep links
  //   2. localStorage venoraa-page — last visited page on refresh
  //   3. 'home' as fallback
  const [page, setPage] = useState(() => {
    if (typeof window !== 'undefined') {
      // Strip any query string off the hash before treating it as a
      // page name — links like `#admin?magic=<token>` (the magic-link
      // sign-in URL) carry their own payload after a `?` inside the
      // fragment. Without the split, page would be 'admin?magic=…',
      // which never matches `case 'admin'` and the user gets bounced
      // to the home page instead of the admin login.
      const hash = window.location.hash.slice(1).split('?')[0];
      if (hash) return hash;
    }
    return localStorage.getItem('venoraa-page') || 'home';
  });
  const [pageData, setPageData] = useState(null);

  // Browser-history integration — every navigate() pushes a real
  // history entry so the browser's Back/Forward buttons work the way
  // the user expects (instead of bouncing them out of the site). On
  // mount, we replaceState the current page so initial Back returns
  // to the previous site rather than reloading. popstate listens for
  // back/forward and restores the page + data from the history entry.
  useEffect(() => {
    // Seed the initial entry with the current page so popstate has a
    // state object to restore to when the user navigates back to the
    // first page they landed on.
    if (!history.state || !history.state.__venoraa) {
      // Preserve whatever's already in the URL hash — `window.location
      // .hash` keeps the full thing (including any `?magic=…` payload
      // tagged on by the magic-link sign-in flow), which the Admin
      // component needs to read on mount. Falling back to `'#' + page`
      // only when there's no hash means a fresh visit to "/" still
      // gets a clean seed entry.
      history.replaceState(
        { __venoraa: true, page, data: null },
        '',
        window.location.hash || ('#' + page)
      );
    }
    const onPop = (e) => {
      if (e.state && e.state.__venoraa) {
        setPage(e.state.page || 'home');
        setPageData(e.state.data || null);
      } else {
        // Fall back to hash → page name. Same `?…`-stripping logic
        // as the initial state seed above.
        const hash = window.location.hash.slice(1).split('?')[0];
        setPage(hash || 'home');
        setPageData(null);
      }
    };
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []); // run once on mount

  // Apply tweaks to CSS vars
  useEffect(() => {
    const accent = ACCENT_MAP[tweaks.accentColor] || ACCENT_MAP.terracotta;
    document.documentElement.style.setProperty('--terracotta', accent.main);
    document.documentElement.style.setProperty('--terracotta-light', accent.light);
    if (tweaks.density === 'compact') {
      document.documentElement.style.setProperty('--nav-h', '56px');
    } else {
      document.documentElement.style.setProperty('--nav-h', '72px');
    }
  }, [tweaks]);

  const navigate = (to, data = null) => {
    // Browser back — defer to native history.back so the URL + state
    // unwind correctly. Our popstate listener catches it and updates
    // page + data from the previous history entry.
    if (to === 'back') {
      history.back();
      return;
    }
    window.scrollTo({ top: 0 });
    setPage(to);
    setPageData(data);
    if (to !== 'admin') localStorage.setItem('venoraa-page', to);
    // Push a real history entry so the browser's Back/Forward buttons
    // can replay this navigation. State carries the page id + the data
    // payload (filter spec, tile object, …) so we can restore exactly
    // what was rendered. Wrapped in try/catch because non-serialisable
    // payloads (rare but possible) would otherwise throw.
    try {
      history.pushState({ __venoraa: true, page: to, data }, '', '#' + to);
    } catch {
      history.pushState({ __venoraa: true, page: to, data: null }, '', '#' + to);
    }
  };

  const applyTweak = (key, val) => {
    const next = { ...tweaks, [key]: val };
    setTweaks(next);
    localStorage.setItem('venoraa-tweaks', JSON.stringify(next));
    window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: val } }, '*');
  };

  // Tweaks host protocol
  useEffect(() => {
    const handler = (e) => {
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', handler);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', handler);
  }, []);

  // ─── Site-wide UI state: tile catalogue + drawers ────────────────────
  const [tiles, setTiles] = useState([]);
  useEffect(() => {
    fetch('/api/tiles').then(r => r.json()).then(d => setTiles(d.tiles || [])).catch(() => {});
  }, []);
  const tileMap = React.useMemo(() => {
    const m = {}; tiles.forEach(t => { m[t.id] = t; }); return m;
  }, [tiles]);

  const [searchOpen, setSearchOpen] = useState(false);
  const [cartOpen, setCartOpen] = useState(false);
  // tryOn / Visualiser feature was removed — see commit "delete AI Studio".
  // window.QW_tryOn is intentionally not provided so the older
  // Collections "Try on in your room" button no-ops if it survives.

  // ─── Phone vs Desktop UI router ─────────────────────────────────
  // Phone-native components ship their own layouts and don't share
  // markup with the desktop versions. The cutoff at 760px catches
  // every phone in portrait + landscape and lets tablets keep the
  // desktop UI (they have room for it). Reacts to resize so an
  // iPad rotating from landscape→portrait flips automatically.
  const [isPhone, setIsPhone] = useState(() =>
    typeof window !== 'undefined' && window.innerWidth <= 760
  );
  useEffect(() => {
    const on = () => setIsPhone(window.innerWidth <= 760);
    window.addEventListener('resize', on, { passive: true });
    return () => window.removeEventListener('resize', on);
  }, []);

  useEffect(() => {
    window.QW_openCart = () => setCartOpen(true);
    window.QW_openSearch = () => setSearchOpen(true);
    window.QW_nav = (to, data) => { setPage(to); setPageData(data || null); };
  }, []);

  // Footer is hidden on Admin. (AI Studio was also hidden when active,
  // but the Studio is archived for now — see _archived/ai-studio/.)
  const showFooter = page !== 'admin';

  // Pick the right component per page based on viewport. Phone-only
  // components live on `window.PhoneHome`, `window.PhoneListing`, etc.
  // (defined in Phone.jsx). When unavailable, we fall back to the
  // desktop component — so we can roll out phone pages incrementally.
  const pick = (PhoneCmp, DesktopCmp) =>
    (isPhone && PhoneCmp) ? PhoneCmp : DesktopCmp;

  // The 'product' route needs its tile payload in `pageData`. That
  // payload only lives in memory, so it's gone after a browser refresh,
  // when someone opens a shared "#product" link, or when a history
  // entry's state was dropped. In those cases the switch below falls
  // back to rendering the collection LISTING — so the nav has to be
  // told it's on a listing too, otherwise it keeps the product page's
  // hero chrome (transparent bar + white-text logo) and the logo turns
  // invisible against the white listing.
  const navPage = (page === 'product' && !pageData) ? 'collections' : page;

  const renderPage = () => {
    switch (page) {
      case 'home': {
        const Cmp = pick(window.PhoneHome, Home);
        return <Cmp navigate={navigate}/>;
      }
      case 'collections':
      case 'collection': {
        const Cmp = pick(window.PhoneListing, CollectionDetail);
        return <Cmp filter={pageData} navigate={navigate}/>;
      }
      case 'collections-hub':  return <CollectionsHub navigate={navigate}/>;
      case 'collections-axis': return <CollectionsAxis axis={pageData || 'room'} navigate={navigate}/>;
      case 'product': {
        if (!pageData) {
          const Cmp = pick(window.PhoneListing, CollectionDetail);
          return <Cmp navigate={navigate}/>;
        }
        const Cmp = pick(window.PhoneProduct, Product);
        return <Cmp product={pageData} navigate={navigate}/>;
      }
      case 'journal': {
        const Cmp = pick(window.PhoneJournal, Journal);
        return <Cmp navigate={navigate} pageData={pageData}/>;
      }
      case 'sample':
      case 'quote': {
        const Cmp = pick(window.PhoneQuote, Quote);
        return <Cmp navigate={navigate} prefill={pageData}/>;
      }
      case 'trade': {
        // Trade enquiry — separate form, separate inbox subject so
        // the studio can sort residential vs trade leads.
        return window.Trade ? <window.Trade navigate={navigate}/> : null;
      }
      case 'privacy': {
        // Internal privacy notice (mirrors the legal text the
        // registered entity, The Tiles Company Ltd, used to publish
        // on the previous site). Linked from the footer's legal bar.
        return window.Privacy ? <window.Privacy navigate={navigate}/> : null;
      }
      // Admin is desktop-only by design — the catalogue table doesn't
      // fit a 360px screen. Phone users see the desktop layout (the
      // login form + edit form are responsive enough on their own).
      case 'admin':            return <Admin navigate={navigate}/>;
      // 'studio' (AI Visualiser) route archived — see
      // _archived/ai-studio/ for the component and the restoration
      // README. Server endpoints remain in server.js but are dormant
      // without a UI to call them.
      case 'account': {
        const Cmp = pick(window.PhoneAccount, Account);
        return <Cmp navigate={navigate} tiles={tiles}/>;
      }
      default: {
        const Cmp = pick(window.PhoneHome, Home);
        return <Cmp navigate={navigate}/>;
      }
    }
  };

  return (
    <>
      {/* Cursor: OS cursor (declared in index.html as `cursor: url(...)`)
          is the always-on base layer. Cursor.jsx mounts a JS dot + ring
          ON TOP for the visual flourish — and only then sets `cursor:
          none` via the .qw-cursor-on class. If the JS cursor ever
          fails or unmounts, the OS cursor is still there. Never
          disappears. */}
      <Cursor/>
      {/* Desktop nav. Phone pages render their own sticky header so
          the global Nav hides on phones to avoid double-stacking.
          `navPage` (not `page`) is passed deliberately — see the
          effectivePage note above renderPage(). */}
      {!isPhone && <Nav currentPage={navPage} navigate={navigate} onOpenCart={() => setCartOpen(true)} onOpenSearch={() => setSearchOpen(true)}/>}
      <main>{renderPage()}</main>
      {showFooter && !isPhone && <Footer navigate={navigate}/>}
      <CartDrawer open={cartOpen} onClose={() => setCartOpen(false)} tileMap={tileMap} navigate={navigate}/>
      <SearchOverlay open={searchOpen} onClose={() => setSearchOpen(false)} tiles={tiles} navigate={navigate}/>
      <DesignAssistant/>
      {/* Global toast — surfaces brief user-facing notices from the
          account store (e.g. "selection is full" when the favourites
          cap is hit). Auto-dismisses; lives outside the drawer so
          customers see it even when adding favourites from the
          listing / product page. */}
      <Toast/>

      {/* Tweaks panel */}
      {tweaksOpen && (
        <div style={{
          position: 'fixed', bottom: '24px', left: '24px', zIndex: 300,
          background: 'var(--cream)', border: '1px solid var(--cream-deep)',
          boxShadow: '0 16px 48px rgba(0,0,0,0.12)',
          padding: '24px', width: '260px',
        }}>
          <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', fontWeight: 500, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--dark)', marginBottom: '20px' }}>Tweaks</p>

          <div style={{ marginBottom: '20px' }}>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '10px' }}>Accent colour</p>
            <div style={{ display: 'flex', gap: '8px' }}>
              {Object.keys(ACCENT_MAP).map(k => (
                <button key={k} onClick={() => applyTweak('accentColor', k)} style={{
                  width: '28px', height: '28px', borderRadius: '50%',
                  background: ACCENT_MAP[k].main,
                  border: tweaks.accentColor === k ? '3px solid var(--dark)' : '3px solid transparent',
                  cursor: 'pointer', outline: '2px solid var(--cream)',
                  outlineOffset: '-4px',
                }}/>
              ))}
            </div>
          </div>

          <div style={{ marginBottom: '20px' }}>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '10px' }}>Density</p>
            <div style={{ display: 'flex', gap: '8px' }}>
              {['comfortable', 'compact'].map(d => (
                <button key={d} onClick={() => applyTweak('density', d)} style={{
                  flex: 1, padding: '8px', border: '1px solid',
                  borderColor: tweaks.density === d ? 'var(--dark)' : 'var(--cream-deep)',
                  background: tweaks.density === d ? 'var(--dark)' : 'transparent',
                  color: tweaks.density === d ? 'var(--cream)' : 'var(--dark-mid)',
                  cursor: 'pointer', fontFamily: 'var(--sans)', fontSize: '11px',
                  letterSpacing: '0.08em', textTransform: 'capitalize',
                }}>{d}</button>
              ))}
            </div>
          </div>

          <div>
            <p style={{ fontFamily: 'var(--sans)', fontSize: '11px', color: 'var(--dark-mid)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: '10px' }}>Navigate to</p>
            <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
              {[['home','Home'], ['collections','Collections'], ['journal','Journal'], ['quote','Request a quote'], ['admin','Admin']].map(([p, label]) => (
                <button key={p} onClick={() => navigate(p)} style={{
                  background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left',
                  fontFamily: 'var(--sans)', fontSize: '12px', padding: '4px 0',
                  color: page === p ? 'var(--dark)' : 'var(--dark-mid)',
                  fontWeight: page === p ? 500 : 300,
                }}>{label} {page === p ? '←' : ''}</button>
              ))}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

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

