// Payment / checkout page — dispatcher.
//
// State + validation live here. Each breakpoint renders an entirely
// separate component (PaymentPageDesktop / Tablet / Mobile) defined in
// its own file, so direct-edits on one breakpoint do NOT affect the others.
//
//   payment-desktop.jsx      → largeDesktop / desktop
//   payment-tablet.jsx       → tablet
//   payment-large-mobile.jsx → largeMobile
//   payment-small-mobile.jsx → smallMobile
//
// Tax = location-aware. Canada uses per-province HST/GST/PST derived from
// the postal code's first letter. The US uses approximate state-by-region
// rates keyed off the ZIP's first digit. Every other country in COUNTRIES
// has its standard national VAT / GST / sales-tax rate. See COUNTRY_TAX
// and US_ZIP_TAX below. All rates are a 2025-04 snapshot — Stripe Tax is
// the recommended live source (see SETUP.md Part C).

// ─── Tax engine ──────────────────────────────────────────────────────────────
// Canadian postal-code first letter → province / territory.
const POSTAL_TO_PROVINCE = {
  A: 'NL', B: 'NS', C: 'PE', E: 'NB',
  G: 'QC', H: 'QC', J: 'QC',
  K: 'ON', L: 'ON', M: 'ON', N: 'ON', P: 'ON',
  R: 'MB', S: 'SK', T: 'AB', V: 'BC',
  X: 'NT', Y: 'YT'
};

// Combined federal + provincial tax rates. HST provinces are a single
// merged rate; non-HST provinces show the GST + PST/QST sum as one number
// so the buyer sees one "tax" line either way.
//
// ⚠️  THESE RATES ARE HARDCODED. The calculation runs live on every keystroke,
//     but the rates themselves are a static snapshot. For a production
//     checkout, swap this table (and COUNTRY_TAX below) for a call to a
//     tax-rate service (Stripe Tax / Avalara / TaxJar) so rate changes
//     propagate automatically. Last reviewed: 2025-04.
const PROVINCE_TAX = {
  AB: 0.05,    BC: 0.12,    MB: 0.12,    NB: 0.15,
  NL: 0.15,    NS: 0.14,    NT: 0.05,    NU: 0.05,
  ON: 0.13,    PE: 0.15,    QC: 0.14975, SK: 0.11,
  YT: 0.05
};

const PROVINCE_NAME = {
  AB: 'Alberta', BC: 'British Columbia', MB: 'Manitoba',
  NB: 'New Brunswick', NL: 'Newfoundland & Labrador', NS: 'Nova Scotia',
  NT: 'Northwest Territories', NU: 'Nunavut', ON: 'Ontario',
  PE: 'Prince Edward Island', QC: 'Quebec', SK: 'Saskatchewan', YT: 'Yukon'
};

