/* ============================================================
   Farmers Fightback Fundraiser — ticketing funnel (served at /fundraiser)
   Design ported from claude.ai/design ticketing/Farmers Fightback
   Rally Ticketing.html. Two flows:
     - paid: Adult + Kids qty → Stripe hosted Checkout → confirm
     - comp: /fundraiser?claim=<token> → confirm (skips Stripe on $0)
   Backend:
     POST /api/rally-checkout  — creates Stripe Checkout Session
     POST /api/rally-claim     — validates comp token, records claim
     GET  ?session_id=X        — Stripe returns here on success
   ============================================================ */

const { useState, useRef, useEffect } = React;

/* Ticket price — this mirrors the design's TWEAK_DEFAULTS. To change after
   ship, update this constant; the server-side Stripe Price still controls
   what actually gets charged, so both must stay in sync.
   One ticket type, one price, for everyone attending — there is no free or
   concession entry. Under-18s need a ticket like anyone else and must be
   accompanied by an adult. */
const ADULT_PRICE = 50;

const EVENT = {
  date: "Saturday 29 August",
  gates: "Gate from 5:00 pm",
  venue: "Marnoo Cricket Ground",
  place: "Marnoo, Victoria",
};

const money = (n) => "$" + Number(n).toLocaleString("en-AU");

/* ---------- tiny inline icons ---------- */
const I = {
  ticket: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2a2 2 0 0 0 0 4v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 0 0-4V8z"/><path d="M14 6v12" strokeDasharray="2 2"/></svg>),
  lock: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>),
  check: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M20 6 9 17l-5-5"/></svg>),
  star: (p) => (<svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M12 2l2.9 6.3 6.9.8-5.1 4.7 1.4 6.8L12 17.8 5.9 21.4l1.4-6.8L2.2 9.9l6.9-.8z"/></svg>),
  warn: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M4.7 19h14.6a2 2 0 0 0 1.7-3l-7.3-12a2 2 0 0 0-3.4 0L3 16a2 2 0 0 0 1.7 3z"/></svg>),
  copy: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>),
  fb: (p) => (<svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M14 8.5V6.8c0-.8.2-1.3 1.4-1.3H17V2.7c-.3 0-1.3-.1-2.4-.1-2.4 0-4 1.5-4 4.1v1.8H8v3h2.6V21H14v-8.5h2.6l.4-3z"/></svg>),
  wa: (p) => (<svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M12 2a10 10 0 0 0-8.5 15.2L2 22l4.9-1.3A10 10 0 1 0 12 2zm5.3 14.1c-.2.6-1.3 1.2-1.8 1.2-.5.1-1 .2-3.3-.7-2.8-1.1-4.5-3.9-4.7-4.1-.1-.2-1.1-1.4-1.1-2.7 0-1.3.7-1.9.9-2.2.2-.2.5-.3.7-.3h.5c.2 0 .4 0 .6.5l.8 2c.1.2.1.4 0 .5l-.4.6c-.2.2-.3.4-.1.7.2.3.8 1.3 1.7 2.1 1.2 1 2.1 1.4 2.4 1.5.3.1.5.1.7-.1l.9-1c.2-.2.4-.2.6-.1l1.9.9c.3.1.4.2.5.3 0 .2 0 .8-.2 1.4z"/></svg>),
  mail: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>),
  sms: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 12a8 8 0 0 1-11.4 7.2L3 21l1.8-5.4A8 8 0 1 1 21 12z"/></svg>),
  portrait: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="9" r="3.3"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg>),
  music: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>),
  food: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 3v7a2 2 0 0 0 2 2 2 2 0 0 0 2-2V3M6 12v9M18.5 3c-1.7 0-3 2.2-3 5.5s1.3 4 3 4v8.5"/></svg>),
  family: (p) => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="8" cy="7" r="2.5"/><circle cx="16.5" cy="8" r="2"/><path d="M3.5 20a4.5 4.5 0 0 1 9 0M13 20a3.8 3.8 0 0 1 7.5 0"/></svg>),
};

/* ---------- helpers ---------- */
function readRef() {
  try {
    const q = new URLSearchParams(window.location.search);
    return q.get("ref") || sessionStorage.getItem("ff_ref") || "";
  } catch { return ""; }
}
function readClaim() {
  try {
    const q = new URLSearchParams(window.location.search);
    return q.get("claim") || sessionStorage.getItem("ff_rally_claim") || "";
  } catch { return ""; }
}
function readSessionId() {
  try {
    const q = new URLSearchParams(window.location.search);
    return q.get("session_id") || "";
  } catch { return ""; }
}

/* ---------- masthead ---------- */
/* hideBand drops the date/venue panel — /cantmakeit is for people who can't
   come, so leading with when and where the event is makes no sense there. */
function Masthead({ hideBand }) {
  return (
    <header className="ffx-mast">
      <span className="ffx-rays" />
      <span className="ffx-sun" />
      <div className="ffx-mast-in">
        <img className="ffx-logo" src="/assets/logo-horizontal.png" alt="Farmers Fightback" />
        <div className="ffx-kicker">You&rsquo;re invited to the</div>
        <h1 className="ffx-title">Farmers Fightback<span className="ffx-rally">FUNdraiser</span></h1>
        <p className="ffx-sub"><strong>A night to fight for the future of farming &mdash; everyone welcome.</strong></p>
        {!hideBand && (
          <div className="ffx-band">
            <div className="ffx-band-date">{EVENT.date}</div>
            <div className="ffx-band-time">{EVENT.gates}</div>
            <div className="ffx-band-place">{EVENT.venue}<small>{EVENT.place}</small></div>
          </div>
        )}
      </div>
    </header>
  );
}

