// site/rsvp.jsx — RSVP avec fuzzy match sur la liste d'invités, formulaire
// complet (présences, enfants, allergies, anecdote, playlist, navette,
// covoiturage), envoi au backend Google Sheet via submitRsvpToBackend()
// + fallback localStorage si le backend est inaccessible.
//
// La liste d'invités pour le fuzzy match vient du Sheet (via useGuestList),
// avec fallback DATA.invites si la connexion échoue.

// Clé localStorage : historique brut des soumissions (utile en debug /
// fallback de l'AdminPanel local).
// Normalise une valeur de régime lue dans le Sheet vers une des options du
// formulaire. Sans ça, une valeur libre type "sans gluten" (avec espace) ou
// "Omnivore" (capitalisée) ne matche aucune <option value=…> et le <select>
// affiche silencieusement la première option tout en gardant la mauvaise
// valeur en state ⇒ submit renvoie la valeur d'origine, RSVP qui semble
// "ne pas s'enregistrer". Cette fonction est ceinture-bretelle.
function normalizeRegime(raw) {
  const norm = String(raw || "").toLowerCase()
    .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z]/g, "");
  if (!norm) return "omni";
  if (norm === "omni" || norm === "omnivore" || norm === "tout") return "omni";
  if (norm === "vegan" || norm === "vegane")                     return "vegan";
  if (norm.startsWith("veg"))                                    return "vege";
  if (norm.includes("gluten"))                                   return "sansgluten";
  if (norm.startsWith("pesc"))                                   return "pesce";
  return "autre";
}

const RSVP_KEY = "ea-mariage:rsvp-responses";

// ── Bridge RSVP → App invité ────────────────────────────────────────────
// Quand un invité envoie son RSVP, on écrit son identité dans le même
// localStorage que l'app invité (clé "ea-app:identity") avec TTL 30 j.
// Conséquence : quand il clique sur "Ouvrir l'app invité" après avoir
// répondu, il est déjà identifié — pas besoin de retaper son nom.
const APP_IDENTITY_KEY = "ea-app:identity";
function writeAppIdentityFromInvite(invite) {
  if (!invite?.name) return;
  const parts = invite.name.trim().split(/\s+/).filter(Boolean);
  const firstName = invite.prenom || parts[0] || "";
  const lastName  = invite.nom    || (parts.length > 1 ? parts.slice(1).join(" ") : "");
  const identity = {
    name: invite.name,
    firstName, lastName,
    source: "match",
    inviteId:   invite.id || null,
    tableId:    invite.table_id || invite.tableId || null,
    tableName:  invite.tableName || null,
    giteId:     invite.gite_id  || invite.giteId  || null,
    giteName:   invite.giteName  || null,
    rsvpStatut: invite.rsvp_statut || null,
  };
  try {
    localStorage.setItem(APP_IDENTITY_KEY, JSON.stringify({ identity, at: Date.now() }));
  } catch {}
}

// Fuzzy match très simple : tokenize, partial match insensible à la casse
// + accents, score basé sur le nombre de tokens matchés.
function normalize(s) {
  return s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, "");
}
function fuzzyScore(query, candidate) {
  const q = normalize(query).trim();
  if (!q) return 0;
  const c = normalize(candidate);
  const qTokens = q.split(/\s+/).filter(Boolean);
  const cTokens = c.split(/\s+/);
  let score = 0;
  for (const qt of qTokens) {
    for (const ct of cTokens) {
      if (ct.startsWith(qt)) { score += qt.length / ct.length + 0.5; }
      else if (ct.includes(qt)) { score += qt.length / ct.length * 0.5; }
    }
  }
  return score;
}
function searchInvites(query, invites, limit = 5) {
  if (!query || query.length < 2) return [];
  return invites
    .map((inv) => ({ ...inv, score: fuzzyScore(query, inv.name || "") }))
    .filter((x) => x.score > 0.3)
    .sort((a, b) => b.score - a.score)
    .slice(0, limit);
}

function RsvpSection() {
  return (
    <Section id="section-rsvp" bg={T.ink} paddingTop={100} paddingBottom={100} style={{ color: T.white }}>
      <SectionHeaderDark
        eyebrow="Répondre à l'invitation"
        title="Êtes-vous des nôtres ?"
        subtitle="Réponse souhaitée avant le 1ᵉʳ juin 2027. Pas d'inquiétude si vous n'êtes pas sûr — vous pouvez nous le redire plus tard."
      />
      <RsvpForm />
    </Section>
  );
}

// Variante du SectionHeader pour fond sombre
function SectionHeaderDark({ eyebrow, title, subtitle }) {
  const [ref, seen] = useInView();
  const item = (delay) => ({
    opacity: seen ? 1 : 0,
    transform: seen ? "translateY(0)" : "translateY(16px)",
    transition: `opacity .55s cubic-bezier(.2,.7,.3,1) ${delay}s, transform .55s cubic-bezier(.2,.7,.3,1) ${delay}s`,
  });
  return (
    <div ref={ref} style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 36 }}>
      <span style={{
        fontFamily: T.mono, fontSize: 11, letterSpacing: 4, textTransform: "uppercase", color: T.or,
        ...item(0),
      }}>
        {eyebrow}
      </span>
      <div style={{ position: "relative", display: "inline-block", maxWidth: "100%", ...item(0.1) }}>
        <div className="display-bold" style={{ fontSize: "clamp(38px, 6vw, 64px)", color: T.white }}>
          {title}
        </div>
        <div style={{
          position: "absolute", bottom: -12, left: 0, right: 0,
          height: 2, background: T.or,
          transformOrigin: "left",
          transform: seen ? "scaleX(1)" : "scaleX(0)",
          transition: "transform 1.2s cubic-bezier(.4,0,.2,1) .35s",
        }} />
      </div>
      {subtitle && (
        <div style={{
          fontFamily: T.italic, fontStyle: "italic", fontSize: "clamp(16px, 2vw, 19px)",
          color: "rgba(250,250,247,0.7)", lineHeight: 1.5, maxWidth: 540, marginTop: 14,
          ...item(0.2),
        }}>
          {subtitle}
        </div>
      )}
    </div>
  );
}

