/* ============================================================
   SVLM — Booking / Checkout / Confirm. Ported behavior 1:1 from
   public/index.html's openBooking()/buildBookingPayload()/renderCart()/
   placeOrder() (see that file for the original) — same cart-then-
   checkout model (nothing is booked until POST /orders at checkout),
   same price math, same real Google Maps PlaceAutocompleteElement for
   the address field (routes/config.js -> GET /config/public ->
   googleMapsApiKey, same key already fixed for the legacy-API issue
   in the vanilla app).
   ============================================================ */
const Pbk = window.PALETTE;

// The one place either screen works out what a customer owes.
//
// The booking form and Checkout each had their own copy of this, with the same row labels, and
// they disagreed: the booking form left the delivery fee out of both the VAT base and the total,
// so the price grew by the fee plus its VAT the moment the customer moved to the next screen.
//
// The delivery fee is VAT-able because it is our revenue, not a passthrough — it already carries
// the company's markup by the time it arrives here. So the full chain, end to end, is:
//
//   delivery cost   = what Deliveree quotes for the trip (or the rate card, if they can't be asked)
//   delivery fee    = delivery cost x (1 + markup%)        <- markup is Ops-editable
//   VAT             = (price after discount + delivery fee) x 7%
//   total           = price after discount + delivery fee + VAT
const VAT_RATE = 0.07;

function orderTotals(discountedSubtotal, deliveryFee = 0) {
  const net = Number(discountedSubtotal) + Number(deliveryFee || 0);
  const vat = Math.round(net * VAT_RATE * 100) / 100;
  return { net, vat, total: Math.round((net + vat) * 100) / 100 };
}

function clientMaxRedeemableCoins(price) {
  if (!Number.isFinite(price) || price < 700 || price > 30000) return 0;
  return (Math.floor(price / 1000) + 1) * 5;
}

function buildDateOptions() {
  const out = [];
  const today = new Date();
  const lang = getLang('customer');
  for (let i = 0; i < 60; i++) {
    const d = new Date(today);
    d.setDate(d.getDate() + i);
    const iso = d.toISOString().slice(0, 10);
    const label = i === 0 ? (t('customer', 'today_label') || 'Today')
      : i === 1 ? (t('customer', 'tomorrow_label') || 'Tomorrow')
      : d.toLocaleDateString(lang, { weekday: 'short', month: 'short', day: 'numeric' });
    out.push([iso, label]);
  }
  return out;
}

function buildTimeOptions() {
  const out = [];
  for (let h = 9; h <= 18; h++) {
    for (const m of [0, 30]) {
      if (h === 18 && m === 30) continue;
      const hh = String(h).padStart(2, '0'), mm = String(m).padStart(2, '0');
      out.push(`${hh}:${mm}`);
    }
  }
  return out;
}

/* The trip drawn on a map: a pin per address in driving order, and the real road line between
   them when the server managed to route it.

   The line comes from the quote rather than being computed here. The server already asks Google's
   Routes API for that route to price the trip, and it returns the encoded polyline with the fee —
   so drawing it costs nothing extra, and the customer sees exactly the route they are paying for.
   Without a polyline (no maps key, routing unavailable, or road-distance pricing switched off) the
   pins are still drawn and the map is still useful; a straight line is deliberately NOT drawn in
   its place, since that would show a route no vehicle can take. */
function RouteMap({ origin, destination, stops = [], polyline }) {
  const boxRef = React.useRef(null);
  const [ready, setReady] = React.useState(false);
  const [fetchedLine, setFetchedLine] = React.useState(null);
  const [summary, setSummary] = React.useState(null);

  // Ask for the route only when the quote didn't already come with one, so the common case costs
  // no extra call. Re-runs when an address changes, which is exactly when the drawn route is stale.
  React.useEffect(() => {
    if (polyline) { setFetchedLine(null); return; }
    if (!origin || !destination) return;
    let cancelled = false;
    api('/delivery/route', {
      method: 'POST',
      body: JSON.stringify({ origin, destination, stops }),
    })
      .then((r) => {
        if (cancelled || !r.route) return;
        setFetchedLine(r.route.polyline || null);
        setSummary({ distanceKm: r.route.distanceKm, durationSeconds: r.route.durationSeconds });
      })
      .catch(() => { /* the pins alone are still worth showing */ });
    return () => { cancelled = true; };
  }, [origin, destination, stops, polyline]);

  const line = polyline || fetchedLine;

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        if (!window.google?.maps) {
          const config = await api('/config/public');
          if (!config.googleMapsApiKey) return;
          await new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = `https://maps.googleapis.com/maps/api/js?key=${config.googleMapsApiKey}&libraries=places,geometry&v=beta`;
            script.onload = resolve;
            script.onerror = reject;
            document.head.appendChild(script);
          });
        }
        // The geometry library decodes the polyline. It may be absent if another component loaded
        // Maps first without it, so ask for it explicitly rather than assuming.
        if (!window.google?.maps?.geometry) await google.maps.importLibrary('geometry');
        if (!cancelled) setReady(true);
      } catch { /* no map is a cosmetic loss — the booking works without it */ }
    })();
    return () => { cancelled = true; };
  }, []);

  React.useEffect(() => {
    if (!ready || !boxRef.current || !origin || !destination) return;
    const points = [origin, ...stops, destination];
    const map = new google.maps.Map(boxRef.current, {
      disableDefaultUI: true, zoomControl: true, gestureHandling: 'cooperative',
    });

    const bounds = new google.maps.LatLngBounds();
    points.forEach((p, i) => {
      const position = { lat: Number(p.lat), lng: Number(p.lng) };
      bounds.extend(position);
      new google.maps.Marker({
        map, position,
        // Numbered so the order is unmistakable: a move with three stops is priced on the order
        // they are driven, and a customer who expected a different order should see it here.
        label: { text: i === 0 ? 'A' : (i === points.length - 1 ? 'B' : String(i)), color: '#fff', fontSize: '12px' },
      });
    });

    if (line && google.maps.geometry?.encoding) {
      new google.maps.Polyline({
        map, path: google.maps.geometry.encoding.decodePath(line),
        strokeColor: '#0f9d58', strokeOpacity: 0.9, strokeWeight: 4,
      });
    }

    map.fitBounds(bounds, 40);
  }, [ready, origin, destination, stops, line]);

  if (!ready) return null;
  return (
    <div style={{ marginBottom: 14 }}>
      <div ref={boxRef} style={{ width: '100%', height: 220, borderRadius: 12, overflow: 'hidden', border: `1px solid ${Pbk.line}` }} />
      {summary && (
        <div style={{ fontSize: 11.5, color: Pbk.muted, marginTop: 6, textAlign: 'center' }}>
          {summary.distanceKm.toFixed(1)} km
          {summary.durationSeconds ? ` · ~${Math.round(summary.durationSeconds / 60)} min` : ''}
        </div>
      )}
    </div>
  );
}

/* Real Google Maps address autocomplete, same PlaceAutocompleteElement API the vanilla app
   uses (the legacy Autocomplete widget 404s on this project's API key — see index.html's
   attachAddressAutocomplete() comment). Falls back to a plain text input if no key is set. */
// Whether Google will actually return address suggestions right now, checked once per page load and
// shared by every address field on the screen.
//
// This exists because the picker cannot be asked. gmp-place-autocomplete emits no error event of
// any kind, so a failing one looks exactly like an empty one: the customer types, no suggestions
// appear, nothing can be selected, gmp-select never fires, and the address is never captured. With
// no manual input on screen either, the booking simply cannot be completed — silently, for as long
// as the failure lasts.
//
// The failure that prompted this was the daily Places quota running out mid-day: fine every
// morning, dead every afternoon, which is what "sometimes it doesn't work" turned out to mean. One
// probe per page load is a rounding error against any quota, and it buys the difference between a
// dead form and a typed address.
let placesUsableProbe = null;
function placesUsable() {
  if (!placesUsableProbe) {
    placesUsableProbe = google.maps.places.AutocompleteSuggestion
      .fetchAutocompleteSuggestions({ input: 'a', includedRegionCodes: ['th'] })
      .then(() => true)
      .catch(() => false);
  }
  return placesUsableProbe;
}