/* ---------- stepper ---------- */
function Stepper({ step, comp, allDone }) {
  const steps = comp ? ["Your details", "Your tickets"] : ["Tickets", "Payment", "Your tickets"];
  return (
    <div className="ffx-stepper">
      {steps.map((s, i) => {
        const done = allDone || i < step;
        const on = !allDone && i === step;
        return (
          <div key={s} className={"ffx-step" + (on ? " on" : "") + (done ? " done" : "")}>
            <span className="ffx-step-n">{done ? <I.check width="14" height="14" /> : i + 1}</span>
            <span className="ffx-step-l">{s}</span>
          </div>
        );
      })}
    </div>
  );
}

/* ---------- qty stepper ---------- */
function Qty({ value, onChange, min = 0, max = 20 }) {
  return (
    <div className="ffx-qty">
      <button type="button" className="ffx-qty-b" disabled={value <= min} onClick={() => onChange(Math.max(min, value - 1))} aria-label="Decrease">&minus;</button>
      <span className="ffx-qty-v">{value}</span>
      <button type="button" className="ffx-qty-b" disabled={value >= max} onClick={() => onChange(Math.min(max, value + 1))} aria-label="Increase">+</button>
    </div>
  );
}

/* ---------- field ---------- */
function Field({ label, opt, err, ...rest }) {
  return (
    <label className={"ffx-field" + (err ? " err" : "")}>
      <span className="ffx-field-l">{label}{opt ? <em>optional</em> : <span className="ffx-req" aria-hidden="true">*</span>}</span>
      <input className="ffx-input" {...rest} />
      {err && <span className="ffx-field-e">{err}</span>}
    </label>
  );
}

/* ---------- getting there ----------
   The map and the directions link both address the venue by name so Google
   resolves the real place. VENUE_LAT/LNG are Marnoo's town-centre
   coordinates and are used only for the rough "how far away am I" figure —
   over the distances people drive to this, the few hundred metres between
   the town centre and the ground is noise. Straight-line distance is
   labelled as such; the drive itself is left to Google. No API key is
   needed for either the embed or the directions link. */
const VENUE_NAME = "Marnoo Cricket Ground";
const VENUE_ADDRESS = "Marnoo Recreation Reserve, Park St, Marnoo VIC 3387";
const VENUE_QUERY = "Marnoo Recreation Reserve, Marnoo VIC 3387";
const VENUE_LAT = -36.65;
const VENUE_LNG = 142.8833;

function GettingThere() {
  const [status, setStatus] = useState("idle"); // idle | locating | done | error
  const [km, setKm] = useState(null);
  const [origin, setOrigin] = useState(null);
  const [msg, setMsg] = useState("");

  const q = encodeURIComponent(VENUE_QUERY);
  const embedSrc = `https://maps.google.com/maps?q=${q}&z=12&output=embed`;
  const directionsUrl = origin
    ? `https://www.google.com/maps/dir/?api=1&origin=${origin.lat},${origin.lng}&destination=${q}`
    : `https://www.google.com/maps/dir/?api=1&destination=${q}`;

  const howFar = () => {
    if (!navigator.geolocation) {
      setStatus("error");
      setMsg("Your browser can’t share your location. Use Get directions instead.");
      return;
    }
    setStatus("locating");
    setMsg("");
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        const lat = pos.coords.latitude, lng = pos.coords.longitude;
        const toRad = (d) => (d * Math.PI) / 180;
        const dLat = toRad(VENUE_LAT - lat), dLng = toRad(VENUE_LNG - lng);
        const a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
          + Math.cos(toRad(lat)) * Math.cos(toRad(VENUE_LAT)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
        setKm(2 * 6371 * Math.asin(Math.min(1, Math.sqrt(a))));
        setOrigin({ lat, lng });
        setStatus("done");
      },
      (err) => {
        setStatus("error");
        setMsg(err && err.code === 1
          ? "Location access was blocked. You can still tap Get directions."
          : "Couldn’t work out where you are. Tap Get directions instead.");
      },
      { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }
    );
  };

  const prettyKm = (n) => (n < 10 ? n.toFixed(1) : Math.round(n).toLocaleString("en-AU"));

  return (
    <section className="ffx-map">
      <div className="ffx-map-head">
        <span className="ffx-map-eb">Getting there</span>
        <h2>{VENUE_NAME}</h2>
        <p>{VENUE_ADDRESS}</p>
      </div>

      <div className="ffx-map-frame">
        <iframe
          title={`Map showing ${VENUE_NAME}`}
          src={embedSrc}
          loading="lazy"
          referrerPolicy="no-referrer-when-downgrade"
          allowFullScreen
        />
      </div>

      <div className="ffx-map-actions">
        <a className="ffx-btn ffx-btn-map" href={directionsUrl} target="_blank" rel="noopener noreferrer">Get directions</a>
        <button type="button" className="ffx-btn ffx-btn-mapghost" onClick={howFar} disabled={status === "locating"}>
          {status === "locating" ? "Finding you…" : "How far is it from me?"}
        </button>
      </div>

      {status === "done" && km != null && (
        <p className="ffx-map-dist">
          You&rsquo;re about <b>{prettyKm(km)} km</b> away in a straight line
          &mdash; tap <span className="ffx-map-dist-cta">Get directions</span> for the drive.
        </p>
      )}
      {status === "error" && <p className="ffx-map-dist ffx-map-dist-err">{msg}</p>}
    </section>
  );
}