function RsvpForm() {
  const { invites: guestList, loading: guestsLoading, source: guestsSource } = useGuestList();
  const [step, setStep] = React.useState(0); // 0 search · 1 form · 2 thanks
  const [chosen, setChosen] = React.useState(null); // invité complet : { id, name, plus_one_max, enfant_max, rsvp_statut, rsvp_via, … }
  const [query, setQuery] = React.useState("");
  const matches = React.useMemo(() => searchInvites(query, guestList), [query, guestList]);

  // Form state
  const [form, setForm] = React.useState(makeInitialForm());
  const setF = (k, v) => setForm((f) => ({ ...f, [k]: v }));

  // État d'envoi : null · "sending" · "ok" · "queued" · "error"
  const [sendState, setSendState] = React.useState(null);
  const [sendError, setSendError] = React.useState("");

  // Verrou : si l'invité choisi a déjà été inscrit par quelqu'un d'autre
  const [verrouAck, setVerrouAck] = React.useState(false);
  const verrouParent = React.useMemo(() => {
    if (!chosen?.rsvp_via || chosen.rsvp_via === chosen.id) return null;
    return guestList.find(g => g.id === chosen.rsvp_via) || null;
  }, [chosen, guestList]);

  // Autofill : si l'invité a déjà répondu, on récupère sa RSVP et on pré-remplit
  const [alreadyAnswered, setAlreadyAnswered] = React.useState(null); // { rsvp_at, presents, … } ou null
  const [autofillAck, setAutofillAck] = React.useState(false);
  React.useEffect(() => {
    setAutofillAck(false);
    setAlreadyAnswered(null);
    if (!chosen?.id) return;
    // Répondu = soit oui/non avec rsvp_at, soit on a un rsvp_via (inscrit par un autre)
    const hasResponse = chosen.rsvp_statut === "oui" || chosen.rsvp_statut === "non";
    if (!hasResponse) return;
    // Récupère la RSVP complète en async
    (async () => {
      const data = await fetchMyRsvp(chosen.id);
      if (!data?.ok || !data?.invite) return;
      setAlreadyAnswered(data);
    })();
  }, [chosen]);

  // Quand l'utilisateur clique "Oui, modifier" sur le banner autofill, on pré-remplit le form
  const applyAutofill = () => {
    if (!alreadyAnswered?.invite) { setAutofillAck(true); return; }
    const inv = alreadyAnswered.invite;
    const presents = (inv.presents || "").split(",").filter(Boolean);
    setForm(prev => ({
      ...prev,
      presents: {
        mairie:     presents.includes("mairie"),
        ceremonie:  presents.includes("ceremonie"),
        vinHonneur: presents.includes("vinHonneur"),
        repas:      presents.includes("repas"),
        soiree:     presents.includes("soiree"),
      },
      plusOneId:       inv.partner_id || null,
      plusOneName:     inv.partner_id ? (guestList.find(g => g.id === inv.partner_id)?.name || "") : "",
      plusOneFreeText: !inv.partner_id ? (inv.plus_one_nom || "") : "",
      plusOneFreeMode: !inv.partner_id && !!inv.plus_one_nom,
      plusOneSeparate: false,
      children: (alreadyAnswered.children || []).map(c => ({ prenom: c.prenom || "", age: c.age || "" })),
      regime:       normalizeRegime(inv.regime),
      allergies:    inv.allergies || "",
      song:         inv.chanson || "",
      hebergement:  inv.hebergement_choix || "non",
      covoiturage:  inv.covoit_choix || "rien",
      message:      inv.message_couple || "",
      email:        inv.email || "",
      phone:        inv.telephone || "",
    }));
    setAutofillAck(true);
  };

  // Sync : quand on choisit un invité, on adapte le nombre de slots enfants
  React.useEffect(() => {
    if (!chosen) return;
    setForm(prev => {
      const max = Number(chosen.enfant_max) || 0;
      const next = [...prev.children];
      while (next.length < max) next.push({ prenom: "", age: "" });
      while (next.length > max) next.pop();
      return { ...prev, children: next };
    });
    setVerrouAck(false);
  }, [chosen]);

  const submit = async (e) => {
    e?.preventDefault?.();
    setSendState("sending");
    setSendError("");

    // 1. Historique local complet (au cas où, et pour l'AdminPanel local)
    try {
      const all = JSON.parse(localStorage.getItem(RSVP_KEY) || "[]");
      all.push({ ...form, name: chosen?.name || form.name, invite_id: chosen?.id || null, at: new Date().toISOString() });
      localStorage.setItem(RSVP_KEY, JSON.stringify(all));
    } catch {}

    // 2. Envoi backend si on a un invite_id
    if (chosen?.id) {
      const statut = Object.values(form.presents).some(Boolean) ? "oui" : "non";
      const presentsCsv = Object.entries(form.presents).filter(([_, v]) => v).map(([k]) => k).join(",");
      const childrenList = (form.children || []).filter(c => c?.prenom?.trim()).map(c => ({ prenom: c.prenom.trim(), age: c.age || "" }));
      const enfantsTxt = childrenList.length
        ? childrenList.map(c => c.prenom + (c.age ? ` (${c.age})` : "")).join(", ")
        : "";
      const plusOneFreeText = (!form.plusOneId && form.plusOneFreeText?.trim()) ? form.plusOneFreeText.trim() : "";
      // plus_one_nom : on écrit le nom LISIBLE quoi qu'il arrive (fuzzy ou texte libre).
      // partner_id (via plus_one_id côté backend) sera également rempli si fuzzy.
      const plusOneNom = form.plusOneId ? (form.plusOneName || "") : plusOneFreeText;
      const payload = {
        invite_id: chosen.id,
        statut,
        plus_one_id: form.plusOneId || null,
        plus_one_nom: plusOneNom,
        plus_one_also_rsvp: !form.plusOneSeparate,
        children: childrenList,
        regime: form.regime || "omni",
        allergies: form.allergies || "",
        message_couple: form.message || "",
        email: form.email || "",
        telephone: form.phone || "",
        presents: presentsCsv,
        enfants: enfantsTxt,
        chanson: form.song || "",
        hebergement_choix: form.hebergement || "",
        covoit_choix: form.covoiturage || "",
      };
      const res = await submitRsvpToBackend(payload);
      // Debug : log la réponse complète du backend (visible dans Console)
      console.log("[RSVP] submitRsvpToBackend response:", res, "payload:", payload);
      if (res.ok) setSendState("ok");
      else { setSendState("queued"); setSendError(res.error || ""); }
      // Bridge : passe l'identité à l'app invité (même onglet, même domaine)
      writeAppIdentityFromInvite(chosen);
    } else {
      setSendState("queued");
      setSendError("Invité non trouvé dans la liste — réponse à valider manuellement par les mariés.");
      // Même en mode "saisie libre", on pousse l'identité vers l'app
      // (source: manual côté app, mais le nom est connu — évite de retaper).
      if (form.name?.trim()) {
        writeAppIdentityFromInvite({ name: form.name.trim() });
      }
    }

    setStep(2);
    window.scrollTo({ top: document.getElementById("section-rsvp").offsetTop - 40, behavior: "smooth" });
  };

  if (step === 2) return <RsvpThanks state={sendState} error={sendError} onAgain={() => { setStep(0); setChosen(null); setQuery(""); setSendState(null); setSendError(""); setForm(makeInitialForm()); }} />;

  return (
    <div style={{ background: T.white, color: T.ink, padding: "clamp(28px, 4vw, 48px)", maxWidth: 760, margin: "0 auto" }}>
      {/* Étape 0 : recherche du nom */}
      {step === 0 && (
        <div>
          <div className="eyebrow-small">Étape 1 sur 2</div>
          <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 26, marginTop: 6, marginBottom: 6 }}>
            Comment vous appelez-vous ?
          </div>
          <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 15, color: T.inkSoft, marginBottom: 18 }}>
            Tapez vos premières lettres, on retrouve votre invitation. (Ou tapez à côté si vous n'êtes pas listé : on vous accueille quand même.)
          </div>

          <input type="text" value={query} onChange={(e) => setQuery(e.target.value)}
            placeholder={guestsLoading ? "Chargement de la liste…" : "Prénom et nom…"} autoFocus
            style={inputStyle()} />

          {matches.length > 0 && (
            <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 6 }}>
              {matches.map((m) => (
                <button key={m.id || m.name} type="button" onClick={() => { setChosen({ ...m }); setStep(1); }}
                  style={{
                    textAlign: "left", padding: "12px 16px",
                    background: T.paper, border: `1px solid ${T.ruleSoft}`,
                    fontFamily: T.sans, fontSize: 15, cursor: "pointer",
                    display: "flex", alignItems: "center", justifyContent: "space-between",
                    transition: "background .15s, border-color .15s",
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = T.orPale; e.currentTarget.style.borderColor = T.or; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = T.paper; e.currentTarget.style.borderColor = T.ruleSoft; }}>
                  <span>{m.name}</span>
                  <span style={{ fontFamily: T.mono, fontSize: 10, color: T.or, letterSpacing: 1 }}>♥ C'est moi →</span>
                </button>
              ))}
            </div>
          )}

          {query.length >= 2 && matches.length === 0 && !guestsLoading && (
            <div style={{ marginTop: 14, padding: "14px 16px", background: T.rougePale, border: `1px solid ${T.rouge}`, fontSize: 14, color: T.rouge }}>
              Pas de correspondance dans notre liste. Pas de souci, cliquez ci-dessous pour répondre quand même — on vérifie à la main.
            </div>
          )}

          <div style={{ marginTop: 16, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
            {guestsSource === "fallback" && !guestsLoading && (
              <div style={{ fontFamily: T.mono, fontSize: 10, color: T.inkMute, letterSpacing: 1 }}>
                ⚠ liste hors-ligne (les mariés recevront tout de même)
              </div>
            )}
            <div style={{ marginLeft: "auto" }}>
              <ButtonSecondary onClick={() => { setChosen({ name: query || "(invité sans nom)", id: null }); setStep(1); }}>
                Je ne me trouve pas → continuer
              </ButtonSecondary>
            </div>
          </div>
        </div>
      )}

      {/* Étape 1 : formulaire complet */}
      {step === 1 && (
        <form onSubmit={submit}>
          <div className="eyebrow-small">Étape 2 sur 2 · {chosen?.name}</div>
          <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 26, marginTop: 6, marginBottom: 24 }}>
            Vos réponses
          </div>

          {verrouParent && !verrouAck && (
            <VerrouBanner parentName={verrouParent.name} onAck={() => setVerrouAck(true)} onCancel={() => setStep(0)}/>
          )}

          {(!verrouParent || verrouAck) && alreadyAnswered && !autofillAck && (
            <AlreadyAnsweredBanner
              rsvpAt={alreadyAnswered.invite?.rsvp_at}
              statut={alreadyAnswered.invite?.rsvp_statut}
              onModify={applyAutofill}
              onCancel={() => setStep(0)}
            />
          )}

          {(!verrouParent || verrouAck) && (!alreadyAnswered || autofillAck) && <>

          {/* Présences */}
          <FieldGroup label="J'assiste à" hint="Cochez tout ce à quoi vous serez présent.">
            <CheckboxRow items={[
              { k: "mairie", l: "Mairie · 14h00" },
              { k: "ceremonie", l: "Cérémonie laïque · 17h30" },
              { k: "vinHonneur", l: "Vin d'honneur · 18h30" },
              { k: "repas", l: "Repas · 20h00" },
              { k: "soiree", l: "Soirée · 22h00 →" },
            ]} value={form.presents} onChange={(k, v) => setF("presents", { ...form.presents, [k]: v })} />
          </FieldGroup>

          {/* Accompagnant·e (+1) */}
          <PlusOnePicker chosen={chosen} guestList={guestList} form={form} setF={setF}/>

          {/* Enfants (uniquement si enfant_max > 0) */}
          {Number(chosen?.enfant_max) > 0 && (
            <ChildrenSection chosen={chosen} form={form} setF={setF}/>
          )}

          {/* Allergies + chanson */}
          {/* Régime + allergies */}
          <FieldGroup label="Régime alimentaire" hint="On transmet au traiteur — même info pour le +1 si vous êtes ensemble.">
            <select value={form.regime} onChange={(e) => setF("regime", e.target.value)} style={inputStyle()}>
              <option value="omni">Omnivore</option>
              <option value="vege">Végétarien</option>
              <option value="vegan">Végan</option>
              <option value="sansgluten">Sans gluten</option>
              <option value="pesce">Pescetarien</option>
              <option value="autre">Autre (préciser ci-dessous)</option>
            </select>
          </FieldGroup>

          <FieldGroup label="Allergies & précisions" hint="Allergies, intolérances, précisions sur le régime. Tout est pris en compte.">
            <input type="text" value={form.allergies} onChange={(e) => setF("allergies", e.target.value)} style={inputStyle()} placeholder="ex: allergie fruits à coque, pas de porc…" />
          </FieldGroup>

          <FieldGroup label="Une chanson pour la playlist" hint="Le morceau qui doit ABSOLUMENT passer.">
            <input type="text" value={form.song} onChange={(e) => setF("song", e.target.value)} style={inputStyle()} placeholder="ex: Stromae · Alors on danse" />
          </FieldGroup>

          {/* Hébergement */}
          <FieldGroup label="Avez-vous besoin d'un hébergement ?">
            <RadioRow value={form.hebergement} onChange={(v) => setF("hebergement", v)} items={[
              { v: "non", l: "Non, c'est ok" },
              { v: "oui", l: "Oui, on cherche" },
              { v: "infos", l: "Envoyez-moi la liste" },
            ]} />
          </FieldGroup>

          {/* Covoiturage */}
          <FieldGroup label="Covoiturage">
            <RadioRow value={form.covoiturage} onChange={(v) => setF("covoiturage", v)} items={[
              { v: "rien", l: "Je gère" },
              { v: "propose", l: "Je propose des places" },
              { v: "cherche", l: "J'en cherche" },
            ]} />
          </FieldGroup>

          {/* Message libre */}
          <FieldGroup label="Un mot pour Enora & Antoine ?" hint="Optionnel. On le lit, promis.">
            <textarea value={form.message} onChange={(e) => setF("message", e.target.value)} rows="2" style={{ ...inputStyle(), resize: "vertical", minHeight: 70 }} />
          </FieldGroup>

          {/* Contact */}
          <div style={{ display: "grid", gap: 14, gridTemplateColumns: "1fr 1fr" }} className="rsvp-row-2">
            <FieldGroup label="Email" hint="Pour que l'on puisse vous tenir au courant.">
              <input type="email" value={form.email} onChange={(e) => setF("email", e.target.value)} style={inputStyle()} placeholder="vous@exemple.com" />
            </FieldGroup>
            <FieldGroup label="Téléphone (optionnel)">
              <input type="tel" value={form.phone} onChange={(e) => setF("phone", e.target.value)} style={inputStyle()} placeholder="+33 6 XX XX XX XX" />
            </FieldGroup>
          </div>

          {/* Submit */}
          <div style={{ marginTop: 28, display: "flex", gap: 12, flexWrap: "wrap", justifyContent: "space-between", alignItems: "center" }}>
            <button type="button" onClick={() => setStep(0)} style={{
              background: "transparent", border: "none", color: T.inkSoft, cursor: "pointer",
              fontFamily: T.mono, fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
              textDecoration: "underline", padding: 0,
            }}>← retour</button>
            <ButtonPrimary type="submit" style={{ padding: "16px 32px" }} disabled={sendState === "sending"}>
              {sendState === "sending" ? "· · · envoi en cours" : "♥ Envoyer ma réponse"}
            </ButtonPrimary>
          </div>
          </>}
        </form>
      )}

      <style>{`
        @media (max-width: 540px) {
          .rsvp-row-2 { grid-template-columns: 1fr !important; }
        }
      `}</style>
    </div>
  );
}