function AddressField({ value, onChange }) {
  const wrapRef = React.useRef(null);
  const [mapsReady, setMapsReady] = React.useState(false);
  // Set when the customer chooses to type instead, or when the probe says suggestions are
  // unavailable. Either way the plain input takes over.
  const [manual, setManual] = React.useState(false);
  const [note, setNote] = React.useState('');

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const config = await api('/config/public');
        if (!config.googleMapsApiKey) { setNote('Map picker not configured yet — enter your address as text.'); return; }
        if (!window.google?.maps) {
          await new Promise((resolve) => {
            const script = document.createElement('script');
            script.src = `https://maps.googleapis.com/maps/api/js?key=${config.googleMapsApiKey}&libraries=places&v=beta`;
            script.onload = resolve;
            document.head.appendChild(script);
          });
        }
        if (cancelled || !window.google?.maps?.places) return;
        await google.maps.importLibrary('places');
        if (cancelled) return;

        // Maps loading is not the same as suggestions working — the quota that runs out is on the
        // Places request, not on the script. Without this check the customer gets a search box that
        // can never find anything.
        const usable = await placesUsable();
        if (cancelled) return;
        if (!usable) {
          setManual(true);
          setNote(t('customer', 'address_manual_note')
            || 'Address search is unavailable right now — please type your address. Our team will confirm the transport fee with you.');
        }
        setMapsReady(true);
      } catch { setNote(''); }
    })();
    return () => { cancelled = true; };
  }, []);

  React.useEffect(() => {
    if (!mapsReady || manual || !wrapRef.current) return;
    const picker = new google.maps.places.PlaceAutocompleteElement({ componentRestrictions: { country: 'th' } });
    picker.style.width = '100%';
    wrapRef.current.innerHTML = '';
    wrapRef.current.appendChild(picker);
    const onSelect = async ({ placePrediction }) => {
      const typed = placePrediction.text.toString();
      try {
        const place = placePrediction.toPlace();
        // 'location' is what makes distance-based delivery pricing possible at all — without it
        // the booking has no coordinates and no fee can be quoted (see deliveryPricing.js).
        await place.fetchFields({ fields: ['formattedAddress', 'location'] });
        const loc = place.location;
        onChange(place.formattedAddress || typed, loc
          ? { lat: typeof loc.lat === 'function' ? loc.lat() : loc.lat, lng: typeof loc.lng === 'function' ? loc.lng() : loc.lng }
          : null);
      } catch {
        // Unhandled, this rejection ate the whole selection: the customer picked an address and
        // nothing happened at all. Keeping what they chose, without coordinates, at least leaves a
        // bookable address for Ops to price by hand.
        onChange(typed, null);
      }
    };
    picker.addEventListener('gmp-select', onSelect);
    return () => picker.removeEventListener('gmp-select', onSelect);
  }, [mapsReady, manual]);

  const plainInput = (
    <input value={value} onChange={e => onChange(e.target.value, null)} placeholder={t('customer', 'area_placeholder')}
      style={{ width: '100%', boxSizing: 'border-box', height: 48, border: `1.5px solid ${Pbk.line}`, borderRadius: 14, padding: '0 14px', fontFamily: 'inherit', fontSize: 14.5, color: Pbk.ink }} />
  );

  return (
    <div>
      {mapsReady && !manual ? <div ref={wrapRef} /> : plainInput}
      {/* Always offered, not only when the search breaks. Plenty of real addresses are not in
          Google — new buildings, sois, rural plots — and a customer who cannot find theirs
          otherwise has no way to finish booking at all. Switching costs the transport quote, so it
          says so rather than letting the fee quietly vanish. */}
      {mapsReady && !manual && (
        <button type="button" onClick={() => setManual(true)}
          style={{ background: 'none', border: 'none', padding: '6px 0 0', color: Pbk.muted, fontSize: 11.5, cursor: 'pointer', fontFamily: 'inherit', textDecoration: 'underline' }}>
          {t('customer', 'address_type_manually') || "Can't find it? Type the address instead"}
        </button>
      )}
      {note && <p style={{ fontSize: 11.5, color: Pbk.faint, margin: '5px 0 0' }}>{note}</p>}
    </div>
  );
}

function Field({ label, children }) {
  return (
    <div style={{ marginBottom: 14 }}>
      <div style={{ fontSize: 12.5, fontWeight: 700, color: Pbk.muted, marginBottom: 6 }}>{label}</div>
      {children}
    </div>
  );
}

const selStyle = { width: '100%', boxSizing: 'border-box', height: 48, border: `1.5px solid ${Pbk.line}`, borderRadius: 14, padding: '0 12px', fontFamily: 'inherit', fontSize: 14.5, color: Pbk.ink, background: '#fff' };
const inputStyle = { width: '100%', boxSizing: 'border-box', height: 48, border: `1.5px solid ${Pbk.line}`, borderRadius: 14, padding: '0 14px', fontFamily: 'inherit', fontSize: 14.5, color: Pbk.ink };