/* ---------- lineup ---------- */
function LineupSection() {
  // The grid below sizes itself to however many are listed, so adding a
  // speaker back is just another entry here.
  const guests = [
    { lead: true, name: "Pauline Hanson", role: "Headline speaker · from 7pm", img: "/assets/rally-hanson.jpg?v=2", ph: "Photo of Pauline Hanson" },
    { name: "Ben Duxson", role: "Farmers Fightback", img: "/assets/rally-duxson.jpg?v=2", ph: "Photo of Ben Duxson" },
  ];
  return (
    <section className="ffx-lineup">
      <div className="ffx-lineup-head">
        <span className="ffx-lineup-eb">On the night</span>
        <div className="ffx-lineup-gates">Gate open from 5pm</div>
        <h2>Speakers from 7pm &mdash; then food &amp; music</h2>
        <p>Come for the fight, stay for the night. A proper country get-together with the people standing up for our farmers.</p>
      </div>
      <div className={"ffx-speakers" + (guests.length === 1 ? " ffx-speakers-solo" : guests.length === 2 ? " ffx-speakers-duo" : "")}>
        {guests.map((g) => (
          <div key={g.name} className={"ffx-spk" + (g.lead ? " ffx-spk-lead" : "")}>
            <div className={"ffx-spk-photo" + (g.img ? " has-img" : "")}>
              {g.img
                ? <img className="ffx-spk-img" src={g.img} alt={g.name} loading="lazy" />
                : <React.Fragment><I.portrait width="32" height="32" /><span className="ph-txt">{g.ph}</span></React.Fragment>}
            </div>
            <div className="ffx-spk-name">{g.name}</div>
            <div className="ffx-spk-role">{g.role}</div>
          </div>
        ))}
      </div>
      <div className="ffx-onnight">
        <div className="ffx-on-item"><span className="ffx-on-ic"><I.music width="18" height="18" /></span><div><b>Live music</b><span>Local bands till late</span></div></div>
        <div className="ffx-on-item"><span className="ffx-on-ic"><I.food width="18" height="18" /></span><div><b>Food &amp; bar</b><span>Country kitchen + local brews</span></div></div>
        <div className="ffx-on-item"><span className="ffx-on-ic"><I.family width="18" height="18" /></span><div><b>Family friendly</b><span>Kids welcome all night</span></div></div>
      </div>
    </section>
  );
}