// Standard national VAT / GST / sales-tax rate per country. Countries that
// don't levy a general consumption tax (Hong Kong, most Gulf states, US — US
// is handled separately via ZIP prefix below) are intentionally omitted and
// fall through to a 0% "No tax" line. Snapshot as of 2025-04 — rates change,
// so this table is best-effort. Stripe Tax (see SETUP.md Part C) is the
// recommended long-term fix.
//
// Format: { rate, label }. `label` is the short name shown in the order
// summary (e.g. "VAT", "GST", "IVA", "Consumption tax").
const COUNTRY_TAX = {
  'Afghanistan': { rate: 0.10,    label: 'BRT' },
  'Albania': { rate: 0.20, label: 'VAT' },
  'Algeria': { rate: 0.19, label: 'VAT' },
  'Andorra': { rate: 0.045, label: 'IGI' },
  'Angola': { rate: 0.14, label: 'VAT' },
  'Antigua & Barbuda': { rate: 0.17, label: 'ABST' },
  'Argentina': { rate: 0.21, label: 'IVA' },
  'Armenia': { rate: 0.20, label: 'VAT' },
  'Australia': { rate: 0.10, label: 'GST' },
  'Austria': { rate: 0.20, label: 'VAT' },
  'Azerbaijan': { rate: 0.18, label: 'VAT' },
  'Bahamas': { rate: 0.10, label: 'VAT' },
  'Bahrain': { rate: 0.10, label: 'VAT' },
  'Bangladesh': { rate: 0.15, label: 'VAT' },
  'Barbados': { rate: 0.175, label: 'VAT' },
  'Belarus': { rate: 0.20, label: 'VAT' },
  'Belgium': { rate: 0.21, label: 'VAT' },
  'Belize': { rate: 0.125, label: 'GST' },
  'Benin': { rate: 0.18, label: 'VAT' },
  'Bhutan': { rate: 0.07, label: 'Sales tax' },
  'Bolivia': { rate: 0.13, label: 'IVA' },
  'Bosnia & Herzegovina': { rate: 0.17, label: 'VAT' },
  'Botswana': { rate: 0.14, label: 'VAT' },
  'Brazil': { rate: 0.17, label: 'ICMS' },
  'Bulgaria': { rate: 0.20, label: 'VAT' },
  'Burkina Faso': { rate: 0.18, label: 'VAT' },
  'Burundi': { rate: 0.18, label: 'VAT' },
  'Cambodia': { rate: 0.10, label: 'VAT' },
  'Cameroon': { rate: 0.1925, label: 'VAT' },
  'Cape Verde': { rate: 0.15, label: 'VAT' },
  'Central African Republic': { rate: 0.19, label: 'VAT' },
  'Chad': { rate: 0.18, label: 'VAT' },
  'Chile': { rate: 0.19, label: 'IVA' },
  'China': { rate: 0.13, label: 'VAT' },
  'Colombia': { rate: 0.19, label: 'IVA' },
  'Comoros': { rate: 0.10, label: 'TVA' },
  'Congo': { rate: 0.189, label: 'VAT' },
  'Congo (DRC)': { rate: 0.16, label: 'VAT' },
  'Costa Rica': { rate: 0.13, label: 'VAT' },
  'Croatia': { rate: 0.25, label: 'VAT' },
  'Cyprus': { rate: 0.19, label: 'VAT' },
  'Czechia': { rate: 0.21, label: 'VAT' },
  'Denmark': { rate: 0.25, label: 'VAT' },
  'Djibouti': { rate: 0.10, label: 'VAT' },
  'Dominica': { rate: 0.15, label: 'VAT' },
  'Dominican Republic': { rate: 0.18, label: 'ITBIS' },
  'Ecuador': { rate: 0.15, label: 'IVA' },
  'Egypt': { rate: 0.14, label: 'VAT' },
  'El Salvador': { rate: 0.13, label: 'VAT' },
  'Equatorial Guinea': { rate: 0.15, label: 'VAT' },
  'Eritrea': { rate: 0.05, label: 'Sales tax' },
  'Estonia': { rate: 0.22, label: 'VAT' },
  'Eswatini': { rate: 0.15, label: 'VAT' },
  'Ethiopia': { rate: 0.15, label: 'VAT' },
  'Fiji': { rate: 0.09, label: 'VAT' },
  'Finland': { rate: 0.255, label: 'VAT' },
  'France': { rate: 0.20, label: 'VAT' },
  'Gabon': { rate: 0.18, label: 'VAT' },
  'Gambia': { rate: 0.15, label: 'VAT' },
  'Georgia': { rate: 0.18, label: 'VAT' },
  'Germany': { rate: 0.19, label: 'VAT' },
  'Ghana': { rate: 0.15, label: 'VAT' },
  'Greece': { rate: 0.24, label: 'VAT' },
  'Grenada': { rate: 0.15, label: 'VAT' },
  'Guatemala': { rate: 0.12, label: 'IVA' },
  'Guinea': { rate: 0.18, label: 'VAT' },
  'Guinea-Bissau': { rate: 0.19, label: 'VAT' },
  'Guyana': { rate: 0.14, label: 'VAT' },
  'Haiti': { rate: 0.10, label: 'VAT' },
  'Honduras': { rate: 0.15, label: 'Sales tax' },
  'Hungary': { rate: 0.27, label: 'VAT' },
  'Iceland': { rate: 0.24, label: 'VAT' },
  'India': { rate: 0.18, label: 'GST' },
  'Indonesia': { rate: 0.11, label: 'VAT' },
  'Iran': { rate: 0.09, label: 'VAT' },
  'Ireland': { rate: 0.23, label: 'VAT' },
  'Israel': { rate: 0.18, label: 'VAT' },
  'Italy': { rate: 0.22, label: 'VAT' },
  'Ivory Coast': { rate: 0.18, label: 'VAT' },
  'Jamaica': { rate: 0.15, label: 'GCT' },
  'Japan': { rate: 0.10, label: 'Consumption tax' },
  'Jordan': { rate: 0.16, label: 'Sales tax' },
  'Kazakhstan': { rate: 0.12, label: 'VAT' },
  'Kenya': { rate: 0.16, label: 'VAT' },
  'Kosovo': { rate: 0.18, label: 'VAT' },
  'Kyrgyzstan': { rate: 0.12, label: 'VAT' },
  'Laos': { rate: 0.10, label: 'VAT' },
  'Latvia': { rate: 0.21, label: 'VAT' },
  'Lebanon': { rate: 0.11, label: 'VAT' },
  'Lesotho': { rate: 0.15, label: 'VAT' },
  'Liberia': { rate: 0.10, label: 'GST' },
  'Liechtenstein': { rate: 0.081, label: 'VAT' },
  'Lithuania': { rate: 0.21, label: 'VAT' },
  'Luxembourg': { rate: 0.17, label: 'VAT' },
  'Madagascar': { rate: 0.20, label: 'VAT' },
  'Malawi': { rate: 0.165, label: 'VAT' },
  'Malaysia': { rate: 0.08, label: 'SST' },
  'Maldives': { rate: 0.08, label: 'GST' },
  'Mali': { rate: 0.18, label: 'VAT' },
  'Malta': { rate: 0.18, label: 'VAT' },
  'Mauritania': { rate: 0.16, label: 'VAT' },
  'Mauritius': { rate: 0.15, label: 'VAT' },
  'Mexico': { rate: 0.16, label: 'IVA' },
  'Moldova': { rate: 0.20, label: 'VAT' },
  'Monaco': { rate: 0.20, label: 'VAT' },
  'Mongolia': { rate: 0.10, label: 'VAT' },
  'Montenegro': { rate: 0.21, label: 'VAT' },
  'Morocco': { rate: 0.20, label: 'VAT' },
  'Mozambique': { rate: 0.16, label: 'VAT' },
  'Myanmar': { rate: 0.05, label: 'Commercial tax' },
  'Namibia': { rate: 0.15, label: 'VAT' },
  'Nepal': { rate: 0.13, label: 'VAT' },
  'Netherlands': { rate: 0.21, label: 'VAT' },
  'New Zealand': { rate: 0.15, label: 'GST' },
  'Nicaragua': { rate: 0.15, label: 'VAT' },
  'Niger': { rate: 0.19, label: 'VAT' },
  'Nigeria': { rate: 0.075, label: 'VAT' },
  'North Macedonia': { rate: 0.18, label: 'VAT' },
  'Norway': { rate: 0.25, label: 'VAT' },
  'Oman': { rate: 0.05, label: 'VAT' },
  'Pakistan': { rate: 0.18, label: 'Sales tax' },
  'Palau': { rate: 0.10, label: 'PGST' },
  'Palestine': { rate: 0.16, label: 'VAT' },
  'Panama': { rate: 0.07, label: 'ITBMS' },
  'Papua New Guinea': { rate: 0.10, label: 'GST' },
  'Paraguay': { rate: 0.10, label: 'IVA' },
  'Peru': { rate: 0.18, label: 'IGV' },
  'Philippines': { rate: 0.12, label: 'VAT' },
  'Poland': { rate: 0.23, label: 'VAT' },
  'Portugal': { rate: 0.23, label: 'VAT' },
  'Romania': { rate: 0.19, label: 'VAT' },
  'Russia': { rate: 0.20, label: 'VAT' },
  'Rwanda': { rate: 0.18, label: 'VAT' },
  'Samoa': { rate: 0.15, label: 'VAGST' },
  'Saudi Arabia': { rate: 0.15, label: 'VAT' },
  'Senegal': { rate: 0.18, label: 'VAT' },
  'Serbia': { rate: 0.20, label: 'VAT' },
  'Seychelles': { rate: 0.15, label: 'VAT' },
  'Sierra Leone': { rate: 0.15, label: 'GST' },
  'Singapore': { rate: 0.09, label: 'GST' },
  'Slovakia': { rate: 0.23, label: 'VAT' },
  'Slovenia': { rate: 0.22, label: 'VAT' },
  'Solomon Islands': { rate: 0.15, label: 'GST' },
  'South Africa': { rate: 0.15, label: 'VAT' },
  'South Korea': { rate: 0.10, label: 'VAT' },
  'South Sudan': { rate: 0.18, label: 'VAT' },
  'Spain': { rate: 0.21, label: 'VAT' },
  'Sri Lanka': { rate: 0.18, label: 'VAT' },
  'Sudan': { rate: 0.17, label: 'VAT' },
  'Suriname': { rate: 0.10, label: 'VAT' },
  'Sweden': { rate: 0.25, label: 'VAT' },
  'Switzerland': { rate: 0.081, label: 'VAT' },
  'Taiwan': { rate: 0.05, label: 'VAT' },
  'Tajikistan': { rate: 0.14, label: 'VAT' },
  'Tanzania': { rate: 0.18, label: 'VAT' },
  'Thailand': { rate: 0.07, label: 'VAT' },
  'Togo': { rate: 0.18, label: 'VAT' },
  'Tonga': { rate: 0.15, label: 'Consumption tax' },
  'Trinidad & Tobago': { rate: 0.125, label: 'VAT' },
  'Tunisia': { rate: 0.19, label: 'VAT' },
  'Turkey': { rate: 0.20, label: 'VAT' },
  'Turkmenistan': { rate: 0.15, label: 'VAT' },
  'Uganda': { rate: 0.18, label: 'VAT' },
  'Ukraine': { rate: 0.20, label: 'VAT' },
  'United Arab Emirates': { rate: 0.05, label: 'VAT' },
  'United Kingdom': { rate: 0.20, label: 'VAT' },
  'Uruguay': { rate: 0.22, label: 'VAT' },
  'Uzbekistan': { rate: 0.12, label: 'VAT' },
  'Vanuatu': { rate: 0.15, label: 'VAT' },
  'Venezuela': { rate: 0.16, label: 'IVA' },
  'Vietnam': { rate: 0.10, label: 'VAT' },
  'Yemen': { rate: 0.05, label: 'Sales tax' },
  'Zambia': { rate: 0.16, label: 'VAT' },
  'Zimbabwe': { rate: 0.15, label: 'VAT' },
};

