/* Shared booking widget — used by ALL persona landings.
   Requires: React (global), CENTRUMS (data.jsx), icons (icons.jsx), window.bookingClient (booking-client.js).
   Load order in HTML: booking-client.js, data.jsx, icons.jsx, booking.jsx, then the page's ui file.

   i18n: <Booking lang="hu" | "en" />  — default "hu". The Hungarian persona pages omit `lang`
   (so they keep Hungarian); the English (Dimitri) page passes lang="en". */

const { useState: useStateB, useReducer, useEffect: useEffectB, useRef: useRefB } = React;

/* ---------- Strings ---------- */
const STRINGS = {
  hu: {
    dow: ["V", "H", "K", "Sze", "Cs", "P", "Szo"],
    mon: ["jan", "feb", "már", "ápr", "máj", "jún", "júl", "aug", "szept", "okt", "nov", "dec"],
    today: "Ma", tomorrow: "Holnap",
    dayTitle: (d) => `${d.dow} ${d.day}. ${d.month}`,
    eyebrow: "A foglaló", titleA: "Foglalj időpontot ", titleEm: "most.",
    whichCentre: "Melyik centrum?",
    step1: "Mikor érsz rá?", step3: "Add meg magad.", pickDay: "Válassz napot",
    loadingTimes: "Szabad időpontok betöltése…", loadingSlots: "Slot-ok betöltése…",
    calendarError: "Nem sikerült betölteni a naptárat. Próbáld újra.",
    slotsError: "Nem sikerült betölteni a slot-okat.",
    noFreeTimes: "Erre a napra már nincs szabad időpont — válassz másikat.",
    slotNote: "Egy 15 perces ablakot foglalsz. Maradj 5 perccel előtte.",
    legend: { many: "sok hely", some: "van", few: "pár", full: "tele" },
    fieldName: "Teljes neved", fieldEmail: "E-mail", fieldPhone: "Telefonszám", checking: "ellenőrzés",
    consentPre: "Elfogadom, hogy felvegyétek velem a kapcsolatot a foglalás miatt. Az ",
    consentTerms: "ÁSZF-et", consentMid: " és az ", consentPrivacy: "adatkezelést", consentPost: " megértettem.",
    pNeedConsent: "Még egy pipa — mindjárt kész.",
    p0: "3 mező van — kb. 30 másodperc.", p1: "Még 2 mező — szuper haladsz.",
    p2: "Még 1 lépés — már majdnem kész.", pValid: "Mehet — találkozunk a centrumban.",
    pConsentLeft: "Még egy lépés — pipáld be a hozzájárulást.",
    bookingInProgress: "Foglalás folyamatban…", booking: "Foglalás…",
    submitCta: "Lefoglalom — 50.000 Ft vár", submitFill: "Tölts ki minden mezőt",
    noSpam: "Nem küldünk levélszemetet. Az e-mail csak a foglalásról szól.",
    errSlotFull: "Sajnos ez a slot épp megtelt — válassz másikat.",
    errDuplicate: "Már van egy friss foglalásod ezzel az e-mail címmel. Nézd meg a postaládádat vagy hívj minket.",
    errExclusion: "Most nem tudunk foglalni neked. Kérlek nézd meg a fenti üzenetet.",
    errPhone: "A telefonszám formátuma nem megfelelő.", errEmail: "Az e-mail cím formátuma nem megfelelő.",
    errGeneric: "Hiba történt. Próbáld újra.",
    errStale: "Most nem tudtuk ellenőrizni a slotot. Próbáld újra egy perc múlva.",
    errNetwork: "Hálózati hiba a végső ellenőrzéskor. Próbáld újra.",
    exclSuggestOutside: (d) => `A foglalót ${d} után tudod nyitni — most még a kizárás van érvényben.`,
    exclCurrentlyTitle: "Ideiglenes kizárás",
    exclCurrentlyUntil: (d) => `Az aktuális kizárásod ${d}-ig tart. Addig nem tudunk időpontot adni neked.`,
    exclCurrentlyNoDate: "Egy aktuális kizárás miatt jelenleg nem tudsz foglalni.",
    exclPermTitle: "Nem tudunk most foglalni",
    exclPermBody: "A nyilvántartás alapján jelenleg nem tudsz plazmát adni nálunk. Részletekért keresd a recepciónkat — el tudják mondani mit lehet tenni.",
    exclCancelTitle: "Visszavonásig kizárva",
    exclCancelBody: "Egy korábbi probléma miatt jelenleg szünetel a foglalás. A recepciónk fel tudja oldani — hívj minket.",
    exclDefaultTitle: "Most nem tudunk foglalni",
    exclDefaultBody: "Valami közbejött. Hívd a centrumot vagy próbáld újra később.",
    exclSuggestLabel: (d) => `Foglalok ${d}-tól`,
    exclCallLabel: (s) => `Hívom: ${s}`,
    exclAnotherCentre: "Másik centrum",
    noBloodTitle: "Helyszíni véradás kell az első alkalomnál",
    noBloodBody: "A nyilvántartás szerint nincs érvényes véradási igazolásod. Nálunk a helyszínen elkészítjük — az első alkalom kicsit hosszabb lesz, de utána már gyorsan megy minden.",
    successTitleA: "Megvan! ", successTitleEm: "Foglalva.",
    successSub: "Találkozunk a centrumban. Küldtünk egy e-mailt a részletekkel.",
    rowCentre: "Centrum", rowTime: "Időpont", rowBring: "Hozz magaddal", rowPay: "Pénz",
    bringVal: "Személyi igazolvány + lakcímkártya", bringSub: "TB-kártyára nincs szükség",
    payVal: "Készpénzben, helyben", paySub: "Új donornak 10.000 Ft, visszatérőnek a saját szintje szerint",
    timeSub: "15 perces ablak — érkezz 5 perccel előtte",
    addCal: "Naptárba", share: "Megosztom", bookAnother: "Másik időpontot foglalok",
  },
  en: {
    dow: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
    mon: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
    today: "Today", tomorrow: "Tomorrow",
    dayTitle: (d) => `${d.dow} ${d.day} ${d.month}`,
    eyebrow: "Booking", titleA: "Book your slot ", titleEm: "now.",
    whichCentre: "Which centre?",
    step1: "When suits you?", step3: "Your details", pickDay: "Pick a day",
    loadingTimes: "Loading available times…", loadingSlots: "Loading times…",
    calendarError: "Couldn't load the calendar. Please try again.",
    slotsError: "Couldn't load times.",
    noFreeTimes: "No free times left for this day — pick another.",
    slotNote: "You're booking a 15-minute window. Arrive 5 minutes early.",
    legend: { many: "lots", some: "some", few: "few", full: "full" },
    fieldName: "Full name", fieldEmail: "Email", fieldPhone: "Phone number", checking: "checking",
    consentPre: "I agree to be contacted about my booking. I've read the ",
    consentTerms: "Terms", consentMid: " and the ", consentPrivacy: "Privacy Policy", consentPost: ".",
    pNeedConsent: "One more tick — almost done.",
    p0: "3 fields — about 30 seconds.", p1: "2 more fields — going great.",
    p2: "1 more step — almost there.", pValid: "Ready — see you at the centre.",
    pConsentLeft: "One more step — tick the consent box.",
    bookingInProgress: "Booking in progress…", booking: "Booking…",
    submitCta: "Book now — 50,000 HUF waiting", submitFill: "Fill in every field",
    noSpam: "No spam. The email is only about your booking.",
    errSlotFull: "Sorry, that time just filled up — pick another.",
    errDuplicate: "You already have a recent booking with this email. Check your inbox or call us.",
    errExclusion: "We can't book you right now. Please see the message above.",
    errPhone: "The phone number format is invalid.", errEmail: "The email format is invalid.",
    errGeneric: "Something went wrong. Please try again.",
    errStale: "We couldn't verify the time just now. Try again in a minute.",
    errNetwork: "Network error during the final check. Please try again.",
    exclSuggestOutside: (d) => `You can book after ${d} — the exclusion is still active until then.`,
    exclCurrentlyTitle: "Temporary exclusion",
    exclCurrentlyUntil: (d) => `Your current exclusion lasts until ${d}. We can't offer a time before then.`,
    exclCurrentlyNoDate: "Due to a current exclusion you can't book right now.",
    exclPermTitle: "We can't book you now",
    exclPermBody: "According to our records you can't donate with us right now. Please contact our reception — they can tell you what to do.",
    exclCancelTitle: "Excluded until further notice",
    exclCancelBody: "Booking is paused due to an earlier issue. Our reception can lift it — please call us.",
    exclDefaultTitle: "We can't book you now",
    exclDefaultBody: "Something came up. Call the centre or try again later.",
    exclSuggestLabel: (d) => `Book from ${d}`,
    exclCallLabel: (s) => `Call ${s}`,
    exclAnotherCentre: "Another centre",
    noBloodTitle: "On-site blood test at your first visit",
    noBloodBody: "Our records show no valid blood-test certificate. We'll do it on site — your first visit takes a little longer, then everything is quick.",
    successTitleA: "Done! ", successTitleEm: "Booked.",
    successSub: "See you at the centre. We've emailed you the details.",
    rowCentre: "Centre", rowTime: "Time", rowBring: "Bring with you", rowPay: "Payment",
    bringVal: "ID card + address card", bringSub: "No health-insurance card needed",
    payVal: "Cash, on site", paySub: "10,000 HUF for new donors; returning donors at their own level",
    timeSub: "15-minute window — arrive 5 min early",
    addCal: "Add to calendar", share: "Share", bookAnother: "Book another time",
  },
};