/* ---------- terms modal ---------- */
function TermsModal({ onClose }) {
  return (
    <div className="ffx-modal" onClick={onClose}>
      <div className="ffx-modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="ffx-modal-top">
          <h3>Terms &amp; Conditions</h3>
          <button type="button" className="ffx-modal-x" onClick={onClose} aria-label="Close">&times;</button>
        </div>
        <div className="ffx-modal-body">
          <h4>1. Tickets &amp; payment</h4>
          <p>All ticket sales are processed securely through Stripe. Prices are in Australian dollars and include GST where applicable. Your booking is complete once payment is received; your ticket is sent to you closer to the event day as confirmation.</p>
          <h4>2. Refunds &amp; transfers</h4>
          <p>Tickets are non-refundable. Tickets are non-transferable and may only be used by the person named on the booking.</p>
          <h4>3. Ticket delivery</h4>
          <p>Tickets are not issued at the time of purchase. Your ticket will be emailed to you at a later date, closer to the event, to the email address on your booking. Your confirmation code is your proof of purchase until then.</p>
          <h4>4. Entry</h4>
          <p>Bring your ticket (printed or on your phone) to scan at the gate, or photo identification matching the name on your booking. Gates open from 6:00pm. Organisers reserve the right to refuse entry.</p>
          <h4>5. Cancellation at our discretion</h4>
          <p>We may cancel or void any ticket at any time, at our absolute discretion and without giving reasons. Where we do, the price paid for that ticket is refunded. This is separate from clause 7, under which a person removed from the event for their behaviour is not entitled to a refund.</p>
          <h4>6. Children &amp; families</h4>
          <p>Everyone attending requires a ticket, including children. There is no free or concession entry. Anyone under 18 must be accompanied by an adult at all times.</p>
          <h4>7. Conduct</h4>
          <p>This is a peaceful, family-friendly community event. Anyone behaving in a threatening, abusive or unsafe manner will be asked to leave without refund.</p>
          <h4>8. Photography &amp; media</h4>
          <p>Photography and filming take place for campaign and media purposes. By attending you consent to appearing in images and footage. Tell a staff member if you would prefer not to be photographed.</p>
          <h4>9. Program changes</h4>
          <p>Speakers, program and running times are indicative and may change without notice. Advertised guests are subject to availability.</p>
          <h4>10. Donations</h4>
          <p>Donations are voluntary, separate from ticket purchases, and non-refundable. They are not tax-deductible unless otherwise stated.</p>
          <h4>11. Privacy</h4>
          <p>Your details are collected to manage your booking and to keep you updated about the campaign. We do not sell your information, and you can unsubscribe at any time.</p>
          <h4>12. Liability</h4>
          <p>Attendees enter the venue at their own risk. The organisers are not liable for any loss, damage or injury except to the extent required by law.</p>
          <p className="ffx-modal-meta">Farmers Fightback · Marnoo, Victoria. Authorised by Ben Duxson. Questions? support@farmersfightback.com. Last updated July 2026.</p>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   STEP 1 — DETAILS  (paid + comp share this)
   ============================================================ */
function DetailsStep({ comp, claimInfo, qty, setQty, form, setForm, onNext, submitting, submitError }) {
  const [errs, setErrs] = useState({});
  const total = comp ? 0 : qty.adults * ADULT_PRICE;
  const totalTix = comp ? qty.comp : qty.adults;
  const compMax = claimInfo ? Math.max(0, (claimInfo.max_qty || 0) - (claimInfo.used_qty || 0)) : 0;

  const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));

  const submit = () => {
    const e = {};
    if (!form.first.trim()) e.first = "Required";
    if (!form.last.trim()) e.last = "Required";
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) e.email = "Enter a valid email";
    if (!form.phone.trim()) e.phone = "Required";
    if (totalTix < 1) e.qty = "Add at least one ticket";
    setErrs(e);
    if (Object.keys(e).length) return;
    onNext();
  };

  return (
    <React.Fragment>
    <div className="ffx-card">
      <Stepper step={0} comp={comp} />

      {comp && claimInfo && !claimInfo.error && (
        <div className="ffx-vip">
          <div className="ffx-vip-tag"><I.star width="13" height="13" /> VIP &amp; donor invitation</div>
          <div className="ffx-vip-h">You&rsquo;ve been shouted {claimInfo.max_qty} free ticket{claimInfo.max_qty === 1 ? "" : "s"}</div>
          <p className="ffx-vip-p">A thank-you from Farmers Fightback. Claim below &mdash; no payment needed.</p>
          <div className="ffx-vip-tok">Token <b>{claimInfo.token}</b> &middot; {compMax} of {claimInfo.max_qty} remaining</div>
        </div>
      )}
      {comp && claimInfo && claimInfo.error && (
        <div className="ffx-vip ffx-vip-err">
          <div className="ffx-vip-tag"><I.warn width="13" height="13" /> Token issue</div>
          <div className="ffx-vip-h">{claimInfo.error}</div>
          <p className="ffx-vip-p">Check the link in your email, or <a href="mailto:support@farmersfightback.com" style={{color:"var(--barn)",textDecoration:"underline"}}>get in touch</a> and we'll sort it.</p>
        </div>
      )}

      <div className="ffx-sec-h">{comp ? "Claim your tickets" : "Choose your tickets"}</div>

      <div className="ffx-tickets">
        {comp ? (
          <div className="ffx-trow">
            <div className="ffx-trow-i"><I.ticket width="22" height="22" className="ffx-trow-ic" />
              <div><div className="ffx-trow-n">Free entry &mdash; comped</div><div className="ffx-trow-p">{money(0)} &middot; capped at {claimInfo ? claimInfo.max_qty : 0}</div></div>
            </div>
            <Qty value={qty.comp} min={1} max={compMax} onChange={(v) => setQty({ ...qty, comp: v })} />
          </div>
        ) : (
          <div className="ffx-trow">
            <div className="ffx-trow-i"><I.ticket width="22" height="22" className="ffx-trow-ic" />
              <div>
                <div className="ffx-trow-n">Ticket <span className="ffx-trow-badge">Meal included</span></div>
                <div className="ffx-trow-note">Under 18s must be accompanied by an adult.</div>
                <div className="ffx-trow-p">{money(ADULT_PRICE)} each &middot; drinks available for purchase</div>
              </div>
            </div>
            <Qty value={qty.adults} onChange={(v) => setQty({ ...qty, adults: v })} />
          </div>
        )}
      </div>
      {errs.qty && <div className="ffx-err-line">{errs.qty}</div>}

      <div className="ffx-sec-h">Your details</div>
      <div className="ffx-fields">
        <Field label="First name" value={form.first} onChange={set("first")} err={errs.first} placeholder="Jane" />
        <Field label="Last name" value={form.last} onChange={set("last")} err={errs.last} placeholder="Farmer" />
        <Field label="Email" type="email" value={form.email} onChange={set("email")} err={errs.email} placeholder="jane@example.com" />
        <Field label="Phone" value={form.phone} onChange={set("phone")} err={errs.phone} placeholder="0400 000 000" />
        <Field label="Postcode" opt value={form.postcode} onChange={set("postcode")} placeholder="3387" />
      </div>

      <div className="ffx-cta-row">
        <div className="ffx-total">
          <span className="ffx-total-l">{comp ? "Total" : totalTix + " ticket" + (totalTix === 1 ? "" : "s")}</span>
          <span className="ffx-total-v">{money(total)}{comp && <em> · free</em>}</span>
        </div>
        <button className="ffx-btn ffx-btn-lg" onClick={submit} disabled={submitting || (comp && (!claimInfo || claimInfo.error))}>
          {submitting ? "Please wait…" : (comp ? "Confirm my free tickets" : "Continue to payment")}
        </button>
      </div>
      {submitError && <div className="ffx-err-line">{submitError}</div>}
      {comp && <p className="ffx-fine">$0 order &mdash; you&rsquo;ll skip payment and go straight to your tickets.</p>}
    </div>
    <a className="ffx-cantmake" href="/cantmakeit">
      Can&rsquo;t make it? <span className="ffx-cantmake-arw" aria-hidden="true">&rsaquo;</span>
    </a>
    <LineupSection />
    </React.Fragment>
  );
}

/* ============================================================
   STEP 2 — CHECKOUT (paid only) — Stripe Embedded Checkout
   Mounts Stripe's card-payment iframe inline. No redirect out until
   Stripe redirects to return_url on success.
   ============================================================ */