// ─── Form helpers ─────────────────────────────────────────────────────

function makeInitialForm() {
  return {
    name: "",
    presents: { mairie: true, ceremonie: true, vinHonneur: true, repas: true, soiree: true },
    plusOneId: null,         // id backend si fuzzy match
    plusOneName: "",         // libellé affiché du +1 fuzzy
    plusOneFreeText: "",     // saisie libre (plus_one_max=1 uniquement)
    plusOneFreeMode: false,  // toggle "ajouter hors liste"
    plusOneSeparate: false,  // toggle "il/elle répondra séparément"
    children: [],            // [{ prenom, age }]
    regime: "omni",
    allergies: "",
    song: "",
    hebergement: "non",
    covoiturage: "rien",
    message: "",
    email: "",
    phone: "",
  };
}

// ─── Bandeau : invité a déjà répondu lui-même ─────────────────────────
function AlreadyAnsweredBanner({ rsvpAt, statut, onModify, onCancel }) {
  const dateStr = rsvpAt ? new Date(rsvpAt).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" }) : "";
  return (
    <div style={{
      marginBottom: 22, padding: "18px 20px",
      background: T.paper, border: `1.5px solid ${T.or}`, borderLeft: `4px solid ${T.or}`,
    }}>
      <div style={{ fontFamily: T.mono, fontSize: 10, letterSpacing: 2, color: T.orDeep, textTransform: "uppercase", marginBottom: 6 }}>
        ✨ déjà répondu
      </div>
      <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 18, color: T.ink, lineHeight: 1.3, marginBottom: 6 }}>
        Vous avez déjà répondu {statut === "non" ? <span style={{ color: T.rouge }}>(refus)</span> : <span style={{ color: T.or }}>(oui)</span>}{dateStr ? <span style={{ fontFamily: T.italic, fontStyle: "italic", fontWeight: 400, fontSize: 14, color: T.inkSoft }}> — le {dateStr}</span> : null}.
      </div>
      <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 14, color: T.inkSoft, lineHeight: 1.5, marginBottom: 14 }}>
        Voulez-vous modifier votre réponse ? Nous pré-remplirons le formulaire avec vos précédents choix.
      </div>
      <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
        <ButtonPrimary type="button" onClick={onModify} style={{ padding: "10px 18px" }}>
          ✎ Oui, modifier
        </ButtonPrimary>
        <ButtonSecondary onClick={onCancel}>Non, retour</ButtonSecondary>
      </div>
    </div>
  );
}