// US sales tax is set by each state (plus local jurisdictions). A perfect
// hardcoded table would need a ZIP → state map (~900 entries). For an honest
// in-page preview we instead key off the ZIP's first digit, which corresponds
// roughly to a US region, and use a representative state's combined
// state + average-local rate as the preview number. Stripe Tax does this
// properly down to the rooftop; until that's wired up, this is the snapshot.
const US_ZIP_TAX = {
  '0': 0.0625, // Northeast: MA/CT/RI/NH/ME/VT/NJ
  '1': 0.08,   // NY/PA/DE
  '2': 0.06,   // VA/DC/MD/NC/SC/WV
  '3': 0.07,   // FL/GA/AL/TN/MS
  '4': 0.0675, // OH/MI/IN/KY
  '5': 0.06,   // WI/MN/IA/MT/ND/SD
  '6': 0.0825, // IL/KS/MO/NE
  '7': 0.0825, // TX/OK/LA/AR
  '8': 0.07,   // AZ/CO/NM/NV/UT/ID/WY
  '9': 0.0825, // CA/OR/WA/AK/HI
};

// Validates that a postal code string looks plausibly real for its country.
// Canada: A1A 1A1 (or A1A1A1) with a first letter that maps to a real province.
// Other countries: basic length sanity check (no per-country regex database
// here — too much surface area for a demo). Returns true for empty so the
// "Required" message can take over for empties.
const isValidPostalCode = (country, postal) => {
  const c = (country || '').trim();
  const z = (postal || '').trim().toUpperCase();
  if (!z) return true;
  if (c === 'Canada') {
    if (!/^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/.test(z)) return false;
    return !!POSTAL_TO_PROVINCE[z.charAt(0)];
  }
  return z.length >= 3;
};