function CheckoutStep({ qty, form, ref_code, onBack, onTerms }) {
  const mountRef = useRef(null);
  const [state, setState] = useState("mounting"); // mounting | mounted | error
  const [errorMsg, setErrorMsg] = useState("");
  const total = qty.adults * ADULT_PRICE;
  const lines = [
    qty.adults > 0 && { n: "Ticket", q: qty.adults, u: ADULT_PRICE },
  ].filter(Boolean);

  useEffect(() => {
    let cancelled = false;
    let checkoutInstance = null;

    (async () => {
      try {
        // 1. Mint a fresh Embedded Checkout Session (server-side).
        const r = await fetch("/api/rally-checkout", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            adult_qty: qty.adults,
            first_name: form.first,
            last_name: form.last,
            email: form.email,
            phone: form.phone,
            postcode: form.postcode,
            ref: ref_code,
          }),
        });
        const data = await r.json().catch(() => ({}));
        if (!r.ok || !data.client_secret) {
          throw new Error(data.error || "Couldn't start checkout. Please try again.");
        }
        if (!window.Stripe) {
          throw new Error("Stripe.js failed to load. Refresh the page.");
        }
        if (!data.publishable_key) {
          throw new Error("Payments aren't fully configured yet. Please try again shortly.");
        }
        if (cancelled) return;

        // 2. Mount Stripe's embedded UI. It handles card entry, error
        //    states, and redirects to return_url on success — we get
        //    session_id back in the URL and render the confirmation.
        const stripe = window.Stripe(data.publishable_key);
        checkoutInstance = await stripe.initEmbeddedCheckout({
          clientSecret: data.client_secret,
        });
        if (cancelled) { checkoutInstance.destroy(); return; }
        checkoutInstance.mount(mountRef.current);
        setState("mounted");
      } catch (e) {
        if (cancelled) return;
        setState("error");
        setErrorMsg(String(e.message || e));
      }
    })();

    return () => {
      cancelled = true;
      if (checkoutInstance) {
        try { checkoutInstance.destroy(); } catch (e) {}
      }
    };
    // Empty deps — one session per mount. If the user hits Back and then
    // Continue again, CheckoutStep unmounts + remounts, and we mint a new
    // session with the latest details.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div className="ffx-card">
      <Stepper step={1} comp={false} />
      <div className="ffx-checkout-head">
        <span className="ffx-lockpill"><I.lock width="15" height="15" /> Secure checkout</span>
        <div className="ffx-sec-h" style={{ margin: "14px 0 0" }}>Confirm &amp; pay</div>
        <p className="ffx-fine" style={{ marginTop: 4 }}>Pay securely with card. Your details go straight to Stripe &mdash; we never see or store them. GST included where applicable.</p>
      </div>

      <div className="ffx-summary">
        {lines.map((l) => (
          <div className="ffx-sum-row" key={l.n}><span>{l.q} &times; {l.n}</span><span>{money(l.u * l.q)}</span></div>
        ))}
        <div className="ffx-sum-row ffx-sum-total"><span>Total</span><span>{money(total)}</span></div>
      </div>

      {/* Stripe's Embedded Checkout requires its mount target to contain no
          child nodes when mount() is called. Keep the ref'd div strictly
          empty and show the loader as a sibling so mount() never rejects. */}
      {state === "mounting" && (
        <div className="ffx-stripe-loader">
          <div className="ffx-spinner" />
          <div className="ffx-proc-h">Loading secure payment&hellip;</div>
        </div>
      )}
      {state !== "error" && (
        <div ref={mountRef} className="ffx-stripe-mount" />
      )}
      {state === "error" && (
        <div style={{margin: "16px 0"}}>
          <div className="ffx-err-line">{errorMsg}</div>
          <button className="ffx-btn ffx-btn-lg" onClick={() => window.location.reload()} style={{ marginTop: 12 }}>Try again</button>
        </div>
      )}
      <p className="ffx-agree">Tickets are non-transferable and non-refundable. By purchasing tickets you agree to the <a href="#terms" onClick={(e) => { e.preventDefault(); onTerms && onTerms(); }}>Terms and Conditions</a>.</p>
      <button className="ffx-link" onClick={onBack}>&larr; Back to details</button>
    </div>
  );
}

/* ============================================================
   SHARE block
   ============================================================ */
function ShareBlock({ myToken }) {
  const url = `https://www.farmersfightback.com/fundraiser${myToken ? `?ref=${myToken}` : ""}`;
  const text = "I'm going to the Farmers Fightback Fundraiser — Sat 29 Aug, Marnoo. Come stand with us:";
  const [copied, setCopied] = useState(false);
  const copy = () => {
    try { navigator.clipboard.writeText(url); } catch (e) {}
    setCopied(true); setTimeout(() => setCopied(false), 1800);
  };
  const enc = encodeURIComponent;
  const chans = [
    { k: "fb", label: "Facebook", Ic: I.fb, href: `https://www.facebook.com/sharer/sharer.php?u=${enc(url)}`, c: "#1877F2" },
    { k: "wa", label: "WhatsApp", Ic: I.wa, href: `https://wa.me/?text=${enc(text + " " + url)}`, c: "#25D366" },
    { k: "mail", label: "Email", Ic: I.mail, href: `mailto:?subject=${enc("Come to the Farmers Fightback Fundraiser")}&body=${enc(text + "\n\n" + url)}`, c: "#AE3528" },
    { k: "sms", label: "Text", Ic: I.sms, href: `sms:?&body=${enc(text + " " + url)}`, c: "#1F6B3B" },
  ];
  return (
    <div className="ffx-block">
      <div className="ffx-block-h"><span className="ffx-block-eb"><I.star width="12" height="12" /> Bring your people</span>
        <h3>The bigger the crowd, the louder we are</h3>
        <p>Make it a moment with your friends &amp; family.</p>
      </div>
      <div className="ffx-linkrow">
        <input className="ffx-input ffx-linkinput" readOnly value={url} onFocus={(e) => e.target.select()} />
        <button className={"ffx-copy" + (copied ? " ok" : "")} onClick={copy}>{copied ? <React.Fragment><I.check width="15" height="15" /> Copied</React.Fragment> : <React.Fragment><I.copy width="15" height="15" /> Copy</React.Fragment>}</button>
      </div>
      <div className="ffx-share-grid">
        {chans.map(({ k, label, Ic, href, c }) => (
          <a key={k} className="ffx-share-btn" href={href} target="_blank" rel="noreferrer" style={{ "--ch": c }}>
            <span className="ffx-share-ic"><Ic width="18" height="18" /></span>{label}
          </a>
        ))}
      </div>
    </div>
  );
}