function BookingScreen({ app, params }) {
  const [service, setService] = React.useState(null);
  const [step, setStep] = React.useState(1);
  const [tierIdx, setTierIdx] = React.useState(0);
  const [visits, setVisits] = React.useState(1);
  const [addons, setAddons] = React.useState({}); // id -> { checked, count }
  const [qty, setQty] = React.useState(1);
  const [cityId, setCityId] = React.useState(null);
  const [hours, setHours] = React.useState(2);
  const [date, setDate] = React.useState(buildDateOptions()[0][0]);
  const [time, setTime] = React.useState('10:00');
  const [area, setArea] = React.useState('');
  const [unitDetail, setUnitDetail] = React.useState('');
  const [notes, setNotes] = React.useState('');
  // Logistics: services with delivery_mode need an "other end" for the trip. 'pickup_return'
  // (laundry) takes items to a partner branch the customer picks; 'one_way' (Move House) takes
  // them to an address the customer types themselves.
  const [dropoffPoints, setDropoffPoints] = React.useState([]);
  // Distinguishes a branch the customer chose from one the nearest-first sort picked for them, so
  // re-sorting after an address change replaces a suggestion but never overrides a decision.
  const [branchPickedByUser, setBranchPickedByUser] = React.useState(false);
  const [dropoffLocationId, setDropoffLocationId] = React.useState('');
  const [destinationAddress, setDestinationAddress] = React.useState('');
  // Coordinates come from the Places picker; null when the customer typed a free-text address or
  // Maps isn't configured, in which case no fee is quoted and Ops prices the job by hand.
  const [originCoords, setOriginCoords] = React.useState(null);
  const [destinationCoords, setDestinationCoords] = React.useState(null);
  // Intermediate stops for a multi-address move, in driving order. Each is { address, lat, lng }
  // with the coordinates filled in only once the customer picks from the Places suggestions — a
  // stop that never resolves is dropped from the quote rather than shortening the route.
  const [stops, setStops] = React.useState([]);
  // How the customer wants the vehicle timed. 'schedule' matches how bookings have always worked,
  // so it stays the default and nothing changes for anyone who ignores this.
  const [timeMode, setTimeMode] = React.useState('schedule');
  const [vehicleTypes, setVehicleTypes] = React.useState([]);
  const [vehicleCode, setVehicleCode] = React.useState('');
  const [deliveryQuote, setDeliveryQuote] = React.useState(null);
  const [promoCode, setPromoCode] = React.useState('');
  const [promo, setPromo] = React.useState(null); // { id, kind, value, label }
  const [promoMsg, setPromoMsg] = React.useState('');
  const [coins, setCoins] = React.useState(0);
  const [wallet, setWallet] = React.useState(0);
  const [wantInvoice, setWantInvoice] = React.useState(false);
  const [billType, setBillType] = React.useState('individual');
  const [billName, setBillName] = React.useState('');
  const [taxId, setTaxId] = React.useState('');
  const [billBranch, setBillBranch] = React.useState(t('customer', 'head_office') || 'Head Office');
  const [billEmail, setBillEmail] = React.useState('');
  const [billAddress, setBillAddress] = React.useState('');
  const [stepErr, setStepErr] = React.useState('');

  // Move-in/Move-out bundle — books A/C Cleaning alongside the cleaning visit as a second,
  // separately-assignable job (aircon jobs route to aircon-role staff, cleaning to maids — one
  // booking can't be split across two staff members, so this becomes two cart items -> two
  // bookings under the same order, see addToCart below). Defaults to on, 2 units, the smallest
  // BTU tier — all adjustable, per the business's own bundle spec.
  const [includeAircon, setIncludeAircon] = React.useState(true);
  const [airconTiers, setAirconTiers] = React.useState([]);
  const [airconTierIdx, setAirconTierIdx] = React.useState(0);
  const [airconQty, setAirconQty] = React.useState(2);

  React.useEffect(() => {
    hydrateServiceDetail(params.id).then(setService);
    api('/customers/me/wallet').then(w => setWallet(w.coin_balance || 0)).catch(() => {});
  }, [params.id]);
  React.useEffect(() => { if (app.cities.length && !cityId) setCityId(app.cities[0].id); }, [app.cities]);
  React.useEffect(() => {
    if (params.id === 'movein') hydrateServiceDetail('aircon').then(s => setAirconTiers(s.tiers || []));
  }, [params.id]);
  // Vehicle rate card, for any service that involves moving goods.
  React.useEffect(() => {
    if (!service?.deliveryMode) return;
    api('/delivery/vehicle-types')
      .then((rows) => {
        setVehicleTypes(rows);
        setVehicleCode((prev) => (prev && rows.some(r => r.code === prev) ? prev : (rows[0]?.code || '')));
      })
      .catch(() => setVehicleTypes([]));
  }, [service?.deliveryMode]);

  // Live quote — re-runs whenever the vehicle, the customer's address, or the far end changes.
  // The far end is the chosen branch for laundry, or the typed destination for Move House.
  React.useEffect(() => {
    if (!service?.deliveryMode || !vehicleCode || !originCoords) { setDeliveryQuote(null); return; }
    const branch = dropoffPoints.find(p => String(p.id) === String(dropoffLocationId));
    const dest = service.deliveryMode === 'pickup_return'
      ? (branch && branch.lat != null ? { lat: Number(branch.lat), lng: Number(branch.lng) } : null)
      : destinationCoords;
    if (!dest) { setDeliveryQuote(null); return; }

    let cancelled = false;
    api('/delivery/quote', {
      method: 'POST',
      body: JSON.stringify({
        vehicle_code: vehicleCode,
        origin_lat: originCoords.lat, origin_lng: originCoords.lng,
        dest_lat: dest.lat, dest_lng: dest.lng,
        // Laundry is collected and returned — two dispatches, charged as two. Must match what
        // quoteForBookingItem derives server-side, or the price shown differs from the price taken.
        legs: service.deliveryMode === 'pickup_return' ? 2 : 1,
        // Only stops that resolved to coordinates — the server drops the rest anyway, and sending
        // half-typed ones would make the quote flicker while the customer is still choosing.
        stops: stops.filter(st => st.lat != null && st.lng != null),
      }),
    })
      .then((r) => { if (!cancelled) setDeliveryQuote(r.quote); })
      .catch(() => { if (!cancelled) setDeliveryQuote(null); });
    return () => { cancelled = true; };
  }, [service?.deliveryMode, vehicleCode, originCoords, destinationCoords, dropoffLocationId, dropoffPoints, stops]);

  // Branch list is city-scoped so the customer only sees points that can actually serve them, and
  // sorted by how far each one is from the address they gave.
  //
  // The coordinates used to be left off this call. The endpoint only measures distance when it is
  // given a point to measure from, so without them it fell back to sort_order — and every laundry
  // customer in Bangkok was handed the same branch, whichever side of the city they lived on. A
  // comment here claimed the API already returned them in order, which was true only of the
  // parameter nobody was passing.
  //
  // Re-runs when the address changes, because the nearest branch to an address is not knowable
  // until there is an address.
  React.useEffect(() => {
    if (service?.deliveryMode !== 'pickup_return' || !cityId) return;
    const near = originCoords && originCoords.lat != null && originCoords.lng != null
      ? `&lat=${encodeURIComponent(originCoords.lat)}&lng=${encodeURIComponent(originCoords.lng)}`
      : '';
    api(`/dropoff-locations?city_id=${encodeURIComponent(cityId)}${near}`)
      .then((rows) => {
        setDropoffPoints(rows);
        // Preselect the nearest so the common case is zero taps — but never overwrite a branch the
        // customer chose for themselves, which they may well have done for a reason we cannot see.
        setDropoffLocationId((prev) => {
          const stillListed = prev && rows.some(r => String(r.id) === String(prev));
          if (branchPickedByUser && stillListed) return prev;
          return rows[0] ? String(rows[0].id) : '';
        });
      })
      .catch(() => setDropoffPoints([]));
  }, [service?.deliveryMode, cityId, originCoords?.lat, originCoords?.lng]);

  if (!service) return <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', color: Pbk.muted }}>Loading…</div>;

  const tier = service.tiers[tierIdx] || service.tiers[0];
  const pkg = service.packages.find(p => p.visits === visits);
  // Crew tiers (e.g. Big Clean) imply a fixed duration same as a service-level fixed_duration_hours
  // would — the generic 2-5hr picker below doesn't know about per-tier hours, so a tier that sets
  // one wins the same read-only treatment.
  const fixedHours = service._raw?.fixed_duration_hours || tier.workingHours;
  const baseUnitPrice = pkg ? Math.round(tier.price * (1 - pkg.discount) * 100) / 100 : tier.price;
  const effectiveQty = visits > 1 ? visits : qty;
  const addonsTotal = Object.entries(addons).reduce((sum, [id, a]) => {
    if (!a.checked) return sum;
    const ad = service.addons.find(x => x.id === id);
    return sum + (ad ? Number(ad.price) : 0);
  }, 0);
  const isMoveinBundle = service.id === 'movein';
  const airconTier = airconTiers[airconTierIdx] || airconTiers[0];
  // Bundle price is just the sum of the two services' own prices (no separate bundle discount) —
  // matches how it'll actually be billed, since this becomes two independent cart items/bookings.
  const airconExtra = (isMoveinBundle && includeAircon && airconTier) ? Number(airconTier.price) * airconQty : 0;
  const subtotal = baseUnitPrice * effectiveQty + addonsTotal + airconExtra;
  const discount = promo ? Math.min(promo.kind === 'percent' ? Math.round(subtotal * Number(promo.value)) / 100 : Number(promo.value), subtotal) : 0;
  const discounted = subtotal - discount;
  // The delivery fee belongs in this total, not only in Checkout's.
  //
  // It used to be shown beside the vehicle picker and left out of the figures below, so a laundry
  // booking quoted at 313 for the trip displayed a total that did not contain it — and the price
  // then jumped by the fee plus its VAT on the very next screen, with nothing to explain the
  // increase. Both screens now run orderTotals() so they cannot say different things again.
  const deliveryFee = deliveryQuote ? Number(deliveryQuote.feeThb) || 0 : 0;
  const { vat, total } = orderTotals(discounted, deliveryFee);
  const coinsCap = Math.min(clientMaxRedeemableCoins(tier.price), wallet);

  const applyPromo = async () => {
    setPromoMsg('');
    if (!promoCode.trim()) { setPromo(null); return; }
    const promos = await api('/promotions/active');
    const match = promos.find(p => p.id.toLowerCase() === promoCode.trim().toLowerCase());
    if (!match) { setPromo(null); setPromoMsg(t('customer', 'promo_invalid') || 'Invalid promo code'); return; }
    setPromo({ id: match.id, kind: match.kind, value: match.value });
    const label = match.kind === 'percent' ? `${match.value}%` : baht(match.value);
    setPromoMsg(`${t('customer', 'promo_applied') || 'Applied'} ${label}`);
  };

  const toAddonPayload = () => Object.entries(addons)
    .filter(([, a]) => a.checked)
    .map(([id, a]) => ({ addon_id: id, qty: 1, item_count: a.count ? Number(a.count) : null }));

  const addToCart = () => {
    if (!date || !time) { setStepErr(t('customer', 'booking_error_datetime') || 'Please choose a date and time.'); return; }
    const addonIds = toAddonPayload();
    const payload = {
      service_id: service.id,
      tier_label: tier.label,
      unit_price_thb: baseUnitPrice,
      city_id: cityId,
      duration_hours: fixedHours || hours,
      quantity: effectiveQty,
      service_package_id: pkg ? pkg.id : null,
      scheduled_date: date,
      scheduled_time: time,
      area_label: area,
      unit_detail: unitDetail || undefined,
      notes,
      // Logistics — only sent for services that use delivery; ignored server-side otherwise.
      dropoff_location_id: service.deliveryMode === 'pickup_return' && dropoffLocationId ? Number(dropoffLocationId) : undefined,
      destination_address: service.deliveryMode === 'one_way' && destinationAddress ? destinationAddress : undefined,
      // The server re-quotes from these rather than trusting any price sent by the client.
      delivery_vehicle_code: service.deliveryMode && vehicleCode ? vehicleCode : undefined,
      origin_lat: service.deliveryMode && originCoords ? originCoords.lat : undefined,
      origin_lng: service.deliveryMode && originCoords ? originCoords.lng : undefined,
      destination_lat: service.deliveryMode === 'one_way' && destinationCoords ? destinationCoords.lat : undefined,
      destination_lng: service.deliveryMode === 'one_way' && destinationCoords ? destinationCoords.lng : undefined,
      // Only stops that resolved to coordinates are worth storing: neither the routing service nor
      // Deliveree can use an address without them, so an unresolved one would be a stop the driver
      // is never actually sent to.
      delivery_stops: service.deliveryMode === 'one_way'
        ? stops.filter(st => st.lat != null && st.lng != null).map(st => ({ address: st.address, lat: st.lat, lng: st.lng }))
        : undefined,
      delivery_time_mode: service.deliveryMode ? timeMode : undefined,
      requested_staff_id: null,
      promo_id: promo?.id || null,
      addon_ids: addonIds.length ? addonIds : undefined,
      coins_redeemed: Number(coins) || undefined,
      _service_name: app.pick(service.en, service.th),
      _addon_summary: addonIds.map(a => app.pick(...(() => { const ad = service.addons.find(x => x.id === a.addon_id); return [ad?.en, ad?.th]; })())).filter(Boolean).join(', '),
      _addons_total: addonsTotal,
      // Display-only, like the other underscore-prefixed fields: lets Checkout show the fee and a
      // matching total. The server ignores it and re-quotes from the coordinates, so a tampered
      // value can never change what's actually charged.
      _delivery_fee: deliveryQuote ? deliveryQuote.feeThb : 0,
    };
    if (wantInvoice) {
      payload.bill_type = billType;
      payload.bill_address = billAddress;
      payload.bill_email = billEmail || undefined;
      payload.bill_name = billName;
      payload.tax_id = taxId;
      if (billType === 'corporate') payload.bill_branch = billBranch;
    }

    if (isMoveinBundle && includeAircon && airconTier) {
      // Separate cart item -> separate booking -> routes to aircon-role staff independently of
      // the cleaning booking above (one booking can only ever have one assigned staff member).
      // No promo_id/coins_redeemed here — both already spent on the primary item above; applying
      // either to both would double-count the discount/redemption.
      const airconPayload = {
        service_id: 'aircon',
        tier_label: airconTier.label,
        unit_price_thb: Number(airconTier.price),
        city_id: cityId,
        duration_hours: airconTier.workingHours || undefined,
        quantity: airconQty,
        scheduled_date: date,
        scheduled_time: time,
        area_label: area,
        unit_detail: unitDetail || undefined,
        notes,
        requested_staff_id: null,
        promo_id: null,
        _service_name: app.lang === 'th' ? 'ล้างแอร์' : 'A/C Cleaning',
        _addon_summary: '',
        _addons_total: 0,
      };
      app.addItemsToCart([payload, airconPayload]);
    } else {
      app.addToCart(payload);
    }
    app.go('checkout');
  };

  return (
    <div>
      <AppHeader onBack={app.back} title={`${t('customer', 'book_prefix') || 'Book:'} ${app.pick(service.en, service.th)}`} />
      <div style={{ padding: '0 20px 120px' }}>
        <p style={{ fontSize: 12.5, fontWeight: 700, color: Pbk.primary, margin: '0 0 14px' }}>
          {step === 1
            ? (app.lang === 'th' ? 'ขั้นตอนที่ 1 จาก 2 — เลือกตัวเลือก' : 'Step 1 of 2 — Choose options')
            : (app.lang === 'th' ? 'ขั้นตอนที่ 2 จาก 2 — เวลาและรายละเอียด' : 'Step 2 of 2 — Schedule & details')}
        </p>

        {step === 1 && (
          <>
            {/* A transport-priced service has no tiers of its own, so data-adapter.js synthesises a
                single "Standard" one at its ฿0 from_price. Offering a one-option dropdown reading
                "Standard — ฿0" suggests the job itself is free, when the real price is the
                vehicle/distance fee chosen further down. Nothing to pick, so nothing is shown. */}
            {!(isTransportPriced(service) && service.tiers.length === 1) && (
              <Field label={t('customer', 'tier_label') || 'Tier'}>
                <select value={tierIdx} onChange={e => setTierIdx(Number(e.target.value))} style={selStyle}>
                  {service.tiers.map((tr, i) => (
                    <option key={i} value={i}>
                      {tr.label} — {baht(tr.price)}{tr.maidsNeeded ? ` (${tr.maidsNeeded} ${app.lang === 'th' ? 'แม่บ้าน' : 'maids'} · ${tr.workingHours} ${t('customer', 'hours_unit') || 'hours'})` : ''}
                    </option>
                  ))}
                </select>
              </Field>
            )}

            {service.packages.length > 0 && (
              <Field label={t('customer', 'package_options') || 'Package options'}>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                  <Chip on={visits === 1} onClick={() => setVisits(1)}>{t('customer', 'single_visit') || 'Single visit'}</Chip>
                  {service.packages.map(p => (
                    <Chip key={p.visits} on={visits === p.visits} onClick={() => setVisits(p.visits)}>
                      {app.pick(p.en, p.th)} — {t('customer', 'save_percent') || 'Save'} {Math.round(p.discount * 100)}%
                    </Chip>
                  ))}
                </div>
                {visits > 1 && pkg && (
                  <p style={{ fontSize: 12, color: Pbk.faint, marginTop: 8 }}>
                    {(t('customer', 'total_price') || 'Total')}: {baht(Math.round(tier.price * visits * (1 - pkg.discount)))}
                  </p>
                )}
              </Field>
            )}

            {visits === 1 && (
              <Field label={t('customer', 'quantity_label') || 'Quantity'}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
                  <button onClick={() => setQty(q => Math.max(1, q - 1))} style={{ width: 40, height: 40, borderRadius: 12, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer', fontSize: 18, fontWeight: 700 }}>−</button>
                  <span style={{ fontSize: 16, fontWeight: 800, minWidth: 24, textAlign: 'center' }}>{qty}</span>
                  <button onClick={() => setQty(q => q + 1)} style={{ width: 40, height: 40, borderRadius: 12, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer', fontSize: 18, fontWeight: 700 }}>+</button>
                </div>
              </Field>
            )}

            {service.addons.length > 0 && (
              <Field label={t('customer', 'add_extras_label') || 'Add extras'}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {service.addons.map(a => {
                    const cur = addons[a.id] || { checked: false, count: '' };
                    return (
                      <label key={a.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, padding: 12, borderRadius: 14, border: `1.5px solid ${cur.checked ? Pbk.primary : Pbk.line}`, background: cur.checked ? Pbk.tintBlue : '#fff', cursor: 'pointer' }}>
                        <input type="checkbox" checked={cur.checked} onChange={e => setAddons(s => ({ ...s, [a.id]: { ...cur, checked: e.target.checked } }))} style={{ marginTop: 3 }} />
                        <div style={{ flex: 1 }}>
                          <div style={{ fontSize: 13.5, fontWeight: 700, color: Pbk.ink }}>{app.pick(a.en, a.th)}</div>
                          {a.maxItemCount && cur.checked && (
                            <input type="number" min="1" max={a.maxItemCount} value={cur.count} placeholder={`up to ${a.maxItemCount}`}
                              onChange={e => setAddons(s => ({ ...s, [a.id]: { ...cur, count: e.target.value } }))}
                              style={{ marginTop: 6, width: 100, height: 34, borderRadius: 10, border: `1px solid ${Pbk.line}`, padding: '0 8px' }} />
                          )}
                        </div>
                        <b style={{ fontSize: 13.5, color: Pbk.ink }}>{baht(a.price)}</b>
                      </label>
                    );
                  })}
                </div>
              </Field>
            )}

            {isMoveinBundle && airconTiers.length > 0 && (
              <Field label={app.lang === 'th' ? 'ล้างแอร์เพิ่ม (แนะนำ)' : 'Add A/C Cleaning (recommended)'}>
                <label style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 12, borderRadius: 14, border: `1.5px solid ${includeAircon ? Pbk.primary : Pbk.line}`, background: includeAircon ? Pbk.tintBlue : '#fff', cursor: 'pointer', marginBottom: includeAircon ? 10 : 0 }}>
                  <input type="checkbox" checked={includeAircon} onChange={e => setIncludeAircon(e.target.checked)} />
                  <span style={{ fontSize: 13.5, fontWeight: 700, color: Pbk.ink }}>
                    {app.lang === 'th' ? 'รวมล้างแอร์ในการจองนี้ (ช่างแอร์จะรับงานแยกต่างหาก)' : 'Include A/C cleaning with this booking (an aircon technician handles it separately)'}
                  </span>
                </label>
                {includeAircon && (
                  <>
                    <select value={airconTierIdx} onChange={e => setAirconTierIdx(Number(e.target.value))} style={{ ...selStyle, marginBottom: 10 }}>
                      {airconTiers.map((tr, i) => <option key={i} value={i}>{tr.label} — {baht(tr.price)}</option>)}
                    </select>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
                      <span style={{ fontSize: 13, color: Pbk.muted }}>{app.lang === 'th' ? 'จำนวนเครื่อง' : 'Number of units'}</span>
                      <button onClick={() => setAirconQty(q => Math.max(1, q - 1))} style={{ width: 36, height: 36, borderRadius: 10, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer', fontSize: 16, fontWeight: 700 }}>−</button>
                      <span style={{ fontSize: 15, fontWeight: 800, minWidth: 20, textAlign: 'center' }}>{airconQty}</span>
                      <button onClick={() => setAirconQty(q => q + 1)} style={{ width: 36, height: 36, borderRadius: 10, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer', fontSize: 16, fontWeight: 700 }}>+</button>
                    </div>
                  </>
                )}
              </Field>
            )}

            <Btn onClick={() => setStep(2)}>{t('customer', 'booking_next') || 'Next'}</Btn>
          </>
        )}

        {step === 2 && (
          <>
            <Field label={t('customer', 'city_label') || 'City'}>
              <select value={cityId || ''} onChange={e => setCityId(e.target.value)} style={selStyle}>
                {app.cities.map(c => <option key={c.id} value={c.id}>{app.pick(c.en, c.th)}</option>)}
              </select>
            </Field>

            <Field label={t('customer', 'duration_label') || 'Duration'}>
              {fixedHours
                ? <input disabled value={`${fixedHours} ${t('customer', 'hours_unit') || 'hours'}`} style={{ ...inputStyle, background: Pbk.lineSoft, color: Pbk.muted }} />
                : (
                  <select value={hours} onChange={e => setHours(Number(e.target.value))} style={selStyle}>
                    {[2, 3, 4, 5].map(h => <option key={h} value={h}>{h} {t('customer', 'hours_unit') || 'hours'}</option>)}
                  </select>
                )}
            </Field>

            <div style={{ display: 'flex', gap: 10 }}>
              <div style={{ flex: 1 }}>
                <Field label={t('customer', 'date_label') || 'Date'}>
                  <select value={date} onChange={e => setDate(e.target.value)} style={selStyle}>
                    {buildDateOptions().map(([iso, label]) => <option key={iso} value={iso}>{label}</option>)}
                  </select>
                </Field>
              </div>
              <div style={{ flex: 1 }}>
                <Field label={t('customer', 'time_label') || 'Time'}>
                  <select value={time} onChange={e => setTime(e.target.value)} style={selStyle}>
                    {buildTimeOptions().map(tm => <option key={tm} value={tm}>{tm}</option>)}
                  </select>
                </Field>
              </div>
            </div>

            <Field label={t('customer', 'area_label') || 'Area / address'}>
              <AddressField value={area} onChange={(v, coords) => { setArea(v); setOriginCoords(coords || null); }} />
            </Field>

            <Field label={t('customer', 'unit_detail_label') || (app.lang === 'th' ? 'เลขที่ / ชั้น / อาคาร' : 'House no. / Floor / Building')}>
              <input value={unitDetail} onChange={e => setUnitDetail(e.target.value)}
                placeholder={t('customer', 'unit_detail_placeholder') || (app.lang === 'th' ? 'เช่น เลขที่ 12/3 ชั้น 4 อาคาร A' : 'e.g. No. 12/3, Floor 4, Building A')}
                style={inputStyle} />
            </Field>

            {service.deliveryMode === 'pickup_return' && (
              <Field label={t('customer', 'dropoff_point_label') || 'Drop-off branch'}>
                {dropoffPoints.length === 0 ? (
                  <div style={{ fontSize: 12.5, color: Pbk.muted }}>
                    {t('customer', 'dropoff_none_available') || 'No branches available in this city yet — our team will arrange collection with you.'}
                  </div>
                ) : (
                  <>
                    <select value={dropoffLocationId} onChange={e => { setBranchPickedByUser(true); setDropoffLocationId(e.target.value); }} style={selStyle}>
                      {dropoffPoints.map(p => (
                        <option key={p.id} value={p.id}>
                          {p.brand} {app.pick(p.name_en || p.name, p.name)}{p.distance_km != null ? ` — ${Number(p.distance_km).toFixed(1)} km` : ''}
                        </option>
                      ))}
                    </select>
                    <div style={{ fontSize: 11.5, color: Pbk.muted, marginTop: 5 }}>
                      {t('customer', 'dropoff_point_hint') || 'We collect your items and take them here, then bring them back to you.'}
                    </div>
                  </>
                )}
              </Field>
            )}

            {service.deliveryMode === 'one_way' && (
              <>
                {/* Stops sit between the pickup and the destination, which is the order they are
                    driven and the order Deliveree charges them in — so they are rendered between
                    the two fields rather than tucked underneath the destination. */}
                {stops.map((st, i) => (
                  <Field key={i} label={`${t('customer', 'stop_label') || 'Stop'} ${i + 1}`}>
                    <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start', width: '100%' }}>
                      {/* minWidth: 0 is what actually lets this shrink. A flex item defaults to
                          min-width: auto — never smaller than its content — and the Places
                          autocomplete inside is 100% wide, so without this the row is wider than
                          the screen and the remove button sits off the edge. */}
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <AddressField
                          value={st.address}
                          onChange={(v, coords) => setStops(prev => prev.map((p2, j) => (
                            j === i ? { address: v, lat: coords ? coords.lat : null, lng: coords ? coords.lng : null } : p2
                          )))}
                        />
                      </div>
                      <button type="button"
                        onClick={() => setStops(prev => prev.filter((_, j) => j !== i))}
                        style={{ background: 'none', border: `1px solid ${Pbk.line}`, borderRadius: 8, padding: '10px 12px', color: Pbk.muted, cursor: 'pointer', lineHeight: 1, flexShrink: 0 }}
                        aria-label={t('customer', 'stop_remove') || 'Remove stop'}>×</button>
                    </div>
                  </Field>
                ))}

                <Field label={t('customer', 'destination_label') || 'Destination address'}>
                  <AddressField value={destinationAddress} onChange={(v, coords) => { setDestinationAddress(v); setDestinationCoords(coords || null); }} />
                  <div style={{ fontSize: 11.5, color: Pbk.muted, marginTop: 5 }}>
                    {t('customer', 'destination_hint') || 'Where your items are being moved to.'}
                  </div>
                </Field>

                {stops.length < 10 && (
                  <button type="button"
                    onClick={() => setStops(prev => [...prev, { address: '', lat: null, lng: null }])}
                    style={{ background: 'none', border: `1px dashed ${Pbk.line}`, borderRadius: 10, padding: '10px 14px', color: Pbk.muted, cursor: 'pointer', fontSize: 13, width: '100%', marginBottom: 14 }}>
                    + {t('customer', 'stop_add') || 'Add a stop'}
                  </button>
                )}

                <Field label={t('customer', 'time_mode_label') || 'When do you need the vehicle?'}>
                  <select value={timeMode} onChange={e => setTimeMode(e.target.value)} style={selStyle}>
                    <option value="schedule">{t('customer', 'time_mode_schedule') || (app.lang === 'th' ? 'จองล่วงหน้า' : 'Schedule')}</option>
                    <option value="quick">{t('customer', 'time_mode_quick') || (app.lang === 'th' ? 'เรียกรถทันที' : 'Quick — as soon as possible')}</option>
                    <option value="full_day">{t('customer', 'time_mode_full_day') || (app.lang === 'th' ? 'เหมาทั้งวัน' : 'Full day')}</option>
                    <option value="fixed_route">{t('customer', 'time_mode_fixed_route') || (app.lang === 'th' ? 'เส้นทางราคาคงที่' : 'Fixed price route')}</option>
                  </select>
                  {(timeMode === 'full_day' || timeMode === 'fixed_route') && (
                    <div style={{ fontSize: 11.5, color: Pbk.muted, marginTop: 6 }}>
                      {t('customer', 'time_mode_manual_hint')
                        || (app.lang === 'th'
                          ? 'ทีมงานจะติดต่อยืนยันราคาและเวลากับคุณก่อนจัดรถ'
                          : 'Our team will confirm the price and timing with you before the vehicle is arranged.')}
                    </div>
                  )}
                </Field>
              </>
            )}

            {/* Only drawn once both ends resolved to coordinates — a map of one pin tells the
                customer nothing they didn't already type. */}
            {service.deliveryMode === 'one_way' && originCoords && destinationCoords && (
              <RouteMap
                origin={originCoords}
                destination={destinationCoords}
                stops={stops.filter(st => st.lat != null && st.lng != null)}
                polyline={deliveryQuote?.polyline || null}
              />
            )}

            {/* A pickup_return trip needs a branch as its far end, so with no branches in this city
                there is nothing to quote against — showing a vehicle picker there would promise a
                fee that can never appear. Ops arranges those collections by hand instead. */}
            {service.deliveryMode && vehicleTypes.length > 0
              && !(service.deliveryMode === 'pickup_return' && dropoffPoints.length === 0) && (
              <Field label={t('customer', 'vehicle_label') || 'Delivery vehicle'}>
                <select value={vehicleCode} onChange={e => setVehicleCode(e.target.value)} style={selStyle}>
                  {vehicleTypes.map(v => (
                    <option key={v.code} value={v.code}>{app.pick(v.name_en, v.name_th)}</option>
                  ))}
                </select>
                {deliveryQuote ? (
                  <div style={{ fontSize: 12, color: Pbk.muted, marginTop: 6 }}>
                    {(t('customer', 'delivery_fee_label') || 'Delivery fee')}: <b style={{ color: Pbk.ink }}>{fmtThb(deliveryQuote.feeThb)}</b>
                    {/* A live Deliveree quote has no distance of its own to report — their price
                        already covers the road route — and Number(null) rendered that as a
                        confident "0.0 km" sitting next to a fee of several hundred baht. Only
                        state a distance when one was actually measured. */}
                    <span style={{ color: Pbk.faint }}>
                      {Number.isFinite(Number(deliveryQuote.distanceKm)) && deliveryQuote.distanceKm !== null
                        ? ` · ${Number(deliveryQuote.distanceKm).toFixed(1)} km` : ''}
                      {deliveryQuote.legs > 1 ? ' · ' + (t('customer', 'round_trip') || 'round trip') : ''}
                    </span>
                  </div>
                ) : (
                  <div style={{ fontSize: 11.5, color: Pbk.faint, marginTop: 6 }}>
                    {t('customer', 'delivery_fee_pending') || 'Pick your address from the suggestions to see the delivery fee — otherwise our team will confirm it with you.'}
                  </div>
                )}
              </Field>
            )}

            <Field label={t('customer', 'notes_label') || 'Notes'}>
              <textarea value={notes} onChange={e => setNotes(e.target.value)} rows={2} style={{ ...inputStyle, height: 'auto', padding: 12, resize: 'vertical' }} />
            </Field>

            <Field label={t('customer', 'promo_code_label') || 'Promo code'}>
              <div style={{ display: 'flex', gap: 8 }}>
                <input value={promoCode} onChange={e => setPromoCode(e.target.value)} placeholder={t('customer', 'promo_code_placeholder') || 'Enter code'} style={{ ...inputStyle, flex: 1 }} />
                <button onClick={applyPromo} style={{ padding: '0 18px', borderRadius: 14, border: `1.5px solid ${Pbk.primary}`, background: '#fff', color: Pbk.primary, fontWeight: 700, cursor: 'pointer' }}>{t('customer', 'apply') || 'Apply'}</button>
              </div>
              {promoMsg && <p style={{ fontSize: 12, color: promo ? Pbk.mint : '#d23a3a', marginTop: 6 }}>{promoMsg}</p>}
            </Field>

            <Field label={`🪙 ${t('customer', 'use_coins_label') || 'Use coins'}`}>
              <input type="number" min="0" max={coinsCap} value={coins} onChange={e => setCoins(e.target.value)} style={inputStyle} />
              <p style={{ fontSize: 11.5, color: Pbk.faint, marginTop: 5 }}>{t('customer', 'coins_available') || 'Available'}: {wallet} — {t('customer', 'coins_max_for_booking') || 'Max for this booking'}: {coinsCap}</p>
            </Field>

            <label style={{ display: 'flex', alignItems: 'flex-start', gap: 8, margin: '14px 0' }}>
              <input type="checkbox" checked={wantInvoice} onChange={e => setWantInvoice(e.target.checked)} style={{ marginTop: 3 }} />
              <span style={{ fontSize: 13.5, fontWeight: 600, color: Pbk.ink }}>{t('customer', 'request_tax_invoice') || 'Request tax invoice'}</span>
            </label>
            {wantInvoice && (
              <div style={{ background: Pbk.lineSoft, borderRadius: 14, padding: 14, marginBottom: 14 }}>
                <Field label={t('customer', 'billing_type') || 'Billing type'}>
                  <select value={billType} onChange={e => setBillType(e.target.value)} style={selStyle}>
                    <option value="individual">{t('customer', 'individual') || 'Individual'}</option>
                    <option value="corporate">{t('customer', 'corporate') || 'Corporate'}</option>
                  </select>
                </Field>
                <Field label={billType === 'corporate' ? (t('customer', 'company_name') || 'Company name') : (t('customer', 'full_name') || 'Full name')}>
                  <input value={billName} onChange={e => setBillName(e.target.value)} style={inputStyle} />
                </Field>
                <Field label={billType === 'corporate' ? (t('customer', 'tax_id') || 'Tax ID') : (t('customer', 'national_id_optional') || 'National ID (optional)')}>
                  <input value={taxId} onChange={e => setTaxId(e.target.value)} style={inputStyle} />
                </Field>
                {billType === 'corporate' && (
                  <Field label={t('customer', 'branch') || 'Branch'}>
                    <input value={billBranch} onChange={e => setBillBranch(e.target.value)} style={inputStyle} />
                  </Field>
                )}
                <Field label={t('customer', 'bill_email_optional') || 'Email (optional)'}>
                  <input type="email" value={billEmail} onChange={e => setBillEmail(e.target.value)} style={inputStyle} />
                </Field>
                <Field label={t('customer', 'billing_address') || 'Billing address'}>
                  <textarea value={billAddress} onChange={e => setBillAddress(e.target.value)} rows={2} style={{ ...inputStyle, height: 'auto', padding: 12, resize: 'vertical' }} />
                </Field>
              </div>
            )}

            <div style={{ background: '#fff', borderRadius: 16, padding: 14, margin: '6px 0 16px', boxShadow: '0 1px 2px rgba(11,37,69,0.04), 0 6px 18px rgba(11,37,69,0.05)' }}>
              {airconExtra > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: Pbk.faint, marginBottom: 6 }}><span>{app.lang === 'th' ? `รวมค่าล้างแอร์ (${airconQty} เครื่อง)` : `Incl. A/C cleaning (${airconQty} units)`}</span><span>{baht(airconExtra)}</span></div>}
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'subtotal_label') || 'Subtotal'}</span><span>{baht(subtotal)}</span></div>
              {discount > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.mint, marginBottom: 6 }}><span>{t('customer', 'discount_label') || 'Discount'}</span><span>-{baht(discount)}</span></div>}
              {/* Listed on its own line rather than folded into the subtotal, so the trip is
                  visibly separate from the service being bought. */}
              {deliveryFee > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'delivery_fee_label') || 'Delivery fee'}</span><span>{baht(deliveryFee)}</span></div>}
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'vat_label') || 'VAT'}</span><span>{baht(vat)}</span></div>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 16, fontWeight: 800, color: Pbk.ink, paddingTop: 8, borderTop: `1px solid ${Pbk.lineSoft}` }}><span>{t('customer', 'total_label') || 'Total'}</span><span style={{ color: Pbk.primary }}>{baht(total)}</span></div>
            </div>

            {stepErr && <p style={{ color: '#d23a3a', fontSize: 13, fontWeight: 600 }}>{stepErr}</p>}
            <div style={{ display: 'flex', gap: 10 }}>
              <Btn variant="ghost" onClick={() => setStep(1)} style={{ flex: 1 }}>{t('customer', 'booking_back') || 'Back'}</Btn>
              <Btn onClick={addToCart} style={{ flex: 2 }}>{t('customer', 'book_now') || 'Book now'}</Btn>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

