const StoreCtx = React.createContext(null);
window.StoreCtx = StoreCtx;

// ─── URL ↔ route mapping ──────────────────────────────────────────────────
// Single source of truth so a buyer can share a link to /scripts/used-car-
// dealership (or /free/multijob) and land on the same page. Worker's
// ASSETS binding has not_found_handling: single-page-application set, so
// any deep path that doesn't match a static file falls back to index.html.
const SIMPLE_ROUTES = ["home", "scripts", "free", "about", "terms", "checkout"];

function stateFromPath(pathname) {
  const parts = (pathname || "/").split("/").filter(Boolean);
  if (parts.length === 0) return { route: "home", slug: null };
  const [first, second] = parts;
  if (first === "scripts" && second) return { route: "product", slug: second };
  if (first === "free" && second) return { route: "free-product", slug: second };
  if (SIMPLE_ROUTES.includes(first)) return { route: first, slug: null };
  return { route: "home", slug: null };
}

function pathFromState(route, slug) {
  if (route === "home") return "/";
  if (route === "product" && slug) return `/scripts/${slug}`;
  if (route === "free-product" && slug) return `/free/${slug}`;
  return `/${route}`;
}

// Canonical production origin — used for SEO canonical/og:url + og:image so a
// staging / workers.dev host doesn't self-canonicalize to the wrong domain.
// ⚠ Replace with the real production domain (keep in sync with index.html /
//   robots.txt / sitemap.xml / llms.txt).
const SITE_ORIGIN = "https://linterror.com";

// isLive(p) — single source of truth for "buyable", defined in data.jsx (loads
// early) so the cart guard, rehydration filter, totals, and home counts agree.

// Tebex sends buyers back to complete_url / cancel_url after payment. Those URLs
// point at "/" (see tebex.jsx), so without this the marker would land on the
// home route and the checkout success screen (which clears the cart + basket)
// would never render. If a ?checkout= marker is present on load, force the
// checkout route so pages-checkout.jsx can handle it — but don't hijack a deep
// link that already resolves to a real page.
function initialRoute() {
  const base = stateFromPath(window.location.pathname);
  const marker = new URLSearchParams(window.location.search).get("checkout");
  if (marker && base.route === "home") return "checkout";
  return base.route;
}