/* ============================================================
   DONATION block — each amount is a REAL anchor pointing straight at
   the exact same Stripe payment page the /donate page uses. The /donate
   page reads content/site.json -> donorPage.amounts[].url (+ otherUrl),
   so those are the authoritative links, mirrored below.

   The URLs are hardcoded as a fallback so the buttons ALWAYS go direct
   to Stripe (never to /donate), and also re-synced from site.json on
   mount so they can't drift if the /donate links are ever changed.
   ============================================================ */

// Authoritative Stripe links, copied from content/site.json donorPage.
// Keep in step with /donate; the mount-time fetch below re-syncs anyway.
const RALLY_DONATE_URLS = {
  35: "https://buy.stripe.com/14AbJ0eNg0in96H2tqbV60Q",
  65: "https://buy.stripe.com/28EdR85cG3uzaaL2tqbV60R",
  265: "https://buy.stripe.com/5kQeVcfRkghlfv5fgcbV60T",
  550: "https://buy.stripe.com/7sY5kCgVo7KP0AbgkgbV60U",
  1500: "https://buy.stripe.com/7sY4gydJcaX1dmX1pmbV60V",
  other: "https://donate.stripe.com/14A6oG8oS4yDciT5FCbV60X",
};

// `heading` overrides the headline. /cantmakeit passes its own because
// "invite your mates" is a ticket-funnel ask, and the people on that page
// have just told us they aren't coming.
function DonationBlock({ heading }) {
  const AMTS = [35, 65, 265, 550, 1500];
  const [urls, setUrls] = useState(RALLY_DONATE_URLS);

  // Re-sync from the same key /donate reads, so if those links change the
  // rally page follows. Purely an override of the hardcoded defaults —
  // never clears them, so the anchors are always live Stripe links.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/content/site.json", { cache: "no-store" });
        const data = await r.json();
        const dp = (data && data.donorPage) || {};
        const next = { ...RALLY_DONATE_URLS };
        (dp.amounts || []).forEach((a) => {
          if (a && a.amount && a.url) next[a.amount] = a.url;
        });
        if (dp.otherUrl) next.other = dp.otherUrl;
        if (!cancelled) setUrls(next);
      } catch (e) { /* keep hardcoded defaults */ }
    })();
    return () => { cancelled = true; };
  }, []);

  return (
    <div className="ffx-block ffx-block-green">
      <div className="ffx-block-h">
        <span className="ffx-block-eb light">Chip in as well?</span>
        <h3 className="light">{heading || <React.Fragment>Invite your mates &mdash; <span className="ffx-scriptgold">and</span> chip in</React.Fragment>}</h3>
        <p className="light">Tickets get you there; donations keep the campaign on the road. Both help. Skip if you&rsquo;d rather not.</p>
      </div>
      <div className="ffx-don-grid">
        {AMTS.map((a) => (
          <a key={a} className="ffx-don-opt" href={urls[a]} target="_top" rel="noopener">{money(a)}</a>
        ))}
        <a className="ffx-don-opt ffx-don-other" href={urls.other} target="_top" rel="noopener">Other</a>
      </div>
      <span className="ffx-fine light" style={{ display: "block", marginTop: 14 }}>One-off &middot; secured by Stripe</span>
    </div>
  );
}

/* ============================================================
   CONFIRMATION
   ============================================================ */