// Returns { rate, province, label } for a given country + postal-code pair.
// rate is a decimal (e.g. 0.13). label is the line-item caption ("HST 13%")
// or "Tax" when we can't resolve a region yet.
const fmtPct = (rate) => +(rate * 100).toFixed(3).replace(/\.?0+$/, '');

const computeTaxInfo = (country, postal) => {
  const c = (country || '').trim();
  const z = (postal || '').trim().toUpperCase();

  // ── Canada — per-province from postal code first letter ────────────────
  if (c === 'Canada') {
    const letter = z.charAt(0);
    const prov = POSTAL_TO_PROVINCE[letter];
    if (prov) {
      const rate = PROVINCE_TAX[prov];
      const pct  = fmtPct(rate);
      const isHst = ['NB', 'NL', 'NS', 'ON', 'PE'].includes(prov);
      const label = isHst
        ? `HST (${PROVINCE_NAME[prov]}) ${pct}%`
        : prov === 'QC'
          ? `GST + QST (${PROVINCE_NAME[prov]}) ${pct}%`
          : ['AB', 'NT', 'NU', 'YT'].includes(prov)
            ? `GST (${PROVINCE_NAME[prov]}) ${pct}%`
            : `GST + PST (${PROVINCE_NAME[prov]}) ${pct}%`;
      return { rate, province: prov, label };
    }
    return { rate: 0, province: null, label: 'HST (enter postal code)' };
  }

  // ── United States — approx. state-by-region from ZIP first digit ──────
  if (c === 'United States') {
    const digit = z.charAt(0);
    const rate = US_ZIP_TAX[digit];
    if (rate != null) {
      return { rate, province: null, label: `Sales tax (approx.) ${fmtPct(rate)}%` };
    }
    return { rate: 0, province: null, label: 'Sales tax (enter ZIP)' };
  }

  // ── Everywhere else — single national VAT / GST / sales-tax rate ──────
  const entry = COUNTRY_TAX[c];
  if (entry) {
    return {
      rate: entry.rate,
      province: null,
      label: `${entry.label} (${c}) ${fmtPct(entry.rate)}%`,
    };
  }

  // Country has no general consumption tax (or we don't have it in the table).
  return { rate: 0, province: null, label: c ? `No tax (${c})` : 'Tax' };
};

