// Shared helpers exposed on window for all 3 designs.

const fmt = (n, currency = 'USD') => {
  const sym = currency === 'EUR' ? '€' : '$';
  return sym + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};

const fmtInt = (n) => n.toLocaleString('en-US');

function applyProductOverrides() {
  if (!window.CATALOG || !window.PRODUCT_DETAIL_OVERRIDES) return;
  window.CATALOG = window.CATALOG.map((p) => {
    const o = window.PRODUCT_DETAIL_OVERRIDES[p.sku];
    if (!o) return p;
    const pricing = o.regionalPricing?.Shanghai || o.regionalPricing?.California;
    const regionalPricing = {
      ...p.regionalPricing,
      ...o.regionalPricing,
      ...(o.regionalPricing?.Shanghai ? { Germany: { ...o.regionalPricing.Shanghai } } : {}),
    };
    const stock = {
      ...p.stock,
      ...(typeof p.stock?.Shanghai === 'number' ? { Germany: p.stock.Shanghai } : {}),
    };
    return {
      ...p,
      brand: o.brand || p.brand,
      name: o.name || p.name,
      category: o.category || p.category,
      description: o.description || p.description,
      upc: o.upc || p.upc,
      dimensions: o.dimensions || p.dimensions,
      weight: o.weight || p.weight,
      regionalPricing,
      germanyStockReport: o.germanyStockReport || p.germanyStockReport,
      moq: pricing?.moq || p.moq,
      retail: pricing?.retail || p.retail,
      buy: pricing?.buy || p.buy,
      stock,
    };
  });
}

applyProductOverrides();

function regionalProduct(product, warehouse, discountPercent) {
  const region = typeof warehouse === 'string' ? warehouse : warehouse?.name;
  const pricing = region ? product.regionalPricing?.[region] : null;
  const merged = pricing ? { ...product, ...pricing } : { ...product };
  // Reseller price is driven by the reseller's discount off retail (from the DB),
  // not the catalog's precomputed buy. When no discount is supplied (e.g. the
  // design canvas) fall back to the catalog buy.
  const disc = Number(discountPercent);
  if (Number.isFinite(disc) && merged.retail != null) {
    merged.buy = merged.retail * (1 - disc / 100);
  }
  return merged;
}

const ORDER_QTY_STEP = 10;
const SHIPPING_LOCATIONS = [
  {
    id: 'hq',
    label: 'Main warehouse',
    address: '284 Cheshire Lane, Manchester, M4 5JD, United Kingdom',
  },
  {
    id: 'west',
    label: 'West Coast showroom',
    address: '1180 Harbor Drive, Long Beach, CA 90802, USA',
  },
  {
    id: 'eu',
    label: 'EU receiving office',
    address: 'Potsdamer Str. 84, 10785 Berlin, Germany',
  },
];
const SHIPPING_METHODS = [
  { id: 'dhl', carrier: 'DHL', account: '199567890', lastUsed: true },
  { id: 'fedex', carrier: 'FedEx', account: '8834217' },
  { id: 'ups', carrier: 'UPS', account: '45Y82R' },
];

function shippingMethodLabel(method) {
  if (!method) return '';
  return method.account ? `${method.carrier} (${method.account})` : method.carrier;
}

function exportLicenseFee(warehouse) {
  const region = typeof warehouse === 'string' ? warehouse : warehouse?.name;
  return region === 'Shanghai' ? 50 : 0;
}

function qtyStep(step) {
  const n = Number.parseInt(step, 10);
  return n > 0 ? n : ORDER_QTY_STEP;
}

function maxOrderQty(max, step = ORDER_QTY_STEP) {
  const inc = qtyStep(step);
  if (!Number.isFinite(Number(max))) return Infinity;
  return Math.max(0, Math.floor(Number(max) / inc) * inc);
}

function normalizeOrderQty(q, step = ORDER_QTY_STEP) {
  const inc = qtyStep(step);
  const n = Number.parseInt(q, 10) || 0;
  if (n <= 0) return 0;
  return Math.ceil(n / inc) * inc;
}

function resolveOrderQty(q, max, step, alreadyWarned) {
  const normalized = normalizeOrderQty(q, step);
  if (normalized <= 0) return 0;
  // Order in MOQ multiples, but compare against RAW availability — anything over
  // what's on hand becomes backorder (no "unavailable" hard stop).
  const allowed = Number.isFinite(Number(max)) ? Math.max(0, Math.floor(Number(max))) : Infinity;
  if (Number.isFinite(allowed) && normalized > allowed && !alreadyWarned) {
    return { requested: normalized, available: allowed };
  }
  return normalized;
}