function ConfirmStep({ comp, qty, form, orderRef, myToken }) {
  const totalTix = comp ? qty.comp : qty.adults;
  const total = comp ? 0 : qty.adults * ADULT_PRICE;
  const name = (form.first || "Friend") + (form.last ? " " + form.last : "");

  useEffect(() => {
    // Fire a client-side Meta Pixel Purchase event too so the browser side
    // matches what stripe-webhook.js fires server-side via CAPI. Value stays
    // 0 for comp tickets — Meta accepts that.
    if (window.fbq) {
      try {
        window.fbq("track", comp ? "Lead" : "Purchase", {
          content_name: comp ? "Fundraiser Ticket (comp)" : "Fundraiser Ticket",
          value: total,
          currency: "AUD",
          num_items: totalTix,
        });
      } catch (e) {}
    }
  }, []);

  return (
    <div className="ffx-card ffx-confirm">
      <Stepper step={comp ? 1 : 2} comp={comp} allDone />
      <div className="ffx-success">
        <span className="ffx-success-badge"><I.check width="34" height="34" /></span>
        <div className="ffx-success-script">You&rsquo;re in!</div>
        <h2 className="ffx-success-h">{totalTix} ticket{totalTix === 1 ? "" : "s"} booked</h2>
        {orderRef && (
          <p className="ffx-order-code">Confirmation code <b>{orderRef}</b></p>
        )}
        <p className="ffx-fine">Thanks &mdash; we&rsquo;ve got your booking.</p>
      </div>

      <p className="ffx-ticket-note">Your ticket will be sent to you closer to the event day as confirmation.</p>

      <ShareBlock myToken={myToken} />
      <DonationBlock />

      <div className="ffx-stub">
        <div className="ffx-stub-main">
          <div className="ffx-stub-eb"><I.star width="12" height="12" /> Farmers Fightback Fundraiser</div>
          <div className="ffx-stub-date">{EVENT.date}</div>
          <div className="ffx-stub-rows">
            <div><span>Name</span><b>{name}</b></div>
            <div><span>{comp ? "Type" : "Paid"}</span><b>{comp ? "Comped · " + money(0) : money(total)}</b></div>
          </div>
        </div>
        <div className="ffx-stub-tear">
          <div className="ffx-stub-ref"><span>Order</span><b>{orderRef}</b></div>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   FUNNEL ROOT
   ============================================================ */
function RallyFunnel() {
  const claimToken = readClaim();
  const sessionId = readSessionId();
  const inboundRef = readRef();

  const comp = !!claimToken && !sessionId;
  const returningFromStripe = !!sessionId;

  const [step, setStep] = useState(returningFromStripe ? "confirm" : "details");
  const [showTerms, setShowTerms] = useState(false);
  // null = still checking; {closed, message} once the server answers. The
  // server enforces the close regardless — this just spares people a form
  // that would only refuse them at the last step.
  const [sales, setSales] = useState(null);
  const [qty, setQty] = useState({ adults: 1, comp: 1 });
  const [form, setForm] = useState({ first: "", last: "", email: "", phone: "", postcode: "" });
  const [claimInfo, setClaimInfo] = useState(null);
  const [claimSubmitting, setClaimSubmitting] = useState(false);
  const [claimError, setClaimError] = useState("");

  const orderRef = useRef("FFR-" + Math.random().toString(36).slice(2, 6).toUpperCase() + "-" + Math.floor(100 + Math.random() * 899)).current;
  const [myReferralCode, setMyReferralCode] = useState("");

  /* Are ticket sales still open? Skipped for someone back from Stripe —
     their purchase is already made and the confirmation must still render.
     If the check itself fails, the form stays up: the server would refuse a
     closed sale anyway, and a network blip must not hide a live checkout. */
  useEffect(() => {
    if (returningFromStripe) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/rally-checkout?state=1");
        const data = await r.json().catch(() => ({}));
        if (!cancelled && data && typeof data.closed === "boolean") setSales(data);
      } catch (e) {}
    })();
    return () => { cancelled = true; };
  }, [returningFromStripe]);

  /* Look up the comp token on landing when claim= is present. */
  useEffect(() => {
    if (!comp) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/rally-claim?token=" + encodeURIComponent(claimToken));
        const data = await r.json().catch(() => ({}));
        if (cancelled) return;
        if (!r.ok || !data.valid) {
          setClaimInfo({ token: claimToken, error: data.error || "This claim link isn't valid." });
        } else {
          setClaimInfo({ token: claimToken, max_qty: data.max_qty, used_qty: data.used_qty });
          setQty((q) => ({ ...q, comp: Math.min(data.max_qty - data.used_qty, q.comp || 1) || 1 }));
        }
      } catch (e) {
        setClaimInfo({ token: claimToken, error: "Couldn't verify your claim link. Try again in a moment." });
      }
    })();
    return () => { cancelled = true; };
  }, [claimToken, comp]);

  /* On returning from Stripe with ?session_id=, pull the session summary
     so we can render a real confirmation. Server also does the Airtable
     write via webhook independently. */
  useEffect(() => {
    if (!returningFromStripe) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/rally-checkout?session_id=" + encodeURIComponent(sessionId));
        const data = await r.json().catch(() => ({}));
        if (cancelled) return;
        if (r.ok && data.session) {
          const s = data.session;
          setForm((f) => ({
            first: s.first_name || f.first,
            last: s.last_name || f.last,
            email: s.email || f.email,
            phone: s.phone || f.phone,
            postcode: s.postcode || f.postcode,
          }));
          // Orders placed while the event had an under-18 ticket type still
          // carry kid_qty; fold it into the count so their confirmation page
          // shows the right number of tickets.
          setQty((q) => ({ ...q, adults: (s.adult_qty || 0) + (s.kid_qty || 0) }));
          if (s.referral_code) setMyReferralCode(s.referral_code);
        }
      } catch (e) {}
    })();
    return () => { cancelled = true; };
  }, [returningFromStripe, sessionId]);

  /* Comp submit — post to /api/rally-claim, then jump to confirm. */
  const submitComp = async () => {
    setClaimSubmitting(true); setClaimError("");
    try {
      const r = await fetch("/api/rally-claim", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          token: claimToken,
          qty: qty.comp,
          first_name: form.first,
          last_name: form.last,
          email: form.email,
          phone: form.phone,
          postcode: form.postcode,
          ref: inboundRef,
        }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok || !data.ok) {
        throw new Error(data.error || "Couldn't confirm your tickets. Please try again.");
      }
      if (data.referral_code) setMyReferralCode(data.referral_code);
      setStep("confirm");
    } catch (e) {
      setClaimError(String(e.message || e));
    } finally {
      setClaimSubmitting(false);
    }
  };

  const goPay = () => setStep("checkout");

  let body;
  if (step === "confirm") {
    body = <ConfirmStep comp={comp} qty={qty} form={form} orderRef={orderRef} myToken={myReferralCode} />;
  } else if (sales && sales.closed && !comp) {
    // Sales are over. Comp claims still resolve (those seats are already
    // set aside); everyone else gets the apology rather than a dead form.
    body = (
      <div className="ffx-card">
        <div className="ffx-closed">
          <h2 className="ffx-closed-h">Ticket sales have now closed</h2>
          <p className="ffx-closed-p">
            We're sorry to miss you — the response has been bigger than we ever hoped, and the
            night is now at capacity. Thank you to every single person who grabbed a ticket.
          </p>
          <p className="ffx-closed-p">
            Already got yours? We'll see you there — {EVENT.gates.toLowerCase()},{" "}
            {EVENT.date}, {EVENT.venue}.
          </p>
          <p className="ffx-closed-p">
            You can still stand with the farmers this fight is for:{" "}
            <a className="ffx-closed-link" href="/every">chip in to the campaign here</a>.
          </p>
        </div>
      </div>
    );
  } else if (step === "checkout" && !comp) {
    body = <CheckoutStep qty={qty} form={form} ref_code={inboundRef} onBack={() => setStep("details")} onTerms={() => setShowTerms(true)} />;
  } else {
    body = (
      <DetailsStep
        comp={comp}
        claimInfo={claimInfo}
        qty={qty}
        setQty={setQty}
        form={form}
        setForm={setForm}
        onNext={comp ? submitComp : goPay}
        submitting={claimSubmitting}
        submitError={claimError}
      />
    );
  }

  return (
    <div className="ffx-app">
      <Masthead />
      <div className="ffx-wrap">{body}<GettingThere /></div>
      <footer className="ffx-foot">
        <div><span className="ffx-foot-l">Enquiries</span> support@farmersfightback.com</div>
        <button type="button" className="ffx-foot-terms" onClick={() => setShowTerms(true)}>Terms &amp; Conditions</button>
        <div className="ffx-foot-auth">Authorised by Ben Duxson, Farmers Fightback, Marnoo VIC.</div>
      </footer>
      {showTerms && <TermsModal onClose={() => setShowTerms(false)} />}
    </div>
  );
}