// ─── Verrou : invité déjà inscrit par un tiers ───────────────────────
function VerrouBanner({ parentName, onAck, onCancel }) {
  return (
    <div style={{
      marginBottom: 22, padding: "18px 20px",
      background: T.orPale, border: `1.5px solid ${T.or}`,
      animation: "fadeIn .25s ease",
    }}>
      <div style={{ fontFamily: T.mono, fontSize: 10, letterSpacing: 2, color: T.orDeep, textTransform: "uppercase", marginBottom: 6 }}>
        ✦ déjà inscrit
      </div>
      <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 18, color: T.ink, lineHeight: 1.3, marginBottom: 6 }}>
        Vous avez déjà été inscrit par <span style={{ color: T.rouge }}>{parentName}</span>.
      </div>
      <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 14, color: T.inkSoft, lineHeight: 1.5, marginBottom: 14 }}>
        Voulez-vous modifier votre réponse ? Vos choix écraseront ceux saisis pour vous.
      </div>
      <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
        <ButtonPrimary type="button" onClick={onAck} style={{ padding: "10px 18px" }}>
          ✓ Oui, modifier
        </ButtonPrimary>
        <ButtonSecondary onClick={onCancel}>Non, retour</ButtonSecondary>
      </div>
    </div>
  );
}