const initial = {
  step: 1,
  selectedDate: null,
  selectedSlot: null,
  form: { name: "", phone: "", email: "", consent: false },
  isSubmitted: false
};

function reducer(state, action) {
  switch (action.type) {
    case "PICK_DATE":return { ...state, selectedDate: action.iso, selectedSlot: null };
    case "PICK_SLOT":return { ...state, selectedSlot: action.id };
    case "SET_FIELD":return { ...state, form: { ...state.form, [action.key]: action.value } };
    case "SUBMIT":return { ...state, isSubmitted: true };
    case "RESET":return { ...initial };
    default:return state;
  }
}

function CapDot({ code }) {
  const map = {
    0: { c: "#D6D6CE" }, 1: { c: "#E0A23A" }, 2: { c: "#9CB66D" }, 3: { c: "#00C896" }
  };
  const v = map[code] || map[2];
  return <span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: v.c }} />;
}

function DayPill({ d, active, onClick }) {
  const closed = d.capCode === 0;
  return (
    <button
      disabled={closed}
      onClick={onClick}
      className={
      "shrink-0 w-[64px] rounded-2xl border px-2 py-3 flex flex-col items-center gap-1 transition active:scale-[0.97] " + (
      active ?
      "bg-ink text-white border-ink" :
      closed ?
      "bg-surface/60 text-ink/30 border-border" :
      "bg-white text-ink border-border hover:border-ink/40")
      }>
      <span className={"text-[10.5px] mono uppercase tracking-wider " + (active ? "text-white/70" : "text-ink/50")}>
        {d.label || d.dow}
      </span>
      <span className="serif text-[24px] leading-none">{d.day}</span>
      <span className={"text-[10.5px] mono " + (active ? "text-white/70" : "text-ink/50")}>{d.month}</span>
      <CapDot code={d.capCode} />
    </button>);
}