/* ============================================================
   CAN'T MAKE IT  (/cantmakeit)
   Served by a rewrite onto this same page shell, so it inherits the
   masthead, styles and footer rather than duplicating them.
   Details go to /api/capture, which normalises the mobile, marks the row
   complete once first+last+email are present, and pushes the supporter to
   Campaign Nucleus — which is what "we'll include you next time" means in
   practice. The donation ladder sits above the form (the ask people who
   can't attend can still act on) and is the shared DonationBlock, so the
   amounts stay in step with /donate.
   ============================================================ */
function CantMakeIt() {
  const [form, setForm] = useState({ first: "", last: "", email: "", phone: "", honeypot: "" });
  const [errs, setErrs] = useState({});
  const [status, setStatus] = useState("idle"); // idle | saving | done | error
  const [showTerms, setShowTerms] = useState(false);
  const sessionId = useRef(null);
  if (!sessionId.current) {
    sessionId.current = (window.crypto && crypto.randomUUID)
      ? crypto.randomUUID()
      : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
  }

  const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));

  const submit = async () => {
    const e = {};
    if (!form.first.trim()) e.first = "Required";
    if (!form.last.trim()) e.last = "Required";
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) e.email = "Enter a valid email";
    if (!form.phone.trim()) e.phone = "Required";
    setErrs(e);
    if (Object.keys(e).length) return;

    setStatus("saving");
    try {
      const r = await fetch("/api/capture", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          session_id: sessionId.current,
          first_name: form.first.trim(),
          last_name: form.last.trim(),
          email: form.email.trim(),
          mobile: form.phone.trim(),
          consent: true,
          honeypot: form.honeypot || "",
          utm_campaign: "fundraiser-cant-make-it",
        }),
      });
      if (!r.ok) throw new Error("capture failed");
      setStatus("done");
    } catch (err) {
      setStatus("error");
    }
  };

  return (
    <div className="ffx-app">
      <Masthead hideBand />
      <div className="ffx-wrap">
        <DonationBlock heading="Chip in" />
        <div className="ffx-card ffx-cmi-card">
          {status === "done" ? (
            <div className="ffx-success">
              <span className="ffx-success-badge"><I.check width="34" height="34" /></span>
              <div className="ffx-success-script">Thanks!</div>
              <h2 className="ffx-success-h">We&rsquo;ll keep you posted</h2>
              <p className="ffx-fine">You&rsquo;re on the list for the next one &mdash; we&rsquo;ll be in touch before it&rsquo;s announced.</p>
            </div>
          ) : (
            <React.Fragment>
              <div className="ffx-sec-h">Can&rsquo;t make it?</div>
              <p className="ffx-fine">No worries &mdash; leave your details and we&rsquo;ll make sure you hear about the next one first.</p>
              <div className="ffx-fields" style={{ marginTop: 16 }}>
                <Field label="First name" value={form.first} onChange={set("first")} err={errs.first} placeholder="Jane" />
                <Field label="Last name" value={form.last} onChange={set("last")} err={errs.last} placeholder="Farmer" />
                <Field label="Email" type="email" value={form.email} onChange={set("email")} err={errs.email} placeholder="jane@example.com" />
                <Field label="Mobile" type="tel" value={form.phone} onChange={set("phone")} err={errs.phone} placeholder="0400 000 000" />
              </div>
              {/* Honeypot — hidden from people, filled by bots; /api/capture drops those. */}
              <div aria-hidden="true" style={{ position: "absolute", left: "-9999px", height: 0, overflow: "hidden" }}>
                <label>Leave this empty<input tabIndex={-1} autoComplete="off" value={form.honeypot} onChange={set("honeypot")} /></label>
              </div>
              <div className="ffx-cta-row">
                <button className="ffx-btn ffx-btn-lg" onClick={submit} disabled={status === "saving"}>
                  {status === "saving" ? "Saving…" : "Keep me posted"}
                </button>
              </div>
              {status === "error" && <div className="ffx-err-line">That didn&rsquo;t save. Please try again.</div>}
            </React.Fragment>
          )}
        </div>
      </div>
      <footer className="ffx-foot">
        <div><span className="ffx-foot-l">Enquiries</span> support@farmersfightback.com</div>
        <button type="button" className="ffx-foot-terms" onClick={() => setShowTerms(true)}>Terms &amp; Conditions</button>
        <div className="ffx-foot-auth">Authorised by Ben Duxson, Farmers Fightback, Marnoo VIC.</div>
      </footer>
      {showTerms && <TermsModal onClose={() => setShowTerms(false)} />}
    </div>
  );
}

/* /cantmakeit is a rewrite onto this same shell, so the path decides which
   app renders. */
const IS_CANT_MAKE_IT = /^\/cantmakeit\/?$/i.test(window.location.pathname);

ReactDOM.createRoot(document.getElementById("root")).render(
  IS_CANT_MAKE_IT ? <CantMakeIt /> : <RallyFunnel />
);