// ─── Card number formatter ────────────────────────────────────────────────────
// Groups digits in 4s ("1234 5678 9012 3456"). Caps at 19 digits to cover
// Amex's 4-6-5 and 19-digit Maestro PANs; visual grouping stays 4-by-4 for
// simplicity. Strips non-digits each call so backspacing through a space
// just removes one digit cleanly.
const formatCardNumber = (raw) => {
  const d = (raw || '').replace(/\D/g, '').slice(0, 19);
  return d.replace(/(\d{4})(?=\d)/g, '$1 ');
};

// Detect card brand from leading digits — used to pick the chip glyph.
const detectCardBrand = (raw) => {
  const d = (raw || '').replace(/\D/g, '');
  if (/^4/.test(d)) return 'visa';
  if (/^(5[1-5]|2[2-7])/.test(d)) return 'mastercard';
  if (/^3[47]/.test(d)) return 'amex';
  if (/^6(?:011|5)/.test(d)) return 'discover';
  return 'unknown';
};

// Auto-formats card expiry as "MM / YY".
const formatExp = (raw) => {
  const d = (raw || '').replace(/\D/g, '').slice(0, 4);
  if (d.length <= 2) return d;
  return d.slice(0, 2) + ' / ' + d.slice(2);
};

// ─── Country list ────────────────────────────────────────────────────────────
// Full ISO list, alphabetical. Used by every breakpoint's country <select>.
// Native browser <select> elements already do type-ahead — pressing "f" jumps
// to France, pressing "f" again advances to French Polynesia, etc.
const COUNTRIES = [
  'Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua & Barbuda',
  'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas',
  'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin',
  'Bhutan', 'Bolivia', 'Bosnia & Herzegovina', 'Botswana', 'Brazil', 'Brunei',
  'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Canada',
  'Cape Verde', 'Central African Republic', 'Chad', 'Chile', 'China',
  'Colombia', 'Comoros', 'Congo', 'Congo (DRC)', 'Costa Rica', 'Croatia',
  'Cuba', 'Cyprus', 'Czechia', 'Denmark', 'Djibouti', 'Dominica',
  'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea',
  'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', 'Finland', 'France',
  'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada',
  'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras',
  'Hong Kong', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq',
  'Ireland', 'Israel', 'Italy', 'Ivory Coast', 'Jamaica', 'Japan', 'Jordan',
  'Kazakhstan', 'Kenya', 'Kiribati', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Laos',
  'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein',
  'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives',
  'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico',
  'Micronesia', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Morocco',
  'Mozambique', 'Myanmar', 'Namibia', 'Nauru', 'Nepal', 'Netherlands',
  'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea',
  'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestine',
  'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland',
  'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Samoa', 'San Marino',
  'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone',
  'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia',
  'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan',
  'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Taiwan', 'Tajikistan',
  'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad & Tobago',
  'Tunisia', 'Turkey', 'Turkmenistan', 'Tuvalu', 'Uganda', 'Ukraine',
  'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay',
  'Uzbekistan', 'Vanuatu', 'Vatican City', 'Venezuela', 'Vietnam', 'Yemen',
  'Zambia', 'Zimbabwe'
];