// Cart hook — qty keyed by sku
function useCart() {
  const [cart, setCart] = React.useState({});
  const [qtyChoice, setQtyChoice] = React.useState(null);
  const [requestMoreChoice, setRequestMoreChoice] = React.useState(null);
  const [removeChoice, setRemoveChoice] = React.useState(null);
  const applyQty = (prev, sku, qty) => {
    const next = { ...prev };
    if (!qty || qty <= 0) delete next[sku];
    else next[sku] = qty;
    return next;
  };
  const setQty = (sku, q, max, step = ORDER_QTY_STEP) => setCart(prev => {
    const stepSize = qtyStep(step);
    const qty = resolveOrderQty(q, max, stepSize, false);
    if (qty && typeof qty === 'object') {
      setQtyChoice({ sku, step: stepSize, ...qty });
      return prev;
    }
    return applyQty(prev, sku, qty);
  });
  const inc = (sku, moq, step = ORDER_QTY_STEP, max) => setCart(prev => {
    const incBy = qtyStep(moq || step);
    const cur = prev[sku] || 0;
    const target = resolveOrderQty(cur === 0 ? incBy : cur + incBy, max, incBy, false);
    if (target && typeof target === 'object') {
      setQtyChoice({ sku, step: incBy, ...target });
      return prev;
    }
    return target > 0 ? applyQty(prev, sku, target) : prev;
  });
  const dec = (sku, moq, step = ORDER_QTY_STEP, max) => setCart(prev => {
    const decBy = qtyStep(moq || step);
    const cur = prev[sku] || 0;
    if (cur <= 0) return prev;
    const target = cur - decBy;
    if (target <= 0) {
      setRemoveChoice({ sku });
      return prev;
    }
    const next = { ...prev };
    next[sku] = target;
    return next;
  });
  const clear = () => { setCart({}); setQtyChoice(null); setRequestMoreChoice(null); setRemoveChoice(null); };
  const resolveQtyChoice = (mode) => {
    const choice = qtyChoice;
    if (!choice) return;
    if (mode === 'more') {
      setRequestMoreChoice(choice);
      setQtyChoice(null);
      return;
    }
    setCart((prev) => applyQty(prev, choice.sku, choice.available));
    setQtyChoice(null);
  };
  const confirmRequestMore = () => {
    const choice = requestMoreChoice;
    if (!choice) return;
    setCart((prev) => applyQty(prev, choice.sku, choice.requested));
    setRequestMoreChoice(null);
  };
  const cancelRequestMore = () => setRequestMoreChoice(null);
  const confirmRemove = () => {
    const choice = removeChoice;
    if (!choice) return;
    setCart((prev) => applyQty(prev, choice.sku, 0));
    setRemoveChoice(null);
  };
  const cancelRemove = () => setRemoveChoice(null);
  const totalUnits = Object.values(cart).reduce((a, b) => a + b, 0);
  const lineItems = (catalog, warehouse, discountPercent) => Object.entries(cart).map(([sku, qty]) => {
    const p = catalog.find(c => c.sku === sku);
    const priced = p ? regionalProduct(p, warehouse, discountPercent) : null;
    if (!priced) return null;
    const warehouseStock = Number(priced.stock?.[warehouse.name]) || 0;
    // Raw available (not rounded to MOQ): hold what we have, backorder the rest —
    // matches the server's split so the review preview and confirmation agree.
    const availableQty = warehouseStock;
    const inStockQty = Number.isFinite(availableQty) ? Math.min(qty, availableQty) : qty;
    const additionalQty = Math.max(0, qty - inStockQty);
    return {
      ...priced,
      qty,
      warehouseStock,
      inStockQty,
      additionalQty,
      subtotal: priced.buy * qty,
      retailSubtotal: priced.retail * qty,
      inStockSubtotal: priced.buy * inStockQty,
      additionalSubtotal: priced.buy * additionalQty,
    };
  }).filter(Boolean);
  return { cart, setQty, inc, dec, clear, qtyChoice, resolveQtyChoice, requestMoreChoice, confirmRequestMore, cancelRequestMore, removeChoice, confirmRemove, cancelRemove, totalUnits, lineItems };
}

// Gruv Gear brand mark. The source JPG is a white "V" on a black square, so we
// let the image fill a rounded square container regardless of the surrounding
// surface color. `size`/`radius` are numbers (px).
function BrandLogo({ size = 30, radius = 8, style }) {
  return (
    <div style={{ width: size, height: size, borderRadius: radius, overflow: 'hidden', flexShrink: 0, ...style }}>
      <img
        src="assets/gruvgear-logo.jpg"
        alt="Gruv Gear"
        style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
      />
    </div>
  );
}