function SlotButton({ s, active, onClick, heat = 0 }) {
  if (s.taken) {
    return (
      <div className="h-11 rounded-xl border border-border bg-surface/60 text-ink/30 text-[13.5px] flex items-center justify-center mono line-through">
        {s.id}
      </div>);
  }
  // base / filling / almost-gone — color only, always clickable
  const heatCls = active
    ? "bg-accent text-white border-accent shadow-[0_6px_18px_-6px_rgba(230,57,70,0.6)]"
    : heat === 2
      ? "bg-accent/[0.06] text-ink border-accent/55 hover:border-accent"
      : heat === 1
        ? "bg-[#E0A23A]/[0.07] text-ink border-[#E0A23A]/55 hover:border-[#E0A23A]"
        : "bg-white text-ink border-border hover:border-ink/40";
  return (
    <button
      onClick={onClick}
      title={!active && heat === 2 ? "Az utolsó pár hely" : !active && heat === 1 ? "Töltődik" : undefined}
      className={"relative h-11 rounded-xl border text-[13.5px] mono flex items-center justify-center transition active:scale-[0.97] " + heatCls}>
      {s.id}
      {!active && heat > 0 &&
        <span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full" style={{ background: heat === 2 ? "#E63946" : "#E0A23A" }} />}
    </button>);
}

function buildDayLabel(iso, idx, T) {
  const date = new Date(iso + "T12:00:00");
  return {
    iso,
    day: date.getDate(),
    month: T.mon[date.getMonth()],
    dow: T.dow[date.getDay()],
    isWeekend: date.getDay() === 0 || date.getDay() === 6,
    label: idx === 0 ? T.today : idx === 1 ? T.tomorrow : null,
  };
}

function capCodeFromCount(n) {
  if (!n || n === 0) return 0;
  if (n < 5) return 1;
  if (n < 20) return 2;
  return 3;
}

/* ---------- Manufactured scarcity (deterministic "heat") ----------
   The centres are over-provisioned, so real availability never looks scarce.
   We derive a STABLE heat per (date+time) so it never flickers on refresh and
   looks like a shared inventory. Realistic drivers: lunch/evening peaks run hotter,
   near-term days run hotter, far-out days stay open. `intensity` scales it (0..1).
   Returns 0 = open, 1 = filling, 2 = almost-gone. Never disables the slot. */
function hashStr(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
  return ((h >>> 0) % 10000) / 10000; // 0..1 stable
}

function slotHeat(dateISO, time, dayIndex, intensity) {
  if (!intensity || intensity <= 0) return 0;
  const hour = parseInt(time, 10);
  let w = 0.12;
  if (hour >= 11 && hour < 14) w += 0.30;   // lunch peak
  if (hour >= 16 && hour < 19) w += 0.36;   // evening peak
  if (hour >= 7 && hour < 9) w += 0.08;      // early-bird
  if (dayIndex === 0) w += 0.26;             // today
  else if (dayIndex === 1) w += 0.16;        // tomorrow
  else if (dayIndex <= 3) w += 0.08;
  else if (dayIndex >= 10) w -= 0.12;        // far out stays open
  const jitter = (hashStr(dateISO + time) - 0.5) * 0.40;
  const score = (w + jitter) * intensity;
  if (score > 0.58) return 2;
  if (score > 0.38) return 1;
  return 0;
}

/* Day-pill dot, coherent with the slot heat: near-term days look fuller. capCode:
   0 = closed (kept as-is), 1 = few, 2 = some, 3 = lots. */
function manufacturedDayCap(realCap, dateISO, dayIndex, intensity) {
  if (realCap === 0) return 0;                 // genuinely closed → keep
  if (!intensity || intensity <= 0) return realCap;
  const j = hashStr(dateISO + "|day");
  let level;
  if (dayIndex <= 1) level = j < 0.55 ? 1 : 2;
  else if (dayIndex <= 4) level = j < 0.4 ? 1 : (j < 0.8 ? 2 : 3);
  else if (dayIndex <= 9) level = j < 0.3 ? 2 : 3;
  else level = 3;
  if (intensity < 0.5 && level < 3) level = level + 1; // gentle mode → more open
  return level;
}

function isEmailLikelyValid(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || "").trim());
}

/* analytics — guarded, never throws into the page */
function pt(type, props) { try { if (window.plazmaTrack) window.plazmaTrack(type, props || {}); } catch (e) {} }
function pid(email, name, phone) { try { if (window.plazmaIdentify) window.plazmaIdentify(email, name, phone); } catch (e) {} }

/* Meta pixel — persona source + guarded trackCustom */
var PLAZMA_PERSONA = (window.PLAZMA_ANALYTICS && window.PLAZMA_ANALYTICS.persona) || "landing";
var META_SOURCE = "landing_" + PLAZMA_PERSONA;
function pm(eventName, customData, eventId) { try { if (window.plazmaMeta) window.plazmaMeta.track(eventName, customData || {}, eventId); } catch (e) {} }
function metaCD(centrumName, extra) { return Object.assign({ source: META_SOURCE, centrum: centrumName }, extra || {}); }