/* ---------- Checkout — real cart (localStorage 'svlm_cart', shared key with the vanilla app so
   nothing is lost switching between builds), same subtotal/discount/VAT/shipping math as
   renderCart(), same POST /orders on submit, same bank_transfer-only payment method (promptpay/
   card are listed but disabled in production too — no gateway wired for them yet). ---------- */
function CheckoutScreen({ app }) {
  const [promoCode, setPromoCode] = React.useState('');
  const [promo, setPromo] = React.useState(null);
  const [promoMsg, setPromoMsg] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [payMethod, setPayMethod] = React.useState('bank_transfer'); // 'bank_transfer' | 'qr' | 'card'
  // What mPay adds on top for each method. Fetched rather than hard-coded because these are the
  // merchant's commercial terms and Ops can change them; an empty list simply shows no fee line,
  // which is right when nothing is configured.
  const [payFees, setPayFees] = React.useState([]);
  // Whether card and PromptPay can be offered at all.
  //
  // While production points at mPay's UAT endpoint for certification, both would hurt a real
  // customer: the QR that comes back is not valid EMV so no banking app can scan it, and the card
  // form is a test page they could type a real card number into. Bank transfer is unaffected.
  //
  // Starts null rather than true, so the two buttons stay hidden until the server has actually
  // said the gateway is live — a slow answer must not flash a payment method we then take away.
  const [gatewayLive, setGatewayLive] = React.useState(null);
  // Offered only for card, and only as a request — mPay's own form carries the actual tick box, so
  // the customer confirms it there with the card in front of them.
  const [saveCard, setSaveCard] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    api('/config/public')
      .then((cfg) => { if (!cancelled) setGatewayLive(!!cfg.mpayGatewayLive); })
      // A failed check hides the two methods rather than showing them. Not knowing whether the
      // gateway is real is the same risk as knowing it is not.
      .catch(() => { if (!cancelled) setGatewayLive(false); });
    return () => { cancelled = true; };
  }, []);
  React.useEffect(() => {
    let cancelled = false;
    api('/payments/fees')
      .then((rows) => { if (!cancelled) setPayFees(Array.isArray(rows) ? rows : []); })
      .catch(() => { if (!cancelled) setPayFees([]); });
    return () => { cancelled = true; };
  }, []);
  // Nothing else can be selected while the gateway is not live, so a customer who had already
  // picked one is moved back rather than left on a method whose button has disappeared.
  React.useEffect(() => {
    if (gatewayLive === false && payMethod !== 'bank_transfer') setPayMethod('bank_transfer');
  }, [gatewayLive, payMethod]);

  const cart = app.cart;

  // Products ship from our warehouse to wherever the customer says, so a product cart needs a
  // delivery address of its own — a booking already has one, a shopping basket doesn't.
  const productItems = cart.filter(item => item.product_id);
  const [shipAddress, setShipAddress] = React.useState('');
  const [shipCoords, setShipCoords] = React.useState(null);
  const [productQuote, setProductQuote] = React.useState(null);

  // Quoted server-side from the real product weights, so the fee shown here is the one
  // routes/orders.js will charge. Null (no warehouse configured, or a free-typed address) simply
  // shows nothing and the order still goes through, with Ops pricing the delivery.
  React.useEffect(() => {
    if (productItems.length === 0 || !shipCoords) { setProductQuote(null); return; }
    let cancelled = false;
    api('/delivery/product-quote', {
      method: 'POST',
      body: JSON.stringify({
        dest_lat: shipCoords.lat, dest_lng: shipCoords.lng,
        items: productItems.map(i => ({ product_id: i.product_id, quantity: i.quantity || 1 })),
      }),
    })
      .then(r => { if (!cancelled) setProductQuote(r.quote); })
      .catch(() => { if (!cancelled) setProductQuote(null); });
    return () => { cancelled = true; };
  }, [shipCoords, cart]);

  const subtotal = cart.reduce((sum, item) => sum + Number(item.unit_price_thb) * Number(item.quantity || 1) + Number(item._addons_total || 0), 0);
  const discount = promo ? Math.min(promo.kind === 'percent' ? Math.round(subtotal * Number(promo.value)) / 100 : Number(promo.value), subtotal) : 0;
  const discounted = subtotal - discount;
  // Delivery is service revenue carrying our own margin, not passthrough postage, so it sits
  // inside the VAT base — this must mirror routes/orders.js exactly or the customer is shown a
  // different number from the one they're charged.
  const deliveryFee = cart.reduce((sum, item) => sum + Number(item._delivery_fee || 0), 0)
    + (productQuote ? Number(productQuote.feeThb) : 0);
  const { vat, total } = orderTotals(discounted, deliveryFee);

  // What mPay will add on top for the method chosen, mirroring the server's own calculation
  // (routes/payments.js paymentFeeFor): amount x percent/100 + flat. Both shapes are needed —
  // card and PromptPay are percentages, internet banking is a flat ฿15 — and reading a flat fee as
  // a percentage would be out by hundreds of baht on a large order.
  //
  // Bank transfer has no gateway and so no fee, which is why it is absent from the table rather
  // than present with zeroes.
  const feeRule = payFees.find((f) => f.method === payMethod);
  const payFee = feeRule
    ? Math.round(((total * Number(feeRule.percent_fee)) / 100 + Number(feeRule.flat_fee_thb)) * 100) / 100
    : 0;
  const payFeeLabel = !feeRule
    ? ''
    : (Number(feeRule.percent_fee) > 0 ? Number(feeRule.percent_fee) + '%' : baht(Number(feeRule.flat_fee_thb)));

  const applyPromo = async () => {
    setPromoMsg('');
    if (!promoCode.trim()) { setPromo(null); return; }
    const promos = await api('/promotions/active');
    const match = promos.find(p => p.id.toLowerCase() === promoCode.trim().toLowerCase());
    if (!match) { setPromo(null); setPromoMsg(t('customer', 'promo_invalid') || 'Invalid promo code'); return; }
    setPromo({ id: match.id, kind: match.kind, value: match.value });
    setPromoMsg(`${t('customer', 'promo_applied') || 'Applied'} ${match.kind === 'percent' ? match.value + '%' : baht(match.value)}`);
  };

  const placeOrder = async () => {
    if (cart.length === 0) return;
    setBusy(true); setErr('');
    const billedItem = cart.find(item => item.bill_type);
    const body = {
      pay_method: payMethod,
      promo_id: promo?.id || null,
      // Kept even when nothing could be quoted from it — Ops still has to deliver there.
      ...(productItems.length > 0 ? {
        delivery_address: shipAddress || null,
        delivery_lat: shipCoords?.lat ?? null,
        delivery_lng: shipCoords?.lng ?? null,
      } : {}),
      items: cart.map(({ _service_name, _addon_summary, _addons_total, bill_type, tax_id, bill_name, bill_address, bill_branch, bill_email, ...item }) => item),
      ...(billedItem ? { bill_type: billedItem.bill_type, tax_id: billedItem.tax_id, bill_name: billedItem.bill_name, bill_address: billedItem.bill_address, bill_branch: billedItem.bill_branch, bill_email: billedItem.bill_email } : {}),
    };
    try {
      const { order } = await api('/orders', { method: 'POST', body: JSON.stringify(body) });

      // Bank transfer needs nothing further — Ops confirms manually, same as before.
      if (payMethod === 'bank_transfer') {
        app.clearCart();
        app.go('confirm');
        return;
      }

      // QR/card both create the order first (pay_status stays 'pending'), then ask mPay to
      // actually start the payment against that order — see routes/payments.js.
      const payment = await api('/payments/mpay/create', {
        method: 'POST',
        body: JSON.stringify({ order_id: order.id, method: payMethod, save_card: payMethod === 'card' && saveCard }),
      });
      app.clearCart();
      if (payMethod === 'qr') {
        app.go('payment-qr', { orderId: order.id, qrImg: payment.qr_img, qrExpireAt: payment.qr_expire_at });
      } else {
        // Card is a full redirect to mPay's own hosted form — this leaves the SPA entirely;
        // mPay redirects back to public/payment-result.html once 3DS completes either way.
        window.location.href = payment.form_url;
      }
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

  if (cart.length === 0) {
    return (
      <div>
        <AppHeader onBack={app.back} title={t('customer', 'checkout_title') || 'Checkout'} />
        <div style={{ textAlign: 'center', padding: '60px 20px', color: Pbk.faint }}>
          <Icon name="cart" size={44} color={Pbk.line} />
          <div style={{ marginTop: 12, fontSize: 14, fontWeight: 600 }}>{t('customer', 'cart_empty') || 'Your cart is empty'}</div>
        </div>
      </div>
    );
  }

  return (
    <div>
      <AppHeader onBack={app.back} title={t('customer', 'checkout_title') || 'Checkout'} />
      <div style={{ padding: '0 20px 130px' }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 18 }}>
          {cart.map((item, i) => {
            const isProduct = !!item.product_id;
            const thumb = isProduct ? app.products.find(p => p.id === item.product_id)?.image_url : app.services.find(s => s.id === item.service_id)?.image_url;
            return (
              <Card key={i} pad={12}>
                <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
                  <div style={{ flexShrink: 0 }}><Photo src={thumb} h={54} r={12} style={{ width: 54 }} /></div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 700, fontSize: 13.5, color: Pbk.ink }}>{item._service_name || item._product_name}{item.tier_label ? ` - ${item.tier_label}` : ''}</div>
                    {item.scheduled_date && <div style={{ fontSize: 11.5, color: Pbk.faint, marginTop: 2 }}>{item.scheduled_date} {item.scheduled_time}</div>}
                    {item._addon_summary && <div style={{ fontSize: 11.5, color: Pbk.faint }}>{t('customer', 'add_extras_label') || 'Extras'}: {item._addon_summary}</div>}
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 8 }}>
                      <button onClick={() => app.adjustCartQty(i, -1)} style={{ width: 28, height: 28, borderRadius: 9, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer' }}>−</button>
                      <span style={{ fontSize: 13, fontWeight: 700 }}>{item.quantity}</span>
                      <button onClick={() => app.adjustCartQty(i, 1)} style={{ width: 28, height: 28, borderRadius: 9, border: `1.5px solid ${Pbk.line}`, background: '#fff', cursor: 'pointer' }}>+</button>
                    </div>
                  </div>
                  <div style={{ textAlign: 'right', flexShrink: 0 }}>
                    <div style={{ fontWeight: 800, fontSize: 14, color: Pbk.ink }}>{baht(Number(item.unit_price_thb) * Number(item.quantity) + Number(item._addons_total || 0))}</div>
                    <button onClick={() => app.removeFromCart(i)} style={{ marginTop: 8, background: 'none', border: 'none', color: '#d23a3a', fontSize: 12, fontWeight: 700, cursor: 'pointer' }}>{t('customer', 'remove') || 'Remove'}</button>
                  </div>
                </div>
              </Card>
            );
          })}
        </div>

        {/* Only a cart with products needs this — a booking already carries its own address. */}
        {productItems.length > 0 && (
          <Field label={t('customer', 'ship_to_label') || 'Delivery address'}>
            <AddressField value={shipAddress} onChange={(v, coords) => { setShipAddress(v); setShipCoords(coords || null); }} />
            {productQuote ? (
              <div style={{ fontSize: 12, color: Pbk.muted, marginTop: 6 }}>
                {(t('customer', 'delivery_fee_label') || 'Delivery fee')}: <b style={{ color: Pbk.ink }}>{fmtThb(productQuote.feeThb)}</b>
                <span style={{ color: Pbk.faint }}> · {Number(productQuote.distanceKm).toFixed(1)} km</span>
              </div>
            ) : (
              <div style={{ fontSize: 11.5, color: Pbk.faint, marginTop: 6 }}>
                {t('customer', 'delivery_fee_pending') || 'Pick your address from the suggestions to see the delivery fee — otherwise our team will confirm it with you.'}
              </div>
            )}
          </Field>
        )}

        <Field label={t('customer', 'promo_code_label') || 'Promo code'}>
          <div style={{ display: 'flex', gap: 8 }}>
            <input value={promoCode} onChange={e => setPromoCode(e.target.value)} placeholder={t('customer', 'promo_code_placeholder') || 'Enter code'} style={{ ...inputStyle, flex: 1 }} />
            <button onClick={applyPromo} style={{ padding: '0 18px', borderRadius: 14, border: `1.5px solid ${Pbk.primary}`, background: '#fff', color: Pbk.primary, fontWeight: 700, cursor: 'pointer' }}>{t('customer', 'apply') || 'Apply'}</button>
          </div>
          {promoMsg && <p style={{ fontSize: 12, color: promo ? Pbk.mint : '#d23a3a', marginTop: 6 }}>{promoMsg}</p>}
        </Field>

        <Field label={t('customer', 'payment_methods') || 'Payment method'}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label onClick={() => setPayMethod('bank_transfer')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 14, borderRadius: 14, cursor: 'pointer', border: `1.5px solid ${payMethod === 'bank_transfer' ? Pbk.primary : Pbk.line}`, background: payMethod === 'bank_transfer' ? Pbk.tintBlue : '#fff' }}>
              <input type="radio" checked={payMethod === 'bank_transfer'} readOnly /> <span style={{ fontWeight: 700, fontSize: 13.5 }}>{t('customer', 'pay_bank_transfer') || 'Bank transfer'}</span>
            </label>
            {/* Real transfer destination — pay_method='bank_transfer' is manually verified by Ops
                (see routes/orders.js/bookings.js pay-status endpoints), so the customer needs to
                actually see where to send money; nothing showed this before. */}
            {payMethod === 'bank_transfer' && (
              <div style={{ padding: '10px 14px', borderRadius: 12, background: Pbk.lineSoft, fontSize: 12.5, color: Pbk.muted, lineHeight: 1.7 }}>
                <div>{t('customer', 'bank_name_label') || 'Bank'}: <b style={{ color: Pbk.ink }}>Kasikorn Bank (กสิกรไทย)</b></div>
                <div>{t('customer', 'bank_account_name_label') || 'Account name'}: <b style={{ color: Pbk.ink }}>Siamvilai Development Co., Ltd.</b></div>
                <div>{t('customer', 'bank_account_number_label') || 'Account number'}: <b style={{ color: Pbk.ink }}>0571495759</b></div>
              </div>
            )}
            {gatewayLive === false && (
              <div style={{ padding: '10px 14px', borderRadius: 12, background: Pbk.lineSoft, fontSize: 12.5, color: Pbk.muted, lineHeight: 1.7 }}>
                {t('customer', 'pay_online_unavailable')
                  || 'Card and PromptPay are temporarily unavailable. Please pay by bank transfer — our team will confirm your booking as soon as the transfer arrives.'}
              </div>
            )}
            {gatewayLive === true && <label onClick={() => setPayMethod('qr')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 14, borderRadius: 14, cursor: 'pointer', border: `1.5px solid ${payMethod === 'qr' ? Pbk.primary : Pbk.line}`, background: payMethod === 'qr' ? Pbk.tintBlue : '#fff' }}>
              <input type="radio" checked={payMethod === 'qr'} readOnly /> <span style={{ fontWeight: 700, fontSize: 13.5 }}>{t('customer', 'pay_promptpay') || 'PromptPay'}</span>
            </label>}
            {gatewayLive === true && <label onClick={() => setPayMethod('card')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 14, borderRadius: 14, cursor: 'pointer', border: `1.5px solid ${payMethod === 'card' ? Pbk.primary : Pbk.line}`, background: payMethod === 'card' ? Pbk.tintBlue : '#fff' }}>
              <input type="radio" checked={payMethod === 'card'} readOnly /> <span style={{ fontWeight: 700, fontSize: 13.5 }}>{t('customer', 'pay_card') || 'Credit/debit card'}</span>
            </label>}
            {gatewayLive === true && payMethod === 'card' && (
              <label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, padding: '2px 4px 0', cursor: 'pointer' }}>
                <input type="checkbox" checked={saveCard} onChange={e => setSaveCard(e.target.checked)} style={{ marginTop: 3 }} />
                <span style={{ fontSize: 12.5, color: Pbk.muted, lineHeight: 1.6 }}>
                  {t('customer', 'save_card_label')
                    || 'Save this card so I don’t have to type it next time'}
                  <br />
                  <span style={{ color: Pbk.faint, fontSize: 11.5 }}>
                    {t('customer', 'save_card_note')
                      || 'Kept by our payment provider, not by us. You confirm this on their secure page.'}
                  </span>
                </span>
              </label>
            )}
          </div>
        </Field>

        <div style={{ background: '#fff', borderRadius: 16, padding: 14, marginTop: 6, boxShadow: '0 1px 2px rgba(11,37,69,0.04), 0 6px 18px rgba(11,37,69,0.05)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'subtotal_label') || 'Subtotal'}</span><span>{baht(subtotal)}</span></div>
          {discount > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.mint, marginBottom: 6 }}><span>{t('customer', 'discount_label') || 'Discount'}</span><span>-{baht(discount)}</span></div>}
          {deliveryFee > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'delivery_fee_label') || 'Delivery fee'}</span><span>{baht(deliveryFee)}</span></div>}
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: Pbk.muted, marginBottom: 6 }}><span>{t('customer', 'vat_label') || 'VAT'}</span><span>{baht(vat)}</span></div>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 16, fontWeight: 800, color: Pbk.ink, paddingTop: 8, borderTop: `1px solid ${Pbk.lineSoft}` }}><span>{t('customer', 'total_label') || 'Total'}</span><span style={{ color: Pbk.primary }}>{baht(total)}</span></div>
          {/* mPay adds its own fee on top of our price and charges the customer the sum. We still
              receive the full total above, so this is not a price rise — but showing only our
              total would mean the customer is debited more than the app ever told them, which is
              the complaint this exists to prevent. Bank transfer has no gateway and no fee. */}
          {payFee > 0 && (
            <>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12.5, color: Pbk.muted, marginTop: 8 }}>
                <span>{t('customer', 'payment_fee_label') || 'Payment fee'}{payFeeLabel ? ' (' + payFeeLabel + ')' : ''}</span>
                <span>{baht(payFee)}</span>
              </div>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 14.5, fontWeight: 800, color: Pbk.ink, paddingTop: 8, marginTop: 4, borderTop: `1px solid ${Pbk.lineSoft}` }}>
                <span>{t('customer', 'amount_charged_label') || 'Amount charged'}</span>
                <span>{baht(total + payFee)}</span>
              </div>
            </>
          )}
        </div>
        {err && <p style={{ color: '#d23a3a', fontSize: 13, fontWeight: 600, marginTop: 10 }}>{err}</p>}
      </div>
      <BottomBar>
        <Btn disabled={busy} onClick={placeOrder}>{busy ? '…' : (t('customer', 'confirm_booking') || 'Confirm booking')}</Btn>
      </BottomBar>
    </div>
  );
}