// --- per-route document head (title / description / canonical / OG) ---------
// The body is client-rendered, but keeping the head accurate per route gives
// crawlers and link unfurlers a unique title + description for every page.
// (Static JSON-LD + a <noscript> summary live in index.html for no-JS crawlers.)
function upsertMeta(attr, key, value) {
  let el = document.head.querySelector(`meta[${attr}="${key}"]`);
  if (!el) {
    el = document.createElement("meta");
    el.setAttribute(attr, key);
    document.head.appendChild(el);
  }
  el.setAttribute("content", value);
}
function upsertCanonical(href) {
  let el = document.head.querySelector('link[rel="canonical"]');
  if (!el) {
    el = document.createElement("link");
    el.setAttribute("rel", "canonical");
    document.head.appendChild(el);
  }
  el.setAttribute("href", href);
}
function applyHead(route, slug) {
  const SITE = "Lint Error";
  const defaults = {
    home:     ["Lint Error — Roleplay FiveM Scripts (QBCore, ESX, QBX)", "Roleplay-first FiveM scripts for QBCore, ESX & QBX — performance-audited, escrow-protected, and backed by a developer who answers every ticket within 12 hours."],
    scripts:  ["Paid Scripts — Lint Error", "Every paid FiveM resource from Lint Error in one place — escrow-protected, licensed to your CFX account, and supported on Discord within 12 hours."],
    free:     ["Free & Open-Source FiveM Resources — Lint Error", "Small, focused FiveM resources built and released free on GitHub by Lint Error. Support provided in Discord."],
    about:    ["About — Lint Error", "One developer tired of bad scripts, now building his own — optimized, secure, immersive, reliable FiveM resources."],
    terms:    ["Terms of Service — Lint Error", "License, transfer, refund and support terms for Lint Error FiveM resources."],
    checkout: ["Checkout — Lint Error", "Secure checkout via Tebex — your script is issued to your CFX.re account."],
  };
  let [title, desc] = defaults[route] || [`${SITE} — FiveM Scripts`, "Premium FiveM scripts and resources."];
  // Resolve a per-route share image so a shared product link unfurls with that
  // product's art rather than the generic hero. Relative catalog paths are made
  // absolute against SITE_ORIGIN.
  const abs = (src) => (/^https?:\/\//.test(src) ? src : `${SITE_ORIGIN}/${src.replace(/^\//, "")}`);
  let image = `${SITE_ORIGIN}/assets/screenshots/hero-composite.jpeg`;
  const galleryImage = (p) => {
    const g = p.gallery || [];
    const vid = g.find((x) => x.youtubeId);
    if (vid) return `https://img.youtube.com/vi/${vid.youtubeId}/maxresdefault.jpg`;
    const img = g.find((x) => x.src || x.poster);
    return img ? abs(img.src || img.poster) : image;
  };
  if (route === "product") {
    const p = (window.PRODUCTS || []).find((x) => x.slug === slug);
    if (p) {
      const soon = p.status === "coming-soon";
      title = `${p.name}${p.resourceName ? ` (${p.resourceName})` : ""}${soon ? " — Coming Soon" : ""} — ${SITE}`;
      desc = p.tagline || desc;
      image = galleryImage(p);
    }
  } else if (route === "free-product") {
    const p = (window.FREE_PRODUCTS || []).find((x) => x.slug === slug);
    if (p) { title = `${p.name} — Free FiveM Resource — ${SITE}`; desc = p.tagline || desc; image = galleryImage(p); }
  }
  const url = SITE_ORIGIN + pathFromState(route, slug);
  document.title = title;
  upsertMeta("name", "description", desc);
  upsertCanonical(url);
  upsertMeta("property", "og:title", title);
  upsertMeta("property", "og:description", desc);
  upsertMeta("property", "og:url", url);
  upsertMeta("property", "og:image", image);
  upsertMeta("name", "twitter:title", title);
  upsertMeta("name", "twitter:description", desc);
  upsertMeta("name", "twitter:image", image);
}

const App = () => {
  // Routing — URL is the truth; localStorage le_slug is kept only as a soft
  // default for the rare case that the URL doesn't carry one.
  const [route, setRoute] = useState(initialRoute);
  const [slug, setSlug] = useState(() => stateFromPath(window.location.pathname).slug || localStorage.getItem("le_slug") || "used-car-dealership");

  const navigate = (to, s) => {
    setRoute(to);
    if (s) {
      setSlug(s);
      localStorage.setItem("le_slug", s);
    }
    const newPath = pathFromState(to, s || slug);
    if (window.location.pathname !== newPath) {
      window.history.pushState({ route: to, slug: s || slug }, "", newPath + window.location.search + window.location.hash);
    }
    window.scrollTo({ top: 0, behavior: "instant" });
  };

  // Browser back/forward buttons — sync React state from whatever URL the
  // history popped to. No pushState here; that'd loop.
  useEffect(() => {
    const onPop = () => {
      const s = stateFromPath(window.location.pathname);
      setRoute(s.route);
      if (s.slug) setSlug(s.slug);
    };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // Tebex basket
  const [basketIdent, setBasketIdent] = useState(() => localStorage.getItem("le_tebex_basket") || null);
  const clearBasket = () => {
    setBasketIdent(null);
    localStorage.removeItem("le_tebex_basket");
  };
  const ensureBasket = async () => {
    // Reuse the stored basket only if it exists AND hasn't been paid for yet.
    // Tebex marks finalized baskets with complete:true and refuses further
    // package adds on them ("basket with that identifier has already been
    // paid for"), so any paid basket must be discarded in favor of a fresh one.
    if (basketIdent) {
      const b = await tebexGetBasket(basketIdent);
      if (b && !b.complete) return basketIdent;
      clearBasket();
    }
    const fresh = await tebexCreateBasket();
    setBasketIdent(fresh.ident);
    localStorage.setItem("le_tebex_basket", fresh.ident);
    // Note: CFX identity on a fresh basket must be bound via Tebex's CFX
    // auth redirect — Tebex Headless has no REST endpoint to set it
    // programmatically. handleCheckout in pages-checkout.jsx detects an
    // identity-less basket and triggers that redirect.
    return fresh.ident;
  };

  // Cart
  const [cart, setCart] = useState(() => {
    try {
      const raw = JSON.parse(localStorage.getItem("le_cart") || "[]");
      // Re-validate on load: drop any line whose product no longer exists or has
      // since been flipped to coming-soon, so a stale persisted item can never
      // be priced or POSTed to Tebex (same guard addToCart applies on the way in).
      return raw.filter((line) => isLive(PRODUCTS.find((x) => x.id === line.id)));
    } catch { return []; }
  });
  useEffect(() => { localStorage.setItem("le_cart", JSON.stringify(cart)); }, [cart]);
  const [cartOpen, setCartOpen] = useState(false);
  const [badgePulse, setBadgePulse] = useState(false);

  const addToCart = async (id) => {
    // Hard guard: coming-soon (or otherwise non-live) products can never enter
    // the cart or the Tebex basket, even if a UI control slips through.
    const prod = PRODUCTS.find((x) => x.id === id);
    if (!isLive(prod)) return;
    setCart((c) => {
      const existing = c.find((x) => x.id === id);
      if (existing) return c; // one-per-script: ignore dupes
      return [...c, { id }];
    });
    setBadgePulse(true);
    setTimeout(() => setBadgePulse(false), 500);
    // Sync to Tebex basket in background (only if token configured)
    const p = PRODUCTS.find((x) => x.id === id);
    if (p && p.tebexPackageId && TEBEX_CONFIG.publicToken !== "YOUR_TEBEX_PUBLIC_TOKEN") {
      try {
        const ident = await ensureBasket();
        await tebexAddPackage(ident, p.tebexPackageId, 1);
      } catch (e) { console.warn("Tebex add failed", e); }
    }
  };
  const removeFromCart = async (id) => {
    setCart((c) => c.filter((x) => x.id !== id));
    const p = PRODUCTS.find((x) => x.id === id);
    if (p && p.tebexPackageId && basketIdent && TEBEX_CONFIG.publicToken !== "YOUR_TEBEX_PUBLIC_TOKEN") {
      try { await tebexRemovePackage(basketIdent, p.tebexPackageId); } catch {}
    }
  };
  const clearCart = () => setCart([]);
  const cartTotal = cart.reduce((sum, line) => {
    const p = PRODUCTS.find((x) => x.id === line.id);
    return sum + (isLive(p) ? p.price : 0);
  }, 0);

  // Fly-to-cart animation
  const flyToCart = (id, fromEl, buyNow) => {
    addToCart(id);
    if (!fromEl) {
      if (buyNow) { setCartOpen(false); navigate("checkout"); }
      else setCartOpen(true);
      return;
    }
    const cartBtn = document.querySelector(".cart-anchor .icon-btn");
    if (!cartBtn) { if (buyNow) navigate("checkout"); return; }
    const from = fromEl.getBoundingClientRect();
    const to = cartBtn.getBoundingClientRect();
    const ghost = document.createElement("div");
    ghost.className = "flyout-ghost";
    ghost.style.left = from.left + from.width / 2 - 24 + "px";
    ghost.style.top = from.top + from.height / 2 - 24 + "px";
    document.body.appendChild(ghost);
    requestAnimationFrame(() => {
      ghost.style.transform = `translate(${to.left + to.width / 2 - from.left - from.width / 2}px, ${to.top + to.height / 2 - from.top - from.height / 2}px) scale(0.3)`;
      ghost.style.opacity = "0";
    });
    setTimeout(() => {
      ghost.remove();
      if (buyNow) { setCartOpen(false); navigate("checkout"); }
      else setCartOpen(true);
    }, 560);
  };

  // Currency
  const [currency, setCurrencyState] = useState(() => localStorage.getItem("le_currency") || "USD");
  const setCurrency = (c) => {
    setCurrencyState(c);
    localStorage.setItem("le_currency", c);
  };
  const fmt = (n) => formatPrice(n, currency);

  // Auth — session lives on the server; hydrate from /api/session on load.
  const [user, setUser] = useState(null);
  // Until the first /api/session resolves we don't yet know if there's a
  // session, so the topbar shows a neutral placeholder instead of flashing
  // "Log in" at a signed-in visitor on every load / cold-Worker start.
  const [authReady, setAuthReady] = useState(false);
  const [loginOpen, setLoginOpen] = useState(false);
  // Hits /api/session and syncs React state. Returns the fresh user (or null).
  // Anything that's about to take a sensitive action (checkout) should call
  // this first so the action is authorized against the *current* server
  // session, not stale React state.
  const refreshSession = async () => {
    try {
      const r = await fetch("/api/session", { credentials: "same-origin" });
      const d = r.ok ? await r.json() : { user: null };
      const fresh = d.user || null;
      setUser(fresh);
      setAuthReady(true);
      return fresh;
    } catch {
      setUser(null);
      setAuthReady(true);
      return null;
    }
  };
  useEffect(() => {
    let cancelled = false;
    fetch("/api/session", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : { user: null }))
      .then((d) => { if (!cancelled) { setUser(d.user || null); setAuthReady(true); } })
      .catch(() => { if (!cancelled) { setUser(null); setAuthReady(true); } });
    // Back-button / bfcache restores don't re-run the mount effect, so
    // listen for pageshow-with-persisted to force a fresh /api/session.
    const onPageShow = (e) => { if (e.persisted && !cancelled) refreshSession(); };
    window.addEventListener("pageshow", onPageShow);
    return () => { cancelled = true; window.removeEventListener("pageshow", onPageShow); };
  }, []);
  const doLogin = (u) => { setUser(u); setLoginOpen(false); };
  const doLogout = async () => {
    try { await fetch("/api/logout", { method: "POST", credentials: "same-origin" }); } catch {}
    setUser(null);
    // The Tebex basket has the previous buyer's CFX identity bound to it —
    // drop it so a new shopper on the same device gets a fresh one with
    // their own identity (and so we don't accidentally attribute their
    // purchase to the last signed-in CFX account).
    clearBasket();
    // Cart items are the user's product selections — leaving them so they
    // can still re-browse / check out after signing back in. Clear any
    // mid-flight auto-continue checkout flag though.
    try { localStorage.removeItem("le_continue_checkout"); } catch {}
  };

  // Tweaks
  const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS);
  const [editMode, setEditMode] = useState(false);
  const setTweak = (k, v) => {
    setTweaks((t) => ({ ...t, [k]: v }));
    window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { [k]: v } }, "*");
  };
  useEffect(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") setEditMode(true);
      if (d.type === "__deactivate_edit_mode") setEditMode(false);
    };
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);
  useEffect(() => {
    document.body.setAttribute("data-density", tweaks.density);
  }, [tweaks.density]);

  useEffect(() => {
    const onOpen = () => setLoginOpen(true);
    document.addEventListener("le:open-login", onOpen);
    return () => document.removeEventListener("le:open-login", onOpen);
  }, []);

  // Global SPA navigation event — lets components without the navigate prop
  // (e.g. the login modal's Terms link) route without a full page reload.
  useEffect(() => {
    const onNav = (e) => {
      const d = e.detail || {};
      if (d.route) navigate(d.route, d.slug);
    };
    document.addEventListener("le:navigate", onNav);
    return () => document.removeEventListener("le:navigate", onNav);
  }, []);

  // Keep the document head (title/description/canonical/OG) in sync with route.
  useEffect(() => { applyHead(route, slug); }, [route, slug]);

  const ctx = { cart, addToCart, removeFromCart, cartTotal, clearCart, user, refreshSession, ensureBasket, basketIdent, setBasketIdent, clearBasket, currency, setCurrency, fmt };

  return (
    <StoreCtx.Provider value={ctx}>
      <div className="page">
        <Topbar
          route={route}
          navigate={navigate}
          cart={cart}
          onCartToggle={setCartOpen}
          cartOpen={cartOpen}
          onLoginOpen={() => setLoginOpen(true)}
          cartBadgePulse={badgePulse}
          user={user}
          authReady={authReady}
          onLogout={doLogout}
          currency={currency}
          setCurrency={setCurrency}
        />
        <main style={{ flex: 1 }} data-screen-label={`Route: ${route}`}>
          {route === "home" && <HomePage navigate={navigate} heroLayout={tweaks.heroLayout} cardStyle={tweaks.cardStyle} onAdd={flyToCart} />}
          {route === "scripts" && <ScriptsPage navigate={navigate} cardStyle={tweaks.cardStyle} onAdd={flyToCart} />}
          {route === "free" && <FreePage navigate={navigate} />}
          {route === "free-product" && <FreeProductPage slug={slug} navigate={navigate} />}
          {route === "product" && <ProductPage slug={slug} navigate={navigate} onAdd={flyToCart} />}
          {route === "about" && <AboutPage />}
          {route === "terms" && <TermsPage />}
          {route === "checkout" && <CheckoutPage navigate={navigate} />}
        </main>
        <Footer navigate={navigate} />
        {loginOpen ? <LoginModal onClose={() => setLoginOpen(false)} onLogin={doLogin} basketIdent={basketIdent} /> : null}
        <TweaksPanel tweaks={tweaks} setTweak={setTweak} visible={editMode} />
      </div>
    </StoreCtx.Provider>
  );
};

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