function Booking({ centrum, centrumKey, onCentrumChange, onSubmittedScroll, lang = "hu", source = "landing", urgency = 0.85 }) {
  const T = STRINGS[lang] || STRINGS.hu;
  const [state, dispatch] = useReducer(reducer, initial);

  const [days, setDays] = useState([]);
  const [loadingDays, setLoadingDays] = useState(true);
  const [daysError, setDaysError] = useState(null);

  const [slotsForSelected, setSlotsForSelected] = useState(null);
  const [loadingSlots, setLoadingSlots] = useState(false);

  const [exclusion, setExclusion] = useState(null);
  const [exclusionInfo, setExclusionInfo] = useState(null);
  const [emailStatus, setEmailStatus] = useState("idle");
  const lastExclusionEmailRef = useRefB({ value: "", site: "", date: "", ts: 0, fingerprint: "" });

  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState(null);

  // -------- Effect 1: day overview --------
  useEffectB(() => {
    let cancelled = false;
    setLoadingDays(true);
    setDaysError(null);
    setSlotsForSelected(null);
    setExclusion(null);
    setExclusionInfo(null);
    setEmailStatus("idle");

    window.bookingClient.fetchDayOverview(centrum.key)
      .then((overview) => {
        if (cancelled) return;
        const now = new Date();
        const built = overview.days.map((d, i) => {
          const base = buildDayLabel(d.date, i, T);
          let cnt = d.total_available;
          if (i === 0) {
            const nowHH = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
            cnt = (d.slots || []).filter((s) => s.start > nowHH).reduce((sum, s) => sum + (s.available || 0), 0);
          }
          const realCap = capCodeFromCount(cnt);
          return { ...base, capCode: manufacturedDayCap(realCap, d.date, i, urgency) };
        });
        setDays(built);
        const firstAvail = built.find((d) => d.capCode > 0);
        if (firstAvail) dispatch({ type: "PICK_DATE", iso: firstAvail.iso });
        setLoadingDays(false);
      })
      .catch((e) => {
        if (cancelled) return;
        console.error("fetchDayOverview failed:", e);
        setDaysError(T.calendarError);
        setLoadingDays(false);
      });
    return () => { cancelled = true; };
  }, [centrum.key, lang, urgency]);

  // -------- Meta pixel: PageView per selected centrum (load + centrum switch) --------
  useEffectB(() => { pm("PageView - " + centrum.name, metaCD(centrum.name)); }, [centrum.key]);

  // -------- Effect 2: slots for selected day --------
  useEffectB(() => {
    if (!state.selectedDate) { setSlotsForSelected(null); return; }
    let cancelled = false;
    setLoadingSlots(true);
    window.bookingClient.fetchSlotsForDay(centrum.key, state.selectedDate)
      .then((res) => {
        if (cancelled) return;
        setSlotsForSelected(res);
        setLoadingSlots(false);
      })
      .catch((e) => {
        if (cancelled) return;
        console.error("fetchSlotsForDay failed:", e);
        setSlotsForSelected({ ok: false, message: T.slotsError });
        setLoadingSlots(false);
      });
    return () => { cancelled = true; };
  }, [centrum.key, state.selectedDate]);

  // -------- Effect 3: debounced exclusion check --------
  useEffectB(() => {
    const email = state.form.email.trim();
    if (!email || !isEmailLikelyValid(email)) {
      setExclusion(null); setExclusionInfo(null); setEmailStatus("idle");
      return;
    }
    if (!state.selectedDate) return;
    setEmailStatus("checking");
    const timer = setTimeout(() => {
      const fingerprint = email + "|" + centrum.key + "|" + state.selectedDate;
      lastExclusionEmailRef.current = { value: email, site: centrum.key, date: state.selectedDate, ts: Date.now(), fingerprint };
      // analytics: identify the visitor as soon as a valid email is entered (captures non-completers too)
      pid(email, state.form.name.trim() || null, state.form.phone.trim() || null);
      window.bookingClient.checkExclusion(centrum.key, state.selectedDate, email)
        .then((res) => {
          const curFP = state.form.email.trim() + "|" + centrum.key + "|" + state.selectedDate;
          if (curFP !== fingerprint) return;
          if (!res.ok) { setExclusion(null); setExclusionInfo(null); setEmailStatus("error"); return; }
          setExclusionInfo(res.info || null);
          if (res.excluded) { setExclusion(res); setEmailStatus("exclusion"); }
          else {
            setExclusion(null); setEmailStatus("ok");
            if (state.selectedSlot && Array.isArray(res.data) && !res.data.includes(state.selectedSlot)) {
              dispatch({ type: "PICK_SLOT", id: null });
              setSubmitError(T.errSlotFull);
              setSlotsForSelected({ ok: true, data: res.data });
            }
          }
        })
        .catch((e) => { console.error("checkExclusion failed:", e); setEmailStatus("error"); });
    }, 500);
    return () => clearTimeout(timer);
  }, [state.form.email, state.selectedDate, centrum.key]);

  const selectedDay = days.find((d) => d.iso === state.selectedDate);
  const selectedDayIndex = days.findIndex((d) => d.iso === state.selectedDate);
  const slots = (slotsForSelected && slotsForSelected.ok && slotsForSelected.data)
    ? slotsForSelected.data.map((t) => ({ id: t, taken: false }))
    : [];
  const slotDayMessage = slotsForSelected && !slotsForSelected.ok ? slotsForSelected.message : null;

  const valid = state.form.name.trim().length > 2 &&
    isEmailLikelyValid(state.form.email) &&
    /^[\d +()\-]{8,}$/.test(state.form.phone) &&
    state.form.consent && state.selectedSlot && !exclusion && !submitting;

  const submit = async (e) => {
    e.preventDefault();
    if (!valid || submitting) return;
    setSubmitError(null);
    pt("submit", { centrum: centrum.key, date: state.selectedDate, slot: state.selectedSlot });
    pm("Időpontfoglalás - " + centrum.name, metaCD(centrum.name));

    const lastTs = lastExclusionEmailRef.current && lastExclusionEmailRef.current.ts;
    const isStale = !lastTs || (Date.now() - lastTs) > 60 * 1000;
    if (isStale) {
      setSubmitting(true);
      try {
        const fresh = await window.bookingClient.checkExclusion(centrum.key, state.selectedDate, state.form.email.trim());
        if (!fresh.ok) { setSubmitError(T.errStale); setSubmitting(false); return; }
        if (fresh.excluded) { setExclusion(fresh); setEmailStatus("exclusion"); setSubmitting(false); return; }
        if (!fresh.data.includes(state.selectedSlot)) {
          setSubmitError(T.errSlotFull);
          setSlotsForSelected({ ok: true, data: fresh.data });
          dispatch({ type: "PICK_SLOT", id: null });
          setSubmitting(false); return;
        }
        lastExclusionEmailRef.current.ts = Date.now();
      } catch (err) { setSubmitError(T.errNetwork); setSubmitting(false); return; }
    }

    setSubmitting(true);
    try {
      const metaEventId = (window.plazmaMeta && window.plazmaMeta.newEventId()) || null;
      const res = await window.bookingClient.createBooking({
        site: centrum.key,
        date: `${state.selectedDate}T${state.selectedSlot}`,
        name: state.form.name.trim(),
        email: state.form.email.trim(),
        phone: state.form.phone,
        consent: state.form.consent === true,
        source,
        lang,
        // Meta CAPI dedup + match-quality inputs
        eventId: metaEventId,
        fbp: (window.plazmaMeta && window.plazmaMeta.fbp()) || null,
        fbc: (window.plazmaMeta && window.plazmaMeta.fbc()) || null,
        persona: PLAZMA_PERSONA,
        pageUrl: (typeof location !== "undefined" && location.href) || "",
        userAgent: (typeof navigator !== "undefined" && navigator.userAgent) || "",
      });
      // analytics conversion — identify (final) + booked
      pid(state.form.email.trim(), state.form.name.trim() || null, state.form.phone.trim() || null);
      pt("booked", { centrum: centrum.key, date: state.selectedDate, slot: state.selectedSlot, source });
      // Meta pixel — mirror EXACTLY the conversion events the server sent to CAPI
      // (shared event_id => Meta deduplicates browser + server within 48h).
      try {
        const cd = metaCD(centrum.name, { appointment_id: res && res.appointmentId });
        const evs = (res && res.metaEvents) || [];
        if (evs.length) evs.forEach((ev) => pm(ev.name, cd, ev.eventId));
        else if (metaEventId) pm("Időpontfoglalás sikeres - " + centrum.name, cd, metaEventId);
      } catch (e) {}
      dispatch({ type: "SUBMIT" });
      setTimeout(onSubmittedScroll, 60);
    } catch (err) {
      console.error("createBooking failed:", err);
      let msg;
      if (err.code === "SLOT_FULL") {
        msg = T.errSlotFull;
        window.bookingClient.invalidateCache(centrum.key, state.selectedDate);
        window.bookingClient.fetchSlotsForDay(centrum.key, state.selectedDate, { forceFresh: true }).then((r) => setSlotsForSelected(r));
        dispatch({ type: "PICK_SLOT", id: null });
      } else if (err.code === "DUPLICATE") { msg = T.errDuplicate; }
      else if (err.code === "EXCLUSION") { msg = T.errExclusion; if (err.detail) setExclusion(err.detail); }
      else if (err.field === "phone") { msg = T.errPhone; }
      else if (err.field === "email") { msg = T.errEmail; }
      else { msg = err.message || T.errGeneric; }
      setSubmitError(msg);
    } finally { setSubmitting(false); }
  };

  const loading = loadingDays;
  const loadError = daysError;

  if (state.isSubmitted) {
    return <Success centrum={centrum} date={selectedDay} slot={state.selectedSlot} T={T} onReset={() => dispatch({ type: "RESET" })} />;
  }

  return (
    <section id="booking" className="px-5 lg:px-12 pt-16 lg:pt-28 pb-8 lg:pb-12 overflow-x-hidden">
      <div className="max-w-5xl mx-auto">
        <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-4 lg:gap-6 mb-8 lg:mb-12">
          <div className="min-w-0">
            <div className="mb-2 mono text-[11px] uppercase tracking-[0.2em] text-accent">{T.eyebrow}</div>
            <h2 className="serif text-[36px] sm:text-[44px] lg:text-[64px] leading-[0.98] text-ink" style={{ letterSpacing: "-0.04em" }}>
              {T.titleA}<em>{T.titleEm}</em>
            </h2>
          </div>
          <div className="w-full lg:w-auto lg:text-right lg:max-w-sm min-w-0">
            <p className="text-[12px] mono uppercase tracking-wider text-ink/55 mb-2">{T.whichCentre}</p>
            <div className="grid grid-cols-3 gap-1 p-1 rounded-2xl border-2 border-ink bg-white w-full lg:inline-grid lg:w-auto">
              {["nyugati","blaha","baja"].map(k => {
                const c = CENTRUMS[k];
                const active = centrumKey === k;
                return (
                  <button key={k} onClick={() => onCentrumChange(k)}
                    className={"h-10 px-2 sm:px-3 rounded-xl text-[13px] font-medium flex items-center justify-center gap-1.5 transition min-w-0 " +
                      (active ? "bg-ink text-white" : "text-ink/65 hover:text-ink hover:bg-cream")}>
                    <IPin size={12} />
                    <span className="truncate">{c.name}</span>
                  </button>
                );
              })}
            </div>
            <p className="text-[13px] lg:text-[14px] text-ink/60 mt-2 truncate">{centrum.fullName} · {centrum.short}</p>
          </div>
        </div>

        <div className="rounded-[28px] lg:rounded-[40px] border-2 border-ink bg-bg overflow-hidden">
          <div className="grid grid-cols-1 lg:grid-cols-2">
            {/* LEFT — calendar + slots */}
            <div className="p-5 lg:p-10 border-b-2 border-ink/10 lg:border-b-0 lg:border-r-2 bg-cream/40 min-w-0">
              <div>
                <StepHeader n="01" label={T.step1} />
                {loading ? (
                  <div className="mt-4 flex items-center gap-2 text-[13px] mono text-ink/55"><Spinner /> {T.loadingTimes}</div>
                ) : loadError ? (
                  <div className="mt-4 rounded-xl bg-accent/10 border border-accent/30 text-accent text-[13px] p-3">{loadError}</div>
                ) : (
                  <>
                    <div className="mt-4 -mx-5 lg:-mx-10 px-5 lg:px-10 overflow-x-auto no-scrollbar">
                      <div className="flex gap-2 pb-1">
                        {days.map((d) =>
                        <DayPill key={d.iso} d={d} active={state.selectedDate === d.iso} onClick={() => dispatch({ type: "PICK_DATE", iso: d.iso })} />)}
                      </div>
                    </div>
                    <Legend T={T} />
                  </>
                )}
              </div>

              <div className="mt-8">
                <StepHeader n="02" label={selectedDay ? T.dayTitle(selectedDay) : T.pickDay} />
                {loadingSlots ? (
                  <div className="mt-4 flex items-center gap-2 text-[13px] mono text-ink/55"><Spinner /> {T.loadingSlots}</div>
                ) : slotDayMessage ? (
                  <div className="mt-4 rounded-xl bg-surface border border-border text-ink/65 text-[13px] p-3">{slotDayMessage}</div>
                ) : slots.length === 0 ? (
                  <div className="mt-4 rounded-xl bg-surface border border-border text-ink/65 text-[13px] p-3">{T.noFreeTimes}</div>
                ) : (
                  <div className="mt-4 grid grid-cols-3 lg:grid-cols-4 gap-2">
                    {slots.map((s) =>
                    <SlotButton key={s.id} s={s} active={state.selectedSlot === s.id}
                      heat={slotHeat(state.selectedDate, s.id, selectedDayIndex, urgency)}
                      onClick={() => { dispatch({ type: "PICK_SLOT", id: s.id }); pt("slot_select", { centrum: centrum.key, date: state.selectedDate, slot: s.id }); }} />)}
                  </div>
                )}
                {!state.selectedSlot && slots.length > 0 &&
                <p className="mt-3 text-[12.5px] text-ink/55 mono">{T.slotNote}</p>}
                {state.selectedSlot && selectedDay &&
                <div className="mt-3 inline-flex items-center gap-2 rounded-full bg-ink text-white px-3 py-1.5 mono text-[12px]">
                  <ICheck size={14} /> {T.dayTitle(selectedDay)} · {state.selectedSlot}
                </div>}
              </div>
            </div>

            {/* RIGHT — form */}
            <form onSubmit={submit} className="p-5 lg:p-10 min-w-0">
              <StepHeader n="03" label={T.step3} />
              <ExclusionPanel exclusion={exclusion} centrum={centrum} T={T}
                onTryAnotherCentrum={() => { const top = document.querySelector('#booking'); if (top) top.scrollIntoView({ behavior: "smooth", block: "start" }); }}
                onSuggestedDate={(iso) => {
                  const exists = days.some((d) => d.iso === iso);
                  if (exists) {
                    dispatch({ type: "PICK_DATE", iso }); setExclusion(null); setEmailStatus("ok");
                    const top = document.querySelector('#booking'); if (top) top.scrollIntoView({ behavior: "smooth", block: "start" });
                  } else { setSubmitError(T.exclSuggestOutside(formatDate(iso, lang))); }
                }} />
              {exclusionInfo && <ExclusionInfoNote info={exclusionInfo} T={T} />}
              <div className="mt-3 space-y-1">
                <Field id="name" label={T.fieldName} value={state.form.name} onChange={(v) => dispatch({ type: "SET_FIELD", key: "name", value: v })} />
                <Field id="email" label={T.fieldEmail} value={state.form.email}
                  onChange={(v) => dispatch({ type: "SET_FIELD", key: "email", value: v })}
                  rightAdornment={emailStatus === "checking" ? <span className="flex items-center gap-1 text-[11px] mono text-ink/55"><Spinner /> {T.checking}</span> :
                                  emailStatus === "ok" ? <span className="text-[12px] text-success" aria-label="ok">✓</span> :
                                  emailStatus === "exclusion" ? <span className="text-[12px] text-accent">⚠</span> : null} />
                <Field id="phone" label={T.fieldPhone} prefix="+36" value={state.form.phone} onChange={(v) => dispatch({ type: "SET_FIELD", key: "phone", value: v })} />
              </div>

              <label className="mt-6 flex items-start gap-3 cursor-pointer select-none">
                <span onClick={(e) => { e.preventDefault(); dispatch({ type: "SET_FIELD", key: "consent", value: !state.form.consent }); }}
                  className={"mt-0.5 w-5 h-5 rounded-md border-2 flex items-center justify-center transition shrink-0 " + (state.form.consent ? "bg-ink border-ink text-white" : "bg-white border-ink/30 text-transparent")}>
                  <ICheck size={14} />
                </span>
                <span className="text-[13px] text-ink/70 leading-[1.5]">
                  {T.consentPre}<a href="https://plazmacentrum.hu/felhasznalasi-feltetelek" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">{T.consentTerms}</a>{T.consentMid}<a href="https://plazmacentrum.hu/adatvedelmi-nyilatkozat" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">{T.consentPrivacy}</a>{T.consentPost}
                </span>
              </label>

              {(() => {
                const filledRequired =
                  (state.form.name.trim().length > 2 ? 1 : 0) +
                  (/^[\d +()\-]{8,}$/.test(state.form.phone) ? 1 : 0) +
                  (state.selectedSlot ? 1 : 0);
                const consentOk = state.form.consent;
                const progressMsg = !consentOk && filledRequired === 3 ? T.pNeedConsent
                  : filledRequired === 0 ? T.p0
                  : filledRequired === 1 ? T.p1
                  : filledRequired === 2 ? T.p2
                  : valid ? T.pValid : T.pConsentLeft;
                return (
                  <>
                    <p className="mt-6 text-[12px] mono text-ink/55 text-center transition-all">{submitting ? T.bookingInProgress : progressMsg}</p>
                    <button type="submit" disabled={!valid || submitting}
                      className={"mt-2 w-full h-14 rounded-2xl text-[16px] font-medium flex items-center justify-center gap-2 transition " + (
                        submitting ? "bg-ink text-white cursor-wait" :
                        valid ? "bg-accent text-white active:scale-[0.97] hover:scale-[1.02] shadow-[0_14px_30px_-12px_rgba(230,57,70,0.7)]" :
                        "bg-surface text-ink/40 border border-border cursor-not-allowed")}>
                      {submitting ? <><Spinner /> {T.booking}</> : valid ? <>{T.submitCta} <IArrow size={18} /></> : T.submitFill}
                    </button>
                    {submitError && <div className="mt-3 rounded-xl bg-accent/10 border border-accent/30 text-accent text-[13px] p-3 text-center">{submitError}</div>}
                  </>
                );
              })()}
              <p className="mt-3 text-[12px] text-ink/45 text-center mono">{T.noSpam}</p>
            </form>
          </div>
        </div>
      </div>
    </section>);
}