// ─── PlusOnePicker : fuzzy match dans la liste, free text si plus_one_max=1 ──
function PlusOnePicker({ chosen, guestList, form, setF }) {
  const [query, setQuery] = React.useState("");
  const allowFree = Number(chosen?.plus_one_max) === 1 || !chosen?.id;
  const freeMode = form.plusOneFreeMode && allowFree;

  // Liste filtrée : exclut soi-même + résultats fuzzy
  const candidates = React.useMemo(() => {
    if (!chosen?.id) return [];
    return guestList.filter(g => g.id && g.id !== chosen.id && g.categorie !== "Enfant" && g.categorie !== "+1");
  }, [guestList, chosen]);
  const matches = React.useMemo(() => {
    if (!query || query.length < 2) return [];
    return candidates
      .map(g => ({ ...g, score: fuzzyScore(query, g.name) }))
      .filter(x => x.score > 0.3)
      .sort((a, b) => b.score - a.score)
      .slice(0, 6);
  }, [query, candidates]);

  // Si on a déjà un partenaire sélectionné, on affiche la carte de confirmation
  if (form.plusOneId) {
    const partner = guestList.find(g => g.id === form.plusOneId);
    const partnerAlreadyOui = partner?.rsvp_statut === "oui";
    return (
      <FieldGroup label="Avec qui venez-vous ?">
        <div style={{ padding: "14px 16px", background: T.orPale, border: `1.5px solid ${T.or}`, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
          <div>
            <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 17, color: T.ink }}>{form.plusOneName}</div>
            <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 12, color: T.inkSoft, marginTop: 2 }}>
              {partnerAlreadyOui
                ? "Cette personne a déjà répondu — votre réponse sera juste liée à la sienne."
                : (form.plusOneSeparate
                  ? "Vous indiquez juste être ensemble — il/elle répondra de son côté."
                  : "Sa réponse sera enregistrée en même temps que la vôtre.")}
            </div>
          </div>
          <button type="button" onClick={() => { setF("plusOneId", null); setF("plusOneName", ""); setF("plusOneSeparate", false); }}
            style={{ background: "transparent", border: `1px solid ${T.ink}`, padding: "6px 12px", fontFamily: T.mono, fontSize: 10, letterSpacing: 1.5, color: T.ink, cursor: "pointer", textTransform: "uppercase" }}>
            × changer
          </button>
        </div>
        {!partnerAlreadyOui && (
          <label style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 10, padding: "8px 12px", background: T.paper, border: `1px solid ${T.ruleSoft}`, cursor: "pointer" }}>
            <input type="checkbox" checked={form.plusOneSeparate} onChange={(e) => setF("plusOneSeparate", e.target.checked)} style={{ accentColor: T.rouge, width: 16, height: 16 }}/>
            <span style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 13, color: T.inkSoft, lineHeight: 1.4 }}>
              Il / elle préfère répondre de son côté (ne pas pré-remplir sa RSVP)
            </span>
          </label>
        )}
      </FieldGroup>
    );
  }

  // Si free mode actif (plus_one_max=1)
  if (freeMode) {
    return (
      <FieldGroup label="Accompagnant·e (hors liste)" hint="Personne qui n'est pas dans la liste d'invités. Prénom et nom complets.">
        <input type="text" value={form.plusOneFreeText} onChange={(e) => setF("plusOneFreeText", e.target.value)} style={inputStyle()} placeholder="Prénom Nom"/>
        <button type="button" onClick={() => { setF("plusOneFreeMode", false); setF("plusOneFreeText", ""); }}
          style={{ marginTop: 8, background: "transparent", border: "none", padding: 0, fontFamily: T.mono, fontSize: 10, letterSpacing: 1.5, color: T.inkSoft, cursor: "pointer", textDecoration: "underline", textTransform: "uppercase" }}>
          ← retour à la liste
        </button>
      </FieldGroup>
    );
  }

  // Mode fuzzy par défaut
  return (
    <FieldGroup label="Avec qui venez-vous ?" hint="Cherchez dans la liste des invités. Laissez vide si vous venez seul·e.">
      <input type="text" value={query} onChange={(e) => setQuery(e.target.value)}
        placeholder="Prénom du / de la partenaire…" style={inputStyle()}/>
      {matches.length > 0 && (
        <div style={{ marginTop: 8, display: "flex", flexDirection: "column", gap: 4 }}>
          {matches.map((m) => {
            const dejaOui = m.rsvp_statut === "oui";
            return (
              <button key={m.id} type="button" onClick={() => { setF("plusOneId", m.id); setF("plusOneName", m.name); setQuery(""); }}
                style={{
                  textAlign: "left", padding: "10px 14px",
                  background: T.paper, border: `1px solid ${T.ruleSoft}`,
                  fontFamily: T.sans, fontSize: 14, cursor: "pointer",
                  display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10,
                  transition: "background .15s, border-color .15s",
                }}
                onMouseEnter={(e) => { e.currentTarget.style.background = T.orPale; e.currentTarget.style.borderColor = T.or; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = T.paper; e.currentTarget.style.borderColor = T.ruleSoft; }}>
                <span>{m.name}</span>
                <span style={{ display: "flex", gap: 6, alignItems: "center" }}>
                  {dejaOui && <span style={{ fontFamily: T.mono, fontSize: 9, color: T.inkMute, background: T.paperDeep || "#EEE7DB", padding: "2px 6px", letterSpacing: 1, textTransform: "uppercase" }}>déjà inscrit</span>}
                  {!dejaOui && m.partner_id && <span style={{ fontFamily: T.mono, fontSize: 9, color: T.or, background: T.orPale, padding: "2px 6px", letterSpacing: 1, textTransform: "uppercase" }}>lié à un autre</span>}
                  <span style={{ fontFamily: T.mono, fontSize: 10, color: T.or, letterSpacing: 1 }}>♥ →</span>
                </span>
              </button>
            );
          })}
        </div>
      )}
      {query.length >= 2 && matches.length === 0 && (
        <div style={{ marginTop: 10, padding: "12px 14px", background: T.rougePale, border: `1px solid ${T.rouge}`, fontSize: 13, color: T.rouge, lineHeight: 1.45 }}>
          {allowFree
            ? <>Pas de correspondance. Cette personne n'est pas dans la liste — <button type="button" onClick={() => { setF("plusOneFreeMode", true); setF("plusOneFreeText", query); setQuery(""); }} style={{ background: "transparent", border: "none", padding: 0, color: T.rouge, fontFamily: T.mono, fontSize: 11, letterSpacing: 1, cursor: "pointer", textDecoration: "underline" }}>l'ajouter hors liste →</button></>
            : "Pas de correspondance. Si votre partenaire n'est pas dans la liste, contactez Enora & Antoine directement."}
        </div>
      )}
      {allowFree && query.length < 2 && (
        <div style={{ marginTop: 10 }}>
          <button type="button" onClick={() => { setF("plusOneFreeMode", true); }}
            style={{ background: "transparent", border: "none", padding: 0, fontFamily: T.mono, fontSize: 10, letterSpacing: 1.5, color: T.inkSoft, cursor: "pointer", textDecoration: "underline", textTransform: "uppercase" }}>
            + ajouter une personne hors liste
          </button>
        </div>
      )}
    </FieldGroup>
  );
}