// The dark navy both portals used behind their old split-screen panel. It is now
// the full-page ground for the sign-in screens, so the brand mark (a white "V" on
// black) and the white card both sit on the same brand color they always did.
const AUTH_BG = '#0b1220';

// Centered sign-in shell shared by the reseller portal and the employee portal:
// brand mark, wordmark, portal label, then one card holding the form. The card
// keeps each app's own light palette, so the shell takes a `palette` prop
// ({ surface, border, text, font, mono }) rather than reaching for a file-local
// theme constant — ledger passes `L`, inventory `P`.
//
// Layout notes: the shell is mounted both as a flex child (ledger) and directly
// at the root (inventory), hence `flex: 1` *and* `minHeight: '100%'`. Centering
// is done with `margin: auto` on the card rather than `alignItems: 'center'` so
// that a short viewport scrolls to the top of the card instead of clipping it.
function AuthShell({ palette, label, children, footer }) {
  return (
    <div style={{
      flex: 1, minHeight: '100%', width: '100%',
      background: AUTH_BG, color: '#fff', fontFamily: palette.font,
      display: 'flex', justifyContent: 'center',
      padding: 'clamp(24px, 6vw, 56px) 20px',
      overflowY: 'auto', boxSizing: 'border-box',
    }}>
      <div style={{ margin: 'auto', width: '100%', maxWidth: 400 }}>
        <div style={{
          display: 'flex', flexDirection: 'column', alignItems: 'center',
          textAlign: 'center', marginBottom: 'clamp(20px, 4vw, 28px)',
        }}>
          {/* The logo's own black square is a near-match for the navy ground, so a
              hairline ring keeps its edge readable. */}
          <BrandLogo size={64} radius={14} style={{ boxShadow: '0 0 0 1px rgba(255,255,255,0.14)' }} />
          <div style={{ marginTop: 16, fontSize: 26, fontWeight: 700, letterSpacing: -0.6 }}>Gruv Gear</div>
          <div style={{
            marginTop: 6, fontFamily: palette.mono, fontSize: 11, letterSpacing: 2.4,
            textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)',
          }}>{label}</div>
        </div>
        <div style={{
          background: palette.surface, border: `1px solid ${palette.border}`,
          borderRadius: 16, padding: 'clamp(22px, 5vw, 32px)', color: palette.text,
          boxShadow: '0 18px 44px rgba(0,0,0,0.28)',
        }}>
          {children}
        </div>
        {footer ? (
          <div style={{
            marginTop: 18, textAlign: 'center', fontSize: 13, lineHeight: 1.5,
            color: 'rgba(255,255,255,0.55)',
          }}>{footer}</div>
        ) : null}
      </div>
    </div>
  );
}

// Link styling for anything in AuthShell's footer — a light tint of the brand
// blue, since the #2e5dff accent is too dark to read on the navy ground.
const AUTH_LINK = { color: '#9db8ff', textDecoration: 'underline', textUnderlineOffset: 2 };

// --- reCAPTCHA v2 ----------------------------------------------------------
// Intuit rejected the app assessment with "Your app must include a ReCAPTCHA
// system to help detect and prevent fraudulent transactions", so this guards the
// payment step and all three sign-in forms. The checkbox variant is deliberate:
// a reviewer can see it, which the invisible v3 score does not give us.
//
// Site keys are public by design — they ship in the page and are locked to the
// domains registered in the reCAPTCHA console. The matching RECAPTCHA_SECRET
// lives in the server env. Set both together: with the key empty this renders
// nothing and sends no token, which pairs with the server allowing requests when
// its secret is unset.
const RECAPTCHA_SITE_KEY = '6LfBWI0tAAAAANRHVpmXxTakXynPJmtKNVSjPHI1';

const recaptchaEnabled = () => Boolean(RECAPTCHA_SITE_KEY);

// api.js publishes grecaptcha.render asynchronously after the script's own load
// event, so waiting on onload alone is not enough.
let recaptchaScriptPromise = null;
function loadRecaptcha() {
  if (recaptchaScriptPromise) return recaptchaScriptPromise;
  recaptchaScriptPromise = new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://www.google.com/recaptcha/api.js?render=explicit';
    script.async = true;
    script.defer = true;
    script.onload = () => {
      const startedAt = Date.now();
      (function poll() {
        if (window.grecaptcha && window.grecaptcha.render) return resolve(window.grecaptcha);
        if (Date.now() - startedAt > 10000) return reject(new Error('reCAPTCHA did not initialise'));
        setTimeout(poll, 50);
      })();
    };
    script.onerror = () => { recaptchaScriptPromise = null; reject(new Error('reCAPTCHA failed to load')); };
    document.head.appendChild(script);
  });
  return recaptchaScriptPromise;
}