function Spinner() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden style={{ animation: "spin 0.8s linear infinite" }}>
      <circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity="0.25" strokeWidth="3" />
      <path d="M12 3a9 9 0 0 1 9 9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
    </svg>);
}

function addDaysIso(dateStr, daysAfter = 1) {
  const d = new Date(String(dateStr).slice(0, 10) + "T00:00:00");
  if (isNaN(d.getTime())) return null;
  d.setDate(d.getDate() + daysAfter);
  return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}

function ExclusionPanel({ exclusion, centrum, T, onTryAnotherCentrum, onSuggestedDate }) {
  if (!exclusion || !exclusion.excluded) return null;
  const { reason, detail } = exclusion;
  const lang = T === STRINGS.en ? "en" : "hu";

  let title, body, primary, secondary;
  switch (reason) {
    case "currently": {
      const until = detail && (detail.excluded_until || detail.ending_date || detail.until || detail.end_date);
      const nextIso = until ? addDaysIso(until, 1) : null;
      title = T.exclCurrentlyTitle;
      body = until ? T.exclCurrentlyUntil(formatDate(until, lang)) : T.exclCurrentlyNoDate;
      primary = nextIso && onSuggestedDate
        ? { kind: "suggest", label: T.exclSuggestLabel(formatDate(nextIso, lang)), target: nextIso }
        : { kind: "call", label: T.exclCallLabel(centrum.short), href: "tel:" + centrum.phone.replace(/\s/g, "") };
      secondary = onTryAnotherCentrum ? { kind: "centrum" } : null;
      break;
    }
    case "permanently": {
      title = T.exclPermTitle; body = T.exclPermBody;
      primary = { kind: "call", label: T.exclCallLabel(centrum.short), href: "tel:" + centrum.phone.replace(/\s/g, "") };
      secondary = onTryAnotherCentrum ? { kind: "centrum" } : null; break;
    }
    case "cancellation": {
      title = T.exclCancelTitle; body = T.exclCancelBody;
      primary = { kind: "call", label: T.exclCallLabel(centrum.short), href: "tel:" + centrum.phone.replace(/\s/g, "") };
      secondary = onTryAnotherCentrum ? { kind: "centrum" } : null; break;
    }
    default: {
      title = T.exclDefaultTitle; body = T.exclDefaultBody;
      primary = { kind: "call", label: T.exclCallLabel(centrum.short), href: "tel:" + centrum.phone.replace(/\s/g, "") };
      secondary = onTryAnotherCentrum ? { kind: "centrum" } : null;
    }
  }

  return (
    <div className="mt-4 rounded-2xl border-2 border-accent/40 bg-accent/[0.04] p-4 lg:p-5" role="alert">
      <div className="flex items-start gap-3">
        <div className="shrink-0 w-8 h-8 rounded-full bg-accent text-white flex items-center justify-center text-[16px]" aria-hidden>!</div>
        <div className="flex-1 min-w-0">
          <div className="serif text-[18px] lg:text-[20px] text-ink leading-tight">{title}</div>
          <p className="mt-1 text-[13.5px] text-ink/75 leading-relaxed">{body}</p>
          <div className="mt-3 flex flex-wrap gap-2">
            {primary.kind === "suggest" ? (
              <button type="button" onClick={() => onSuggestedDate(primary.target)} className="inline-flex items-center gap-1.5 h-9 px-3 rounded-lg bg-accent text-white text-[13px] font-medium hover:bg-accent/90 transition">{primary.label}</button>
            ) : (
              <a href={primary.href} className="inline-flex items-center gap-1.5 h-9 px-3 rounded-lg bg-accent text-white text-[13px] font-medium hover:bg-accent/90 transition">{primary.label}</a>
            )}
            {secondary && secondary.kind === "centrum" && (
              <button type="button" onClick={onTryAnotherCentrum} className="inline-flex items-center gap-1.5 h-9 px-3 rounded-lg bg-white border border-border text-ink text-[13px] font-medium hover:border-ink/40 transition">{T.exclAnotherCentre}</button>
            )}
          </div>
        </div>
      </div>
    </div>);
}