// ─── ChildrenSection : N slots de prénom + âge (nom hérité du parent) ──
function ChildrenSection({ chosen, form, setF }) {
  const max = Number(chosen?.enfant_max) || 0;
  const children = form.children || [];
  const updateChild = (i, field, value) => {
    const next = [...children];
    next[i] = { ...next[i], [field]: value };
    setF("children", next);
  };
  return (
    <FieldGroup label={`Vos enfants (jusqu'à ${max})`} hint={`Le nom est hérité de "${chosen?.nom || chosen?.name}". Prénom + âge.`}>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {children.map((c, i) => (
          <div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
            <div style={{ fontFamily: T.mono, fontSize: 11, letterSpacing: 1, color: T.inkMute, width: 16, textAlign: "right" }}>{i + 1}.</div>
            <input type="text" value={c.prenom || ""} onChange={(e) => updateChild(i, "prenom", e.target.value)}
              placeholder={`Prénom de l'enfant ${i + 1}`} style={{ ...inputStyle(), flex: 1 }}/>
            <input type="text" value={c.age || ""} onChange={(e) => updateChild(i, "age", e.target.value)}
              placeholder="Âge" style={{ ...inputStyle(), width: 80 }}/>
          </div>
        ))}
      </div>
      <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 12, color: T.inkMute, marginTop: 6 }}>
        Laissez vide si vous venez sans enfant ou avec moins d'enfants que prévu.
      </div>
    </FieldGroup>
  );
}