function ConfirmScreen({ app }) {
  return (
    <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
      <div style={{ width: 76, height: 76, borderRadius: 99, background: Pbk.mintSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
        <Icon name="check" size={36} color={Pbk.mint} sw={2.6} />
      </div>
      <h2 style={{ fontSize: 20, fontWeight: 800, color: Pbk.ink, margin: '0 0 8px' }}>{t('customer', 'order_placed_success') || 'Order placed successfully!'}</h2>
      <p style={{ fontSize: 13.5, color: Pbk.muted, margin: '0 0 28px', maxWidth: 280 }}>{app.lang === 'th' ? 'ทีมงานของเราจะติดต่อยืนยันการนัดหมายเร็ว ๆ นี้' : "We'll confirm your booking shortly."}</p>
      <div style={{ width: '100%', maxWidth: 320 }}>
        <Btn onClick={() => app.go('tab-home')}>{app.lang === 'th' ? 'กลับหน้าแรก' : 'Back to Home'}</Btn>
      </div>
    </div>
  );
}

/* ---------- PromptPay QR screen — shows the code, polls GET /payments/mpay/status/:orderId every
   3s until paid/failed/expired. The order was already created before this screen is reached (see
   CheckoutScreen.placeOrder), so there's nothing left to submit here, only to wait on. ---------- */
function PaymentQrScreen({ app, params }) {
  const [status, setStatus] = React.useState('pending');
  const [secondsLeft, setSecondsLeft] = React.useState(() => Math.max(0, Math.round((new Date(params.qrExpireAt).getTime() - Date.now()) / 1000)));

  React.useEffect(() => {
    if (status !== 'pending') return;
    const poll = setInterval(async () => {
      try {
        const res = await api(`/payments/mpay/status/${params.orderId}`);
        if (res.pay_status === 'paid') { setStatus('paid'); clearInterval(poll); }
        else if (res.pay_status === 'failed') { setStatus('failed'); clearInterval(poll); }
      } catch (e) { /* transient network hiccup — next tick tries again */ }
    }, 3000);
    return () => clearInterval(poll);
  }, [status, params.orderId]);

  React.useEffect(() => {
    if (status !== 'pending') return;
    const tick = setInterval(() => setSecondsLeft((s) => Math.max(0, s - 1)), 1000);
    return () => clearInterval(tick);
  }, [status]);

  React.useEffect(() => {
    if (status === 'pending' && secondsLeft === 0) setStatus('expired');
  }, [secondsLeft, status]);

  if (status === 'paid') {
    return (
      <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
        <div style={{ width: 76, height: 76, borderRadius: 99, background: Pbk.mintSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
          <Icon name="check" size={36} color={Pbk.mint} sw={2.6} />
        </div>
        <h2 style={{ fontSize: 20, fontWeight: 800, color: Pbk.ink, margin: '0 0 8px' }}>{app.lang === 'th' ? 'ชำระเงินสำเร็จ' : 'Payment successful'}</h2>
        <div style={{ width: '100%', maxWidth: 320, marginTop: 20 }}>
          <Btn onClick={() => app.go('tab-home')}>{app.lang === 'th' ? 'กลับหน้าแรก' : 'Back to Home'}</Btn>
        </div>
      </div>
    );
  }

  if (status === 'failed' || status === 'expired') {
    return (
      <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, textAlign: 'center' }}>
        <div style={{ width: 76, height: 76, borderRadius: 99, background: '#fde8e8', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
          <Icon name="x" size={32} color="#d23a3a" sw={2.4} />
        </div>
        <h2 style={{ fontSize: 20, fontWeight: 800, color: Pbk.ink, margin: '0 0 8px' }}>
          {status === 'expired' ? (app.lang === 'th' ? 'QR หมดอายุแล้ว' : 'QR code expired') : (app.lang === 'th' ? 'การชำระเงินไม่สำเร็จ' : 'Payment failed')}
        </h2>
        <p style={{ fontSize: 13.5, color: Pbk.muted, margin: '0 0 28px', maxWidth: 280 }}>{app.lang === 'th' ? 'คุณสามารถลองชำระเงินอีกครั้งได้จากหน้าคำสั่งซื้อของฉัน' : 'You can retry payment from My Bookings.'}</p>
        <div style={{ width: '100%', maxWidth: 320 }}>
          <Btn onClick={() => app.go('tab-home')}>{app.lang === 'th' ? 'กลับหน้าแรก' : 'Back to Home'}</Btn>
        </div>
      </div>
    );
  }

  const mm = String(Math.floor(secondsLeft / 60)).padStart(2, '0');
  const ss = String(secondsLeft % 60).padStart(2, '0');

  return (
    <div>
      <AppHeader onBack={app.back} title={t('customer', 'pay_promptpay') || 'PromptPay'} />
      <div style={{ padding: '20px 20px 40px', textAlign: 'center' }}>
        <p style={{ fontSize: 13.5, color: Pbk.muted, marginBottom: 16 }}>{app.lang === 'th' ? 'สแกนเพื่อชำระเงินด้วยแอปธนาคารของคุณ' : 'Scan with your banking app to pay'}</p>
        <div style={{ display: 'inline-block', padding: 16, borderRadius: 20, border: `1.5px solid ${Pbk.line}`, background: '#fff' }}>
          <img src={`data:image/png;base64,${params.qrImg}`} alt="PromptPay QR" style={{ width: 220, height: 220, display: 'block' }} />
        </div>
        <div style={{ marginTop: 16, fontSize: 13, color: Pbk.muted }}>
          {app.lang === 'th' ? 'หมดอายุใน' : 'Expires in'} <b style={{ color: Pbk.ink, fontVariantNumeric: 'tabular-nums' }}>{mm}:{ss}</b>
        </div>
        <div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, color: Pbk.faint, fontSize: 12.5 }}>
          <span className="spin-dot" style={{ width: 8, height: 8, borderRadius: 99, background: Pbk.primary, display: 'inline-block' }} />
          {app.lang === 'th' ? 'กำลังรอการชำระเงิน...' : 'Waiting for payment...'}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { BookingScreen, CheckoutScreen, ConfirmScreen, PaymentQrScreen });