function ExclusionInfoNote({ info, T }) {
  if (!info) return null;
  if (info.kind !== "no_bloodtest") return null;
  return (
    <div className="mt-4 rounded-2xl border border-butter bg-butter/40 p-4 lg:p-5" role="status">
      <div className="flex items-start gap-3">
        <div className="shrink-0 w-7 h-7 rounded-full bg-ink/10 text-ink flex items-center justify-center text-[14px]" aria-hidden>ℹ</div>
        <div className="flex-1 min-w-0">
          <div className="text-[14px] font-semibold text-ink">{T.noBloodTitle}</div>
          <p className="mt-1 text-[12.5px] text-ink/70 leading-relaxed">{T.noBloodBody}</p>
        </div>
      </div>
    </div>);
}

function formatDate(d, lang = "hu") {
  if (!d) return "";
  const parts = String(d).slice(0, 10).split("-");
  if (parts.length !== 3) return String(d);
  const m = parseInt(parts[1], 10);
  const day = parseInt(parts[2], 10);
  if (lang === "en") {
    const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    return `${day} ${months[m - 1]} ${parts[0]}`;
  }
  const months = ["jan", "feb", "márc", "ápr", "máj", "jún", "júl", "aug", "szept", "okt", "nov", "dec"];
  return `${parts[0]}. ${months[m - 1]}. ${day}.`;
}