// `onToken` receives the solved token, or '' when it expires or errors — the
// parent disables its submit button on the empty string. Bump `resetSignal`
// after a rejected submit: a token is single-use, so the widget must be reset
// before the user can try again.
function Recaptcha({ onToken, resetSignal = 0, theme = 'light' }) {
  const holder = React.useRef(null);
  const widgetId = React.useRef(null);
  const [failed, setFailed] = React.useState(false);

  React.useEffect(() => {
    if (!recaptchaEnabled()) return undefined;
    let cancelled = false;
    loadRecaptcha()
      .then((grecaptcha) => {
        if (cancelled || !holder.current || widgetId.current !== null) return;
        widgetId.current = grecaptcha.render(holder.current, {
          sitekey: RECAPTCHA_SITE_KEY,
          theme,
          callback: (token) => onToken(token),
          'expired-callback': () => onToken(''),
          'error-callback': () => onToken(''),
        });
      })
      .catch(() => { if (!cancelled) setFailed(true); });
    return () => { cancelled = true; };
  }, []);

  React.useEffect(() => {
    if (!resetSignal || widgetId.current === null) return;
    try { window.grecaptcha.reset(widgetId.current); } catch (_) {}
    onToken('');
  }, [resetSignal]);

  if (!recaptchaEnabled()) return null;
  return (
    <div style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}>
      {failed ? (
        <div style={{ fontSize: 12, color: '#b42318', textAlign: 'center' }}>
          Could not load the captcha. Check your connection and reload.
        </div>
      ) : (
        // The widget is a fixed 304px; on a 375px screen it fits inside the card
        // padding, and this keeps it from forcing the page to scroll sideways.
        <div ref={holder} style={{ maxWidth: '100%', overflowX: 'auto' }} />
      )}
    </div>
  );
}

// Gruv Gear's published customer contact (gruvgear.com/pages/contact-us). Intuit
// requires a way to reach support from inside the app, so this appears on both
// sign-in screens and on the payment receipt — the three places a user is most
// likely to be stuck. Kept here so there is one address to change.
const SUPPORT_EMAIL = 'customercare@gruvgear.com';

// `linkStyle` differs by surface: AUTH_LINK on the navy sign-in ground, the
// app's own accent inside the white payment modal.
function SupportLine({ linkStyle = AUTH_LINK, prefix = 'Need help?' }) {
  return (
    <>
      {prefix} Contact{' '}
      <a href={`mailto:${SUPPORT_EMAIL}`} style={linkStyle}>{SUPPORT_EMAIL}</a>
    </>
  );
}

// Sign-in field. Both portals use this one rather than their own `LedgerField` /
// `InvField` so the two login cards stay identical — `InvField` renders uppercase
// labels and is used across the inventory product modals, so it is left alone.
// 44px inputs to clear the touch-target minimum.
function AuthField({ palette, label, value, onChange, type = 'text', autoComplete }) {
  return (
    <label style={{ display: 'block' }}>
      <div style={{ fontSize: 13, fontWeight: 500, color: palette.text, marginBottom: 6 }}>{label}</div>
      <input
        type={type} value={value} autoComplete={autoComplete}
        onChange={(e) => onChange(e.target.value)}
        style={{
          width: '100%', height: 44, padding: '0 12px', borderRadius: 10,
          border: `1px solid ${palette.border}`, background: '#fff', color: palette.text,
          fontSize: 14, fontFamily: palette.font, outline: 'none', boxSizing: 'border-box',
        }}
      />
    </label>
  );
}

// Fake user account
const MOCK_USER = {
  id: 'DIST-0247',
  name: 'Marlowe & Finch Music Co.',
  contact: 'j.marlowe@marlowefinch.com',
  tier: 'Gold',
  ytd: 184320,
  shippingMethods: SHIPPING_METHODS,
};

Object.assign(window, { fmt, fmtInt, regionalProduct, ORDER_QTY_STEP, SHIPPING_LOCATIONS, SHIPPING_METHODS, shippingMethodLabel, exportLicenseFee, maxOrderQty, useCart, MOCK_USER, BrandLogo, AuthShell, AuthField, AUTH_LINK, SUPPORT_EMAIL, SupportLine, Recaptcha, recaptchaEnabled });