function FieldGroup({ label, hint, children }) {
  return (
    <div style={{ marginBottom: 18 }}>
      <label style={{ display: "block", fontFamily: T.bold, fontWeight: 600, fontSize: 14, color: T.ink, marginBottom: hint ? 4 : 8 }}>
        {label}
      </label>
      {hint && (
        <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 12, color: T.inkSoft, marginBottom: 8 }}>
          {hint}
        </div>
      )}
      {children}
    </div>
  );
}

function inputStyle() {
  return {
    width: "100%", padding: "12px 14px",
    background: T.paper, border: `1px solid ${T.ruleSoft}`,
    fontFamily: T.sans, fontSize: 14, color: T.ink,
    outline: "none", transition: "border-color .15s, background .15s",
  };
}

function CheckboxRow({ items, value, onChange }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
      {items.map((it) => (
        <label key={it.k} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 14px", background: value[it.k] ? T.orPale : T.paper, border: `1px solid ${value[it.k] ? T.or : T.ruleSoft}`, cursor: "pointer", transition: "all .12s" }}>
          <input type="checkbox" checked={value[it.k]} onChange={(e) => onChange(it.k, e.target.checked)} style={{ accentColor: T.rouge, width: 18, height: 18 }} />
          <span style={{ fontFamily: T.sans, fontSize: 14, color: T.ink }}>{it.l}</span>
        </label>
      ))}
    </div>
  );
}

function RadioRow({ items, value, onChange }) {
  return (
    <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
      {items.map((it) => (
        <label key={it.v} style={{
          flex: "1 1 auto", display: "flex", alignItems: "center", gap: 8, padding: "10px 14px",
          background: value === it.v ? T.orPale : T.paper, border: `1px solid ${value === it.v ? T.or : T.ruleSoft}`,
          cursor: "pointer", transition: "all .12s", minWidth: 120, justifyContent: "center",
        }}>
          <input type="radio" name={items[0].v + items.map(i => i.v).join()} checked={value === it.v} onChange={() => onChange(it.v)} style={{ accentColor: T.rouge }} />
          <span style={{ fontFamily: T.sans, fontSize: 13, color: T.ink }}>{it.l}</span>
        </label>
      ))}
    </div>
  );
}