function StepHeader({ n, label }) {
  return (
    <div className="flex items-baseline justify-between">
      <div className="flex items-baseline gap-3">
        <span className="serif text-[22px] text-ink/30">{n}</span>
        <span className="text-[15px] font-medium text-ink">{label}</span>
      </div>
    </div>);
}

function Field({ id, label, value, onChange, prefix, rightAdornment }) {
  return (
    <div className="field" style={{ minWidth: 0 }}>
      <div className="flex items-baseline gap-2 pt-1" style={{ minWidth: 0 }}>
        {prefix && <span className="mono text-[15px] text-ink/55 pt-[22px] shrink-0">{prefix}</span>}
        <input id={id} placeholder=" " value={value} onChange={(e) => onChange(e.target.value)} autoComplete="off" style={{ flex: "1 1 0", minWidth: 0, width: 0 }} />
        {rightAdornment && <span className="pt-[22px] pr-1 shrink-0">{rightAdornment}</span>}
      </div>
      <label htmlFor={id} style={prefix ? { left: 38 } : null}>{label}</label>
    </div>);
}

function Legend({ T }) {
  return (
    <div className="mt-3 flex items-center gap-3 text-[11.5px] text-ink/50 mono">
      <span className="flex items-center gap-1.5"><CapDot code={3} />{T.legend.many}</span>
      <span className="flex items-center gap-1.5"><CapDot code={2} />{T.legend.some}</span>
      <span className="flex items-center gap-1.5"><CapDot code={1} />{T.legend.few}</span>
      <span className="flex items-center gap-1.5"><CapDot code={0} />{T.legend.full}</span>
    </div>);
}