Object.assign(window, {
  computeTaxInfo, formatCardNumber, formatExp, detectCardBrand,
  isValidPostalCode,
  POSTAL_TO_PROVINCE, PROVINCE_TAX, PROVINCE_NAME,
  COUNTRY_TAX, US_ZIP_TAX, COUNTRIES
});

// ─── Page dispatcher ─────────────────────────────────────────────────────────
const PaymentPage = ({ product, onBack, demoState = 'default', forceDone = false, forcedErrors }) => {
  const bp = useBreakpoint();
  const subtotal = product.price;

  const [fields, setFields] = React.useState({
    firstName: '', lastName: '', email: '',
    card: '', exp: '', cvc: '',
    country: 'Canada', zip: '',
    optIn: true
  });
  const [errors, setErrors] = React.useState({});
  const [done, setDone] = React.useState(
    forceDone ||
    demoState === 'success' ||
    (typeof window !== 'undefined' && /[?&]paid=1\b/.test(window.location.search))
  );
  const [submitting, setSubmitting] = React.useState(false);
  // Stripe PaymentIntent id from a successful live charge — handed to the
  // Thank-you page so the Worker can verify the payment before minting a token.
  // On a redirect return (?paid=1), Stripe also appends ?payment_intent=pi_… —
  // pick that up so the download still works after a wallet/redirect flow.
  const [paymentIntentId, setPaymentIntentId] = React.useState(() => {
    if (typeof window === 'undefined') return null;
    return new URLSearchParams(window.location.search).get('payment_intent');
  });

  // When Stripe is configured, the live Payment Element registers its confirm
  // function here so submit() can drive the real charge. In demo mode (no key)
  // this stays null and submit() falls back to the original success flow.
  const stripeConfirmRef = React.useRef(null);
  const stripeOn = typeof window.isStripeConfigured === 'function' && window.isStripeConfigured();

  // Track whether the user has touched the country picker themselves.
  // If they have, the IP-geolocation effect below leaves their choice alone.
  const countryTouchedRef = React.useRef(false);

  // ── Auto-detect default country via IP geolocation (one-shot, on mount) ──
  // Uses ipapi.co's free tier (CORS-enabled, no key). Silently no-ops on
  // network failure / rate-limit / unrecognized country — the hardcoded
  // 'Canada' default just stays in place.
  React.useEffect(() => {
    let cancelled = false;
    fetch('https://ipapi.co/json/')
      .then((r) => r.ok ? r.json() : null)
      .then((data) => {
        if (cancelled || !data || countryTouchedRef.current) return;
        const detected = data.country_name;
        if (!detected) return;
        const next = COUNTRIES.includes(detected) ? detected : 'United States';
        setFields((f) => countryTouchedRef.current ? f : { ...f, country: next });
      })
      .catch(() => { /* offline / blocked — keep default */ });
    return () => { cancelled = true; };
  }, []);

  // Live tax calc — recomputes on every keystroke of country / zip / price.
  const taxInfo = React.useMemo(
    () => computeTaxInfo(fields.country, fields.zip),
    [fields.country, fields.zip]
  );
  const tax   = +(subtotal * taxInfo.rate).toFixed(2);
  const total = +(subtotal + tax).toFixed(2);

  React.useEffect(() => {
    if (forceDone || demoState === 'success') {
      setDone(true);
    } else if (demoState === 'error') {
      setErrors({ firstName: 'Required', email: 'Required', card: 'Required' });
      setDone(false);
    } else {
      setErrors({});
      setDone(false);
    }
  }, [demoState, forceDone]);

  const set = (k) => (v) => {
    let next = v;
    if (k === 'exp')  next = formatExp(v);
    if (k === 'card') next = formatCardNumber(v);
    if (k === 'cvc')  next = (v || '').replace(/\D/g, '').slice(0, 4);
    if (k === 'zip')  next = (v || '').toUpperCase();
    if (k === 'country') countryTouchedRef.current = true;
    setFields((f) => ({ ...f, [k]: next }));
    // Live-validate the postal code as the user types — once they've entered
    // a plausibly-complete value (>=5 chars covers Canadian A1A1A1 and most
    // foreign formats), surface "Valid postal code required" if it doesn't
    // resolve. Country changes re-run the same check against the new zip.
    if (k === 'zip' || k === 'country') {
      const nextCountry = k === 'country' ? next : fields.country;
      const nextZip     = k === 'zip'     ? next : fields.zip;
      const longEnough = nextZip.trim().length >= 5;
      if (longEnough && !isValidPostalCode(nextCountry, nextZip)) {
        setErrors((e) => ({ ...e, zip: 'Valid postal code required' }));
      } else if (errors.zip) {
        setErrors((e) => ({ ...e, zip: null }));
      }
      if (k === 'country' && errors.country) setErrors((e) => ({ ...e, country: null }));
      return;
    }
    if (errors[k]) setErrors((e) => ({ ...e, [k]: null }));
  };

  const validate = () => {
    const e = {};
    if (!fields.firstName.trim()) e.firstName = 'Required';
    if (!fields.lastName.trim()) e.lastName = 'Required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fields.email.trim())) e.email = 'Valid email required';
    // Card / expiry / CVC are validated by the Stripe Payment Element itself
    // when Stripe is configured — skip the mock-field checks in that case.
    if (!stripeOn) {
      if (fields.card.replace(/\s/g, '').length < 12) e.card = 'Card number required';
      if (!/^\d{1,2}\s*\/\s*\d{2,4}$/.test(fields.exp.trim())) e.card = e.card || 'Expiry required';
      if (fields.cvc.length < 3) e.card = e.card || 'CVC required';
    }
    if (!fields.country.trim()) e.country = 'Required';
    if (!fields.zip.trim()) e.zip = 'Required';
    else if (!isValidPostalCode(fields.country, fields.zip)) e.zip = 'Valid postal code required';
    return e;
  };

  const submit = async () => {
    const e = validate();
    setErrors(e);
    if (Object.keys(e).length !== 0) return;

    // ── Live Stripe path ──────────────────────────────────────────────────
    if (stripeOn && stripeConfirmRef.current) {
      setSubmitting(true);
      const res = await stripeConfirmRef.current();
      setSubmitting(false);
      if (res && res.error) {
        setErrors((er) => ({ ...er, card: res.error.message || 'Payment could not be completed.' }));
        return;
      }
      // Success without a redirect (typical for cards).
      if (res && res.paymentIntentId) setPaymentIntentId(res.paymentIntentId);
      setDone(true);
      try {
        const list = JSON.parse(localStorage.getItem('spl_orders') || '[]');
        list.push({
          email: fields.email, name: `${fields.firstName} ${fields.lastName}`,
          product: product.title, total, date: new Date().toISOString()
        });
        localStorage.setItem('spl_orders', JSON.stringify(list));
      } catch {}
      return;
    }

    // ── Demo path (no Stripe key) — original behavior ──────────────────────
    setDone(true);
    try {
      const list = JSON.parse(localStorage.getItem('spl_orders') || '[]');
      list.push({
        email: fields.email, name: `${fields.firstName} ${fields.lastName}`,
        product: product.title, total, date: new Date().toISOString()
      });
      localStorage.setItem('spl_orders', JSON.stringify(list));
    } catch {}
  };

  const shared = {
    product, subtotal, tax, total, taxInfo,
    fields, errors, set, submit, done, onBack,
    stripeConfirmRef, submitting,
    paymentIntentId, email: fields.email,
  };

  if (bp === 'largeDesktop' || bp === 'desktop') return <PaymentPageDesktop {...shared} />;
  if (bp === 'tablet') return <PaymentPageTablet {...shared} />;
  if (bp === 'largeMobile') return <PaymentPageLargeMobile {...shared} />;
  return <PaymentPageSmallMobile {...shared} />;
};

Object.assign(window, { PaymentPage });
