﻿// Product data + cart store + helpers

// ---------- WhatsApp number helpers ----------
// The number ultimately comes from public.site_settings.whatsapp_number,
// loaded by zsb.useSettingsStore + applySettingRow → window.WHATSAPP_NUMBER.
// The DEFAULT below is only used until settings finish loading on the first
// render and as a hard fallback if the admin clears the setting.
const DEFAULT_WHATSAPP_NUMBER = "31638362147";
if (typeof window.WHATSAPP_NUMBER === "undefined") {
  window.WHATSAPP_NUMBER = DEFAULT_WHATSAPP_NUMBER;
}

function normalizeWhatsAppHref(value) {
  const raw = String(value || "").trim();
  if (!raw) return "https://wa.me/" + DEFAULT_WHATSAPP_NUMBER;
  if (raw.startsWith("https://wa.me/") || raw.startsWith("http://wa.me/")) {
    return raw.replace(/^http:\/\//, "https://");
  }
  if (raw.startsWith("https://api.whatsapp.com/") || raw.startsWith("http://api.whatsapp.com/")) {
    return raw.replace(/^http:\/\//, "https://");
  }
  const clean = raw.replace(/[^\d]/g, "");
  if (!clean) return "https://wa.me/" + DEFAULT_WHATSAPP_NUMBER;
  return "https://wa.me/" + clean;
}

function formatWhatsAppDisplay(value) {
  const raw = String(value || "").trim();
  let clean = raw;
  if (raw.includes("wa.me/")) {
    clean = raw.split("wa.me/")[1].split("?")[0];
  }
  clean = clean.replace(/[^\d]/g, "");
  if (clean === "31638362147") return "+31 6 38 36 21 47";
  if (clean.startsWith("31") && clean.length === 11) {
    return "+31 " + clean.slice(2, 3) + " " + clean.slice(3, 5) + " " + clean.slice(5, 7) + " " + clean.slice(7, 9) + " " + clean.slice(9);
  }
  if (clean) return "+" + clean;
  return "+31 6 38 36 21 47";
}

// Convenience used by every WhatsApp CTA. Reads window.WHATSAPP_NUMBER at
// call time so a hot settings refresh propagates on the next render. The
// optional `text` is URL-encoded and appended only if the normalized base
// doesn't already contain a text query.
let __whatsappLogged = false;
function getWhatsAppHref(text) {
  const raw = (typeof window !== "undefined" && window.WHATSAPP_NUMBER) || DEFAULT_WHATSAPP_NUMBER;
  const base = normalizeWhatsAppHref(raw);
  if (!__whatsappLogged && typeof console !== "undefined") {
    __whatsappLogged = true;
    try {
      console.log("[whatsapp] raw:", raw);
      console.log("[whatsapp] href:", base);
    } catch (_) {}
  }
  if (!text) return base;
  if (base.includes("?text=")) return base; // caller-supplied URL already has text
  const sep = base.includes("?") ? "&" : "?";
  return base + sep + "text=" + encodeURIComponent(text);
}

function getWhatsAppDisplay() {
  const raw = (typeof window !== "undefined" && window.WHATSAPP_NUMBER) || DEFAULT_WHATSAPP_NUMBER;
  return formatWhatsAppDisplay(raw);
}

// Backward-compat shim: anything still referencing WHATSAPP_URL gets a
// freshly-resolved URL via a getter. Should be deleted once all call sites
// are switched to getWhatsAppHref(text).
Object.defineProperty(window, "WHATSAPP_URL", {
  configurable: true,
  get: function () { return getWhatsAppHref("Ik wil advies over een Zyro Electric fiets"); },
});

const PRODUCTS = {
  v20: {
    id:"v20",
    type:"fatbike",
    name:"Zyro V20 Pro Special Edition",
    short:"Zyro V20 Pro",
    price:950,
    oldPrice:null,
    color:"Matte antraciet",
    badge:"Compleet",
    blurb:"Comfort-fatbike met dubbele vering, 20×4″ banden en verlengd zadel. Inclusief volledige accessoire-set: voor- en achterdrager, ringslot, alarm, frametas en voetsteunen.",
    benefits:[
      "Compleet pakket: alle accessoires inbegrepen",
      "Comfort-zithouding, ook voor langere ritten",
      "Ondersteuning tot 25 km/u — 100% legaal",
      "Tot 80 km actieradius op 48V 15Ah",
    ],
    specs:{
      "Motor":"250W achternaaf-motor",
      "Trapondersteuning":"Tot 25 km/u (EU pedelec)",
      "Walk-assist":"Tot 6 km/u",
      "Accu":"48V · 15Ah · uitneembaar",
      "Actieradius":"Tot 80 km (eco-modus)",
      "Laadtijd":"5-6 uur",
      "Remmen":"Hydraulische schijfremmen voor & achter",
      "Versnellingen":"Shimano 7-speed",
      "Wielen":"20 × 4.0″ fatbanden",
      "Vering":"Voorvork + achtervering",
      "Display":"LCD kleurendisplay",
      "Kleur":"Matte antraciet / zwart",
      "Frame":"Aluminium, matte coating",
      "Max belasting":"150 kg",
      "Gewicht":"33 kg incl. accu",
    },
    inBox:[
      "Zyro V20 Pro fatbike",
      "Uitneembare 48V 15Ah accu",
      "Snellader",
      "Verlengd tweepersoonszadel",
      "Voor- en achterdrager",
      "Achterdrager-kussen",
      "ART-2 ringslot",
      "Voetsteunen passagier",
      "Frametas (waterdicht)",
      "Bewegingsalarm + afstandsbediening",
      "Bel & spatborden",
      "Handleiding (NL/EN)",
    ],
    forWho:[
      "Studenten & jongeren 16+ die opvallend willen rijden",
      "Mensen met regelmatig passagier achterop",
      "Wie comfort en rechtop zitten waardeert",
      "Stad woon-werk binnen 25 km",
    ],
  },
  gt2000: {
    id:"gt2000",
    type:"crossbike",
    name:"Zyro GT2000 Black Edition",
    short:"Zyro GT2000",
    price:1399,
    oldPrice:null,
    color:"Zwart",
    badge:"Nieuw",
    blurb:"Crossbike-stijl e-bike met geïntegreerde 30Ah accu en NFC-display. Sportieve zithouding, noppenbanden en lange actieradius — gemaakt voor lange shifts en lange ritten.",
    benefits:[
      "Tot 120 km op één lading",
      "30Ah accu geïntegreerd in frame",
      "NFC-ontgrendeling met keycard",
      "Sportieve crossbike-zithouding",
    ],
    specs:{
      "Motor":"250W achternaaf-motor",
      "Trapondersteuning":"Tot 25 km/u (EU pedelec)",
      "Walk-assist":"Tot 6 km/u",
      "Accu":"48V · 30Ah · geïntegreerd, uitneembaar",
      "Actieradius":"Tot 120 km (eco-modus)",
      "Laadtijd":"8-9 uur",
      "Remmen":"Hydraulische schijfremmen voor & achter",
      "Versnellingen":"Shimano 7-speed",
      "Wielen":"24″ met noppenbanden",
      "Vering":"Voorvork + achtervering (instelbaar)",
      "Display":"LCD met NFC-ontgrendeling",
      "Kleur":"Zwart",
      "Frame":"Aluminium 6061, crossbike geometrie",
      "Max belasting":"150 kg",
      "Gewicht":"36 kg incl. accu",
    },
    inBox:[
      "Zyro GT2000 crossbike",
      "Geïntegreerde 48V 30Ah accu",
      "Snellader 3A",
      "2× NFC keycard",
      "Sport-zadel",
      "Spatborden geïntegreerd",
      "Bel",
      "RDW frame-registratie",
      "Handleiding (NL/EN)",
    ],
    forWho:[
      "Bezorgers die lange shifts draaien",
      "Sportieve rijders met crossbike-voorkeur",
      "Forensen 15-40 km enkel",
      "Wie NFC-ontgrendeling en opvallende styling wil",
    ],
  },
};

const ACCESSORIES = [
  {id:"acc-lock",     name:"ART-3 vouwslot",          price:79,  icon:"lock",     type:"accessoire", desc:"ART-3 goedgekeurd vouwslot. Onmisbaar voor goede verzekering."},
  {id:"acc-helmet",   name:"Stadshelm matte zwart",   price:49,  icon:"helmet",   type:"accessoire", desc:"Lichte helm met LED-achterlicht, maat M/L."},
  {id:"acc-phone",    name:"Telefoonhouder universeel",price:24,  icon:"phone",   type:"accessoire", desc:"Snelle quick-release houder voor smartphones 4.5-7\"."},
  {id:"acc-alarm",    name:"Extra alarmmodule",       price:39,  icon:"alarm",    type:"accessoire", desc:"Bewegingssensor met afstandsbediening, 120dB."},
  {id:"acc-charger",  name:"Extra 48V snellader",     price:69,  icon:"charger",  type:"accessoire", desc:"3A snellader — vol in 5 uur. Handig op werk of school."},
  {id:"acc-cover",    name:"Regenhoes premium",       price:29,  icon:"cover",    type:"accessoire", desc:"Waterdichte hoes, past op beide modellen."},
  {id:"acc-toolkit",  name:"Onderhoudspakket",        price:35,  icon:"toolkit",  type:"accessoire", desc:"Multitool, kettingolie, bandenplakset en doek."},
];

const ALL_ITEMS = { ...PRODUCTS };
ACCESSORIES.forEach(a => ALL_ITEMS[a.id] = a);

// Image galleries
const GALLERY = {
  v20: [
    "assets/v20/01.jpg",
    "assets/v20/02.jpg",
    "assets/v20/03.jpg",
    "assets/v20/04.jpg",
    "assets/v20/05.jpg",
    "assets/v20/06.jpg",
    "assets/v20/07.jpg",
    "assets/v20/08.jpg",
    "assets/v20/09.jpg",
  ],
  gt2000: [
    "assets/gt2000/01.jpg",
    "assets/gt2000/02.jpg",
    "assets/gt2000/03.jpg",
    "assets/gt2000/04.jpg",
    "assets/gt2000/05.jpg",
    "assets/gt2000/06.jpg",
    "assets/gt2000/07.jpg",
    "assets/gt2000/08.jpg",
    "assets/gt2000/09.jpg",
    "assets/gt2000/10.jpg",
    "assets/gt2000/11.jpg",
    "assets/gt2000/12.jpg",
    "assets/gt2000/13.jpg",
    "assets/gt2000/14.jpg",
  ],
};

// Optional assembly service — added to cart alongside a bike when user picks "gemonteerd"
// ASSEMBLY_SERVICE blijft bestaan als fallback voor oude carts en als generieke
// service. ASSEMBLY_PER_BIKE bevat aparte SKUs per fiets zodat de live
// assembly_price uit Supabase per model verschillend kan zijn.
const ASSEMBLY_SERVICE = {
  id: "svc-assembly",
  name: "Montage door Zyro",
  short: "Montage door Zyro",
  type: "service",
  price: 50,
  icon: "tools",
  desc: "Volledig gemonteerd, afgesteld en rij-klaar aan de deur. Inclusief eerste accu-lading."
};
ALL_ITEMS[ASSEMBLY_SERVICE.id] = ASSEMBLY_SERVICE;

const ASSEMBLY_PER_BIKE = {
  v20: {
    id: "svc-assembly-v20",
    name: "Montage door Zyro",
    short: "Montage door Zyro · V20 Pro",
    type: "service",
    price: 50,
    icon: "tools",
    bikeSlug: "v20",
    desc: "Volledig gemonteerd, afgesteld en rij-klaar aan de deur. Inclusief eerste accu-lading."
  },
  gt2000: {
    id: "svc-assembly-gt2000",
    name: "Montage door Zyro",
    short: "Montage door Zyro · GT2000",
    type: "service",
    price: 50,
    icon: "tools",
    bikeSlug: "gt2000",
    desc: "Volledig gemonteerd, afgesteld en rij-klaar aan de deur. Inclusief eerste accu-lading."
  },
};
ALL_ITEMS[ASSEMBLY_PER_BIKE.v20.id]    = ASSEMBLY_PER_BIKE.v20;
ALL_ITEMS[ASSEMBLY_PER_BIKE.gt2000.id] = ASSEMBLY_PER_BIKE.gt2000;

// ============================================================
// PRODUCT i18n (EN translations) + LP() helper
// ============================================================
// EN overrides for product/accessory fields. Same key path as the NL
// version. window.LP(obj, "blurb", "en") returns the EN value or falls
// back to obj.blurb (NL) when no override exists. Specs are a nested
// dict; LP recognises that and falls through to the per-key map.
const I18N_PRODUCTS = {
  v20: {
    color: "Matte anthracite",
    type: "fatbike",
    badge: "Complete",
    blurb: "Comfort fatbike with dual suspension, 20×4″ tyres and an extended saddle. Includes a full accessory set: front and rear rack, ring lock, alarm, frame bag and footrests.",
    benefits: [
      "Complete package: all accessories included",
      "Comfort upright riding position, also for longer rides",
      "Assist up to 25 km/h — 100% legal",
      "Up to 80 km range on 48V 15Ah",
    ],
    specs: {
      "Motor": "250W rear-hub motor",
      "Pedal assist": "Up to 25 km/h (EU pedelec)",
      "Walk-assist": "Up to 6 km/h",
      "Battery": "48V · 15Ah · removable",
      "Range": "Up to 80 km (eco mode)",
      "Charge time": "5-6 hours",
      "Brakes": "Hydraulic disc brakes front & rear",
      "Gears": "Shimano 7-speed",
      "Wheels": "20 × 4.0″ fat tyres",
      "Suspension": "Front fork + rear shock",
      "Display": "LCD colour display",
      "Colour": "Matte anthracite / black",
      "Frame": "Aluminium, matte coating",
      "Max load": "150 kg",
      "Weight": "33 kg incl. battery",
    },
    inBox: [
      "Zyro V20 Pro fatbike",
      "Removable 48V 15Ah battery",
      "Fast charger",
      "Extended 2-seater saddle",
      "Front and rear rack",
      "Rear-rack cushion",
      "ART-2 ring lock",
      "Passenger footrests",
      "Frame bag (waterproof)",
      "Motion alarm + remote",
      "Bell & mudguards",
      "Manual (NL/EN)",
    ],
    forWho: [
      "Students & riders 16+ who want to stand out",
      "People who regularly carry a passenger",
      "Anyone who values comfort and upright posture",
      "City commute up to 25 km",
    ],
  },
  gt2000: {
    color: "Black",
    type: "crossbike",
    badge: "New",
    blurb: "Crossbike-style e-bike with integrated 30Ah battery and NFC display. Sporty riding position, knobby tyres and long range — made for long shifts and long rides.",
    benefits: [
      "Up to 120 km on a single charge",
      "30Ah battery integrated in the frame",
      "NFC unlock with keycard",
      "Sporty crossbike riding position",
    ],
    specs: {
      "Motor": "250W rear-hub motor",
      "Pedal assist": "Up to 25 km/h (EU pedelec)",
      "Walk-assist": "Up to 6 km/h",
      "Battery": "48V · 30Ah · integrated, removable",
      "Range": "Up to 120 km (eco mode)",
      "Charge time": "8-9 hours",
      "Brakes": "Hydraulic disc brakes front & rear",
      "Gears": "Shimano 7-speed",
      "Wheels": "24″ with knobby tyres",
      "Suspension": "Front fork + adjustable rear shock",
      "Display": "LCD with NFC unlock",
      "Colour": "Black",
      "Frame": "Aluminium 6061, crossbike geometry",
      "Max load": "150 kg",
      "Weight": "36 kg incl. battery",
    },
    inBox: [
      "Zyro GT2000 crossbike",
      "Integrated 48V 30Ah battery",
      "3A fast charger",
      "2× NFC keycard",
      "Sport saddle",
      "Integrated mudguards",
      "Bell",
      "RDW frame registration",
      "Manual (NL/EN)",
    ],
    forWho: [
      "Couriers doing long shifts",
      "Sporty riders preferring a cross-bike",
      "Commuters 15-40 km one way",
      "Anyone who wants NFC unlock and bold styling",
    ],
  },
  "acc-lock":    { name: "ART-3 folding lock",       desc: "ART-3 certified folding lock. Required for proper theft insurance.", type: "accessory" },
  "acc-helmet":  { name: "City helmet matte black",  desc: "Lightweight helmet with LED rear light, size M/L.",                 type: "accessory" },
  "acc-phone":   { name: "Universal phone mount",    desc: "Quick-release mount for smartphones 4.5-7\".",                       type: "accessory" },
  "acc-alarm":   { name: "Extra alarm module",       desc: "Motion sensor with remote, 120dB.",                                  type: "accessory" },
  "acc-charger": { name: "Extra 48V fast charger",   desc: "3A fast charger — full in 5 hours. Handy at work or school.",        type: "accessory" },
  "acc-cover":   { name: "Premium rain cover",       desc: "Waterproof cover, fits both models.",                                type: "accessory" },
  "acc-toolkit": { name: "Maintenance kit",          desc: "Multitool, chain oil, puncture repair set and cloth.",               type: "accessory" },
  "svc-assembly":           { name: "Assembly by Zyro", short: "Assembly by Zyro",                 desc: "Fully assembled, tuned and ride-ready at your door. Includes first battery charge.", type: "service" },
  "svc-assembly-v20":       { name: "Assembly by Zyro", short: "Assembly by Zyro · V20 Pro",       desc: "Fully assembled, tuned and ride-ready at your door. Includes first battery charge.", type: "service" },
  "svc-assembly-gt2000":    { name: "Assembly by Zyro", short: "Assembly by Zyro · GT2000",        desc: "Fully assembled, tuned and ride-ready at your door. Includes first battery charge.", type: "service" },
};

// LP(obj, field, lang) — Localize Product field.
// Returns the EN override when lang === 'en' AND an override exists for
// this product+field; otherwise falls back to obj[field] (the NL value).
// Safe with missing data: never throws, always returns something useful.
window.LP = function localizeProduct(obj, field, lang) {
  if (!obj) return undefined;
  if (lang === "en") {
    const ov = I18N_PRODUCTS[obj.id];
    if (ov && ov[field] != null) return ov[field];
  }
  return obj[field];
};
window.I18N_PRODUCTS = I18N_PRODUCTS;

// Null-safe currency formatter. Returns "€ 0" for null/undefined/NaN so a
// missing oldPrice can't crash a render.
const formatEUR = n => {
  const v = Number(n);
  if (n === null || n === undefined || !isFinite(v)) return "€ 0";
  return "€ " + v.toLocaleString("nl-NL", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
};

const formatEURCompact = n => formatEUR(n).replace("€ ", "€");
const hasDiscount = item => {
  const oldPrice = Number(item && item.oldPrice);
  const price = Number(item && item.price);
  return Number.isFinite(oldPrice) && Number.isFinite(price) && oldPrice > price;
};
const discountAmount = item => hasDiscount(item) ? Number(item.oldPrice) - Number(item.price) : 0;
const discountLabel = (item, lang) => `${formatEURCompact(discountAmount(item))} ${lang === "en" ? "off" : "korting"}`;

// ---------- Availability helpers ----------
// `active = false`  → product is fully hidden (catalog filter)
// availability_status:
//   "available"    → can buy (default for legacy rows)
//   "sold_out"     → visible, NOT buyable
//   "unavailable"  → visible, NOT buyable
const isProductBuyable = (product) => {
  if (!product) return false;
  if (product.active === false) return false;
  const status = product.availability_status;
  if (status == null || status === "") return true;
  return status === "available";
};
const availabilityLabel = (status, lang) => {
  if (status === "sold_out")    return lang === "en" ? "Sold out" : "Uitverkocht";
  if (status === "unavailable") return lang === "en" ? "Temporarily unavailable" : "Tijdelijk niet beschikbaar";
  return null;
};
const hasUnavailableCartItem = (items) => Array.isArray(items) && items.some(it => {
  const p = ALL_ITEMS[it && it.id];
  return p && !isProductBuyable(p);
});

const getShippingConfig = () => ({
  nl: 19,
  be: 29,
  de: 29,
  free_shipping_threshold: 500,
  default_country: "NL",
  pickup_enabled: true,
  delivery_enabled: true,
  ...(window.SHIPPING_CONFIG || {}),
});

const shippingFeeFor = (subtotal, country = null, method = "bezorgen") => {
  const amount = Number(subtotal) || 0;
  if (amount <= 0 || method === "ophalen") return 0;
  const cfg = getShippingConfig();
  const c = String(country || cfg.default_country || "NL").toUpperCase();
  const fee = Number(c === "BE" ? cfg.be : c === "DE" ? cfg.de : cfg.nl) || 0;
  const threshold = Number(cfg.free_shipping_threshold) || 0;
  return threshold > 0 && amount >= threshold ? 0 : fee;
};

/* ============ CART STORE ============ */
const CartContext = React.createContext(null);

// Per-account localStorage key. Guests get their own key so a logged-in cart
// never leaks into the guest session and vice-versa. We never merge carts
// across accounts.
const getCartStorageKey = (userId) =>
  userId ? `zyro_cart_user_${userId}` : "zyro_cart_guest";
// Legacy single-bucket key — drained on first load only.
const LEGACY_CART_KEY = "zyro-cart";

const loadCartFromStorage = (userId) => {
  try {
    let raw = localStorage.getItem(getCartStorageKey(userId));
    // First-load migration: pull from the legacy single-bucket key if the
    // per-user bucket is empty. Only applied for guests so we don't
    // accidentally pin a legacy cart to user A on first sign-in.
    if ((raw === null || raw === "") && !userId) {
      const legacy = localStorage.getItem(LEGACY_CART_KEY);
      if (legacy) {
        raw = legacy;
        try { localStorage.removeItem(LEGACY_CART_KEY); } catch (_) {}
      }
    }
    const parsed = JSON.parse(raw || "[]");
    if (!Array.isArray(parsed)) return [];
    // Drop entries whose product is gone or no longer buyable.
    return parsed.filter(it => {
      const p = ALL_ITEMS[it && it.id];
      return p && isProductBuyable(p);
    });
  } catch (e) {
    return [];
  }
};

// useCartStore(authUser) — authUser may be null/undefined (guest).
// When the auth user changes, the previous cart is persisted under its own
// key and the new user's cart is loaded fresh. Guest carts stay separate.
const useCartStore = (authUser) => {
  const userId = (authUser && authUser.id) || null;

  const [items, setItems] = React.useState(() => loadCartFromStorage(userId));
  const [open, setOpen] = React.useState(false);
  const prevUserIdRef = React.useRef(userId);

  // Persist current cart on every items / userId change.
  React.useEffect(() => {
    try {
      localStorage.setItem(getCartStorageKey(userId), JSON.stringify(items));
    } catch (_) { /* localStorage disabled — ignore */ }
  }, [items, userId]);

  // On auth user change: stash current items under the PREVIOUS key, then
  // load the NEW user's cart from storage. Do not merge.
  React.useEffect(() => {
    if (prevUserIdRef.current === userId) return;
    try {
      localStorage.setItem(
        getCartStorageKey(prevUserIdRef.current),
        JSON.stringify(items)
      );
    } catch (_) {}
    setItems(loadCartFromStorage(userId));
    prevUserIdRef.current = userId;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId]);

  const add = (id, qty = 1) => {
    const product = ALL_ITEMS[id];
    if (!product || !isProductBuyable(product)) {
      // Hard reject — UI should already disable the button for these.
      return false;
    }
    setItems(prev => {
      const found = prev.find(p => p.id === id);
      if (found) return prev.map(p => p.id === id ? {...p, qty: p.qty + qty} : p);
      return [...prev, {id, qty}];
    });
    setOpen(true);
    return true;
  };
  const remove = (id) => setItems(prev => prev.filter(p => p.id !== id));
  const setQty = (id, qty) => setItems(prev => qty <= 0 ? prev.filter(p => p.id !== id) : prev.map(p => p.id === id ? {...p, qty} : p));
  const clear = () => setItems([]);
  const count = items.reduce((s, it) => s + it.qty, 0);
  const subtotal = items.reduce((s, it) => s + (ALL_ITEMS[it.id]?.price || 0) * it.qty, 0);
  // Shipping is computed from live site_settings (loaded by zsb.useSettingsStore
  // into window.SHIPPING_CONFIG). Falls back to the original 500/19 if the
  // settings store hasn't loaded yet, so the cart never crashes.
  const SC = getShippingConfig();
  const country = (SC.default_country || "NL").toUpperCase();
  const freeThreshold = Number(SC.free_shipping_threshold) || 0;
  const shipping = shippingFeeFor(subtotal, country, "bezorgen");
  const total = subtotal + shipping;
  const VAT_RATE = (window.VAT_RATE || 21);
  const vatAmount = total * VAT_RATE / (100 + VAT_RATE);
  const hasUnavailable = hasUnavailableCartItem(items);

  return {
    items, add, remove, setQty, clear,
    count, subtotal, shipping, total, vatAmount, country,
    freeShippingThreshold: freeThreshold,
    hasUnavailable,
    userId,
    open, setOpen,
  };
};

window.PRODUCTS = PRODUCTS;
window.ACCESSORIES = ACCESSORIES;
window.ASSEMBLY_SERVICE = ASSEMBLY_SERVICE;
window.ASSEMBLY_PER_BIKE = ASSEMBLY_PER_BIKE;
window.GALLERY = GALLERY;
window.ALL_ITEMS = ALL_ITEMS;
window.formatEUR = formatEUR;
window.formatEURCompact = formatEURCompact;
window.hasDiscount = hasDiscount;
window.discountAmount = discountAmount;
window.discountLabel = discountLabel;
window.isProductBuyable = isProductBuyable;
window.availabilityLabel = availabilityLabel;
window.hasUnavailableCartItem = hasUnavailableCartItem;
window.getShippingConfig = getShippingConfig;
window.shippingFeeFor = shippingFeeFor;
// WhatsApp helpers — `window.WHATSAPP_URL` is now a live getter defined
// earlier in this file. Just expose the underlying helpers.
window.normalizeWhatsAppHref = normalizeWhatsAppHref;
window.formatWhatsAppDisplay = formatWhatsAppDisplay;
window.getWhatsAppHref = getWhatsAppHref;
window.getWhatsAppDisplay = getWhatsAppDisplay;
window.CartContext = CartContext;
window.useCartStore = useCartStore;