/* ---------- Success state ---------- */
function Success({ centrum, date, slot, onReset, T }) {
  return (
    <section id="booking" className="px-5 pt-10 pb-8">
      <div className="rounded-3xl border border-border bg-white p-6 overflow-hidden relative">
        <div className="mx-auto w-20 h-20 rounded-full bg-success/10 flex items-center justify-center ring-in">
          <svg width="44" height="44" viewBox="0 0 24 24" fill="none">
            <path className="check-path" d="m5 12 4 4 10-10" stroke="#00C896" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>
        <h2 className="mt-5 serif text-[40px] leading-[1] text-ink text-center" style={{ letterSpacing: "-0.03em" }}>
          {T.successTitleA}<em>{T.successTitleEm}</em>
        </h2>
        <p className="mt-2 text-center text-[14px] text-ink/60">{T.successSub}</p>
        <div className="mt-6 rounded-2xl bg-surface border border-border divide-y divide-border">
          <Row k={T.rowCentre} v={centrum.fullName} sub={centrum.address} />
          <Row k={T.rowTime} v={`${date ? T.dayTitle(date) : ""} · ${slot}`} sub={T.timeSub} />
          <Row k={T.rowBring} v={T.bringVal} sub={T.bringSub} />
          <Row k={T.rowPay} v={T.payVal} sub={T.paySub} />
        </div>
        <div className="mt-5 grid grid-cols-2 gap-2">
          <button className="h-12 rounded-xl border border-border bg-white text-[13.5px] font-medium text-ink flex items-center justify-center gap-2"><ICalendar size={16} />{T.addCal}</button>
          <button className="h-12 rounded-xl border border-border bg-white text-[13.5px] font-medium text-ink flex items-center justify-center gap-2"><IShare size={16} />{T.share}</button>
        </div>
        <div className="mt-5 text-center">
          <button onClick={onReset} className="text-[13px] text-ink/55 underline underline-offset-4">{T.bookAnother}</button>
        </div>
      </div>
    </section>);
}

function Row({ k, v, sub }) {
  return (
    <div className="px-4 py-3 flex items-start justify-between gap-3">
      <div className="mono text-[11px] uppercase tracking-wider text-ink/50 pt-1 w-24 shrink-0">{k}</div>
      <div className="text-right flex-1 min-w-0">
        <div className="text-[14.5px] text-ink">{v}</div>
        {sub && <div className="text-[12px] text-ink/50 mt-0.5">{sub}</div>}
      </div>
    </div>);
}

window.Booking = Booking;