// Confirmation
function RsvpThanks({ onAgain, state, error }) {
  const isQueued = state === "queued";
  return (
    <div style={{ background: T.white, color: T.ink, padding: "60px 32px", maxWidth: 660, margin: "0 auto", textAlign: "center" }}>
      <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 60, color: T.rouge, lineHeight: 1 }}>♥</div>
      <div className="display-bold" style={{ fontSize: 40, color: T.ink, marginTop: 16 }}>
        Merci, on a votre carte.
      </div>
      <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 17, color: T.inkSoft, lineHeight: 1.55, marginTop: 14, maxWidth: 480, margin: "14px auto 0" }}>
        On vous tient au courant des dernières infos (navette, hébergements, dress code détaillé…) par email d'ici l'été 2027.
      </div>
      {isQueued && (
        <div style={{ marginTop: 24, padding: "12px 16px", background: T.orPale, border: `1px solid ${T.or}`, maxWidth: 480, margin: "24px auto 0", textAlign: "left" }}>
          <div style={{ fontFamily: T.mono, fontSize: 10, letterSpacing: 2, color: T.orDeep, textTransform: "uppercase", marginBottom: 6 }}>✨ réponse en attente</div>
          <div style={{ fontFamily: T.italic, fontStyle: "italic", fontSize: 13, color: T.ink, lineHeight: 1.5 }}>
            Votre réponse est enregistrée sur votre navigateur. On la récupérera automatiquement dès que possible.{error ? <><br/><span style={{ fontFamily: T.mono, fontSize: 10, color: T.inkMute }}>{error}</span></> : null}
          </div>
        </div>
      )}
      <div style={{ marginTop: 32, display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }}>
        <ButtonSecondary onClick={onAgain}>+ Inscrire quelqu'un d'autre</ButtonSecondary>
      </div>
      <AppLinkPostRsvpCTA />
    </div>
  );
}

// Admin : panneau caché pour Enora & Antoine
// Accessible via ?admin dans l'URL
function AdminPanel() {
  const [responses, setResponses] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(RSVP_KEY) || "[]"); } catch { return []; }
  });

  React.useEffect(() => {
    const id = setInterval(() => {
      try { setResponses(JSON.parse(localStorage.getItem(RSVP_KEY) || "[]")); } catch {}
    }, 2000);
    return () => clearInterval(id);
  }, []);

  const exportCsv = () => {
    if (!responses.length) return;
    const headers = ["at", "name", "mairie", "ceremonie", "vinHonneur", "repas", "soiree", "plusOne", "childrenCount", "childrenAges", "allergies", "song", "hebergement", "covoiturage", "message", "email", "phone"];
    const escape = (v) => `"${String(v).replace(/"/g, '""')}"`;
    const rows = [headers.join(",")];
    for (const r of responses) {
      rows.push(headers.map((h) => {
        if (h in r.presents) return escape(r.presents[h] ? "oui" : "non");
        return escape(r[h] ?? "");
      }).join(","));
    }
    const blob = new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `rsvp-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click();
  };

  const clearAll = () => {
    if (confirm("Effacer toutes les réponses du navigateur ?")) {
      localStorage.removeItem(RSVP_KEY);
      setResponses([]);
    }
  };

  return (
    <Section id="section-admin" bg={T.paper} paddingTop={60} paddingBottom={60}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24, flexWrap: "wrap", gap: 12 }}>
        <div>
          <div className="eyebrow">Admin · réservé aux mariés</div>
          <div style={{ fontFamily: T.bold, fontWeight: 700, fontSize: 32, color: T.ink, marginTop: 4 }}>
            Réponses RSVP <span style={{ color: T.rouge }}>({responses.length})</span>
          </div>
        </div>
        <div style={{ display: "flex", gap: 10 }}>
          <ButtonPrimary onClick={exportCsv}>⤓ Exporter CSV</ButtonPrimary>
          <ButtonSecondary onClick={clearAll}>Effacer tout</ButtonSecondary>
        </div>
      </div>

      {responses.length === 0 ? (
        <div style={{ padding: 24, background: T.white, fontFamily: T.italic, fontStyle: "italic", color: T.inkSoft }}>
          Aucune réponse pour l'instant.
        </div>
      ) : (
        <div style={{ overflowX: "auto", background: T.white }}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12, fontFamily: T.sans }}>
            <thead>
              <tr style={{ background: T.ink, color: T.white }}>
                {["Quand", "Nom", "Présences", "+1", "Enfants", "Allergies", "Email"].map((h) => (
                  <th key={h} style={{ padding: "8px 10px", textAlign: "left", fontFamily: T.mono, fontSize: 10, letterSpacing: 1, textTransform: "uppercase" }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {responses.map((r, i) => (
                <tr key={i} style={{ borderBottom: `1px solid ${T.ruleSoft}` }}>
                  <td style={{ padding: "8px 10px", fontFamily: T.mono, fontSize: 11, color: T.inkSoft }}>{new Date(r.at).toLocaleString("fr-FR")}</td>
                  <td style={{ padding: "8px 10px", fontWeight: 600 }}>{r.name}</td>
                  <td style={{ padding: "8px 10px", color: T.inkSoft }}>
                    {Object.entries(r.presents || {}).filter(([_, v]) => v).map(([k]) => k).join(", ")}
                  </td>
                  <td style={{ padding: "8px 10px" }}>{r.plusOne || "—"}</td>
                  <td style={{ padding: "8px 10px" }}>{r.childrenCount > 0 ? `${r.childrenCount} (${r.childrenAges})` : "—"}</td>
                  <td style={{ padding: "8px 10px", color: T.rouge }}>{r.allergies || "—"}</td>
                  <td style={{ padding: "8px 10px", fontFamily: T.mono, fontSize: 11 }}>{r.email || "—"}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </Section>
  );
}

Object.assign(window, { RsvpSection, AdminPanel });
