// admin-app/gites.jsx — Plan hébergement (gîtes + chambres + hors château)
//
// Drag-and-drop à 2 niveaux :
// - Carte par gîte (9 gîtes du château)
// - Sous-cartes par chambre (24 chambres, chacune drop-target)
// - Zone "Hors château" avec 3 buckets (voiture / pied / inconnu)
//
// Quand on drag un invité qui a un partner_id ou des enfants, un mini-modal
// propose de placer aussi les autres dans la même chambre (ou bucket).
//
// Le backend déduit gite_id depuis chambre_id via assignChambre ; pour les
// buckets hors-château, on assigne gite_id="hors-XXX" et chambre_id="".

const UNASSIGNED_ID = "__unassigned__";

// Buckets hors-château (côté UI — doit matcher la convention backend)
const HORS_BUCKETS = [
  { id: "hors-voiture", label: "Voiture",        icon: "🚗", desc: "Rentre en voiture" },
  { id: "hors-pied",    label: "À pied",         icon: "🚶", desc: "Rentre à pied / sur place" },
  { id: "hors-inconnu", label: "Statut inconnu", icon: "?",  desc: "Hors château, à clarifier" },
];

function isHorsBucket(id) { return /^hors-/.test(String(id || "")); }

// localStorage key pour les modifs en attente (persist entre refresh)
const DIRTY_LS_KEY = "ea-admin:gites:dirty:v1";

function loadDirty_() {
  try {
    const raw = localStorage.getItem(DIRTY_LS_KEY);
    if (!raw) return {};
    const parsed = JSON.parse(raw);
    return (parsed && typeof parsed === "object") ? parsed : {};
  } catch { return {}; }
}

function saveDirty_(dirty) {
  try {
    if (!dirty || Object.keys(dirty).length === 0) {
      localStorage.removeItem(DIRTY_LS_KEY);
    } else {
      localStorage.setItem(DIRTY_LS_KEY, JSON.stringify(dirty));
    }
  } catch {}
}

// ════════════════════════════════════════════════════════════════════
// PAGE PRINCIPALE
// ════════════════════════════════════════════════════════════════════
function GitesPage({ data, onReload }) {
  const invites  = data.invites  || [];
  const gites    = data.gites    || [];
  const chambres = data.chambres || [];

  // ─── Modifs en attente (dirty) ─ mode BATCH : aucun backend call par drag.
  // Le bandeau bas affiche le nombre de modifs ; clic "Sauvegarder" → 1 seul
  // appel bulkAssign. Persisté en localStorage pour survivre au refresh.
  const [dirty, setDirtyRaw] = React.useState(() => loadDirty_());
  const [saving, setSaving] = React.useState(false);
  const [saveErr, setSaveErr] = React.useState("");

  const setDirty = (updater) => {
    setDirtyRaw(prev => {
      const next = typeof updater === "function" ? updater(prev) : updater;
      saveDirty_(next);
      return next;
    });
  };

  // Tant qu'il y a des modifs non sauvées, on freeze l'auto-refresh pour
  // que le pull backend ne vienne pas perturber le state local.
  const dirtyCount = Object.keys(dirty).length;
  React.useEffect(() => {
    window.__adminFreezeRefresh = dirtyCount > 0;
    return () => { window.__adminFreezeRefresh = false; };
  }, [dirtyCount]);

  // Après un reload backend réussi, nettoie les dirty qui sont déjà alignés
  // côté backend (peut arriver si quelqu'un d'autre a fait l'assign ou si on
  // vient de Save).
  React.useEffect(() => {
    setDirty(prev => {
      const next = { ...prev };
      let changed = false;
      for (const id of Object.keys(prev)) {
        const inv = invites.find(i => i.id === id);
        if (!inv) continue;
        const d = prev[id];
        if ((inv.gite_id || "") === (d.gite_id || "") &&
            (inv.chambre_id || "") === (d.chambre_id || "")) {
          delete next[id]; changed = true;
        }
      }
      return changed ? next : prev;
    });
  }, [invites]);

  // Filtres
  const [coteFilter, setCoteFilter] = React.useState("all");   // all | E | A
  const [catFilter, setCatFilter]   = React.useState("all");   // all | Famille | Amis
  const [enfFilter, setEnfFilter]   = React.useState("all");   // all | avec | sans

  // Suggestion couples/enfants : { inviteId, suggested:[ids], target:{gite,chambre} } | null
  const [pendingSuggest, setPendingSuggest] = React.useState(null);

  // Édition gîte (modal — réutilise pattern Tables)
  const [editingGiteId, setEditingGiteId] = React.useState(null);

  // ─── Calculs ───
  const placable = invites.filter(i => i.rsvp_statut === "oui");

  // Apply filtres (uniquement sur la zone "non placés" et visibilité)
  const passesFilters = (inv) => {
    if (coteFilter !== "all" && !((inv.cote || "").toLowerCase().startsWith(coteFilter.toLowerCase()))) return false;
    if (catFilter !== "all" && (inv.categorie || "") !== catFilter) return false;
    if (enfFilter !== "all") {
      // "avec enfants" = invité qui a au moins 1 enfant lié (categorie==='Enfant' && plus_one_of===inv.id)
      const hasChildren = invites.some(c => c.categorie === "Enfant" && c.plus_one_of === inv.id);
      if (enfFilter === "avec" && !hasChildren) return false;
      if (enfFilter === "sans" && hasChildren) return false;
    }
    return true;
  };

  // gite_id et chambre_id effectifs (dirty > backend)
  const eff = (inv) => {
    const d = dirty[inv.id];
    return {
      gite_id: d?.gite_id !== undefined ? d.gite_id : (inv.gite_id || ""),
      chambre_id: d?.chambre_id !== undefined ? d.chambre_id : (inv.chambre_id || ""),
    };
  };

  // Groupage
  const byChambre = React.useMemo(() => {
    const map = {};
    chambres.forEach(c => { map[c.id] = []; });
    placable.forEach(inv => {
      const { chambre_id } = eff(inv);
      if (chambre_id && map[chambre_id]) map[chambre_id].push(inv);
    });
    Object.keys(map).forEach(k => sortInvites_(map[k]));
    return map;
  }, [placable, chambres, dirty]);

  // Invités dans un gîte mais sans chambre précise (placés en gîte sans détail)
  const byGiteNoChambre = React.useMemo(() => {
    const map = {};
    gites.forEach(g => { map[g.id] = []; });
    placable.forEach(inv => {
      const { gite_id, chambre_id } = eff(inv);
      if (gite_id && !chambre_id && map[gite_id]) map[gite_id].push(inv);
    });
    Object.keys(map).forEach(k => sortInvites_(map[k]));
    return map;
  }, [placable, gites, dirty]);

  const byHors = React.useMemo(() => {
    const map = {};
    HORS_BUCKETS.forEach(b => { map[b.id] = []; });
    placable.forEach(inv => {
      const { gite_id } = eff(inv);
      if (isHorsBucket(gite_id) && map[gite_id]) map[gite_id].push(inv);
    });
    Object.keys(map).forEach(k => sortInvites_(map[k]));
    return map;
  }, [placable, dirty]);

  const unassigned = placable.filter(inv => {
    const { gite_id, chambre_id } = eff(inv);
    return !gite_id && !chambre_id;
  });
  sortInvites_(unassigned);

  // KPIs
  const reservedSeats = chambres.filter(c => c.reserve).reduce((s, c) => s + (Number(c.capacite) || 0), 0);
  const totalCapa = chambres.reduce((s, c) => s + (Number(c.capacite) || 0), 0);
  const dispoCapa = totalCapa - reservedSeats; // 50 - 2 = 48
  const placedChateau = placable.filter(inv => {
    const { gite_id } = eff(inv);
    return gite_id && !isHorsBucket(gite_id);
  }).length;
  const placedHors = placable.filter(inv => isHorsBucket(eff(inv).gite_id)).length;
  const gitesSatures = gites.filter(g => {
    const occ = placable.filter(inv => eff(inv).gite_id === g.id).length;
    return occ > (Number(g.lits) || 0);
  }).length;
  const gitesVides = gites.filter(g => {
    const occ = placable.filter(inv => eff(inv).gite_id === g.id).length;
    return occ === 0;
  }).length;

  // ─── Drag-and-drop — mode BATCH ───
  // Le drag modifie uniquement le state local "dirty". Aucun appel backend.
  // On garde la suggestion partner/enfants — le confirm met juste à jour dirty.
  const moveInvite = (inviteId, target, opts = {}) => {
    const { skipSuggest } = opts;
    const inv = invites.find(i => i.id === inviteId);
    if (!inv) return;
    const current = eff(inv);

    let newGite = "";
    let newChambre = "";
    if (target.unassigned) {
      newGite = ""; newChambre = "";
    } else if (target.chambre_id) {
      const c = chambres.find(ch => ch.id === target.chambre_id);
      if (!c) return;
      if (c.reserve) {
        alert(`Chambre réservée pour ${c.reserve}.`);
        return;
      }
      newChambre = target.chambre_id;
      newGite = c.gite_id;
    } else if (target.gite_id) {
      newGite = target.gite_id;
      newChambre = "";
    }

    if (current.gite_id === newGite && current.chambre_id === newChambre) return;

    // Update dirty
    setDirty(prev => ({ ...prev, [inviteId]: { gite_id: newGite, chambre_id: newChambre } }));

    // Suggestion partner + enfants
    if (!target.unassigned && !skipSuggest) {
      const linked = findLinkedInvites_(inv, invites);
      const toSuggest = linked.filter(other => {
        const o = eff(other);
        return o.gite_id !== newGite || o.chambre_id !== newChambre;
      });
      if (toSuggest.length > 0) {
        setPendingSuggest({
          anchor: inv,
          linked: toSuggest,
          target: { gite_id: newGite, chambre_id: newChambre },
        });
      }
    }
  };

  // Confirmation suggestion → met dirty pour tous les linked en une fois
  const confirmSuggest = () => {
    const snapshot = pendingSuggest;
    setPendingSuggest(null);
    if (!snapshot) return;
    const { linked, target } = snapshot;
    setDirty(prev => {
      const next = { ...prev };
      const newGite = target.chambre_id
        ? (chambres.find(c => c.id === target.chambre_id)?.gite_id || "")
        : (target.gite_id || "");
      const newChambre = target.chambre_id || "";
      for (const inv of linked) {
        next[inv.id] = { gite_id: newGite, chambre_id: newChambre };
      }
      return next;
    });
  };

  // Sauvegarder — envoie 1 seul bulkAssign avec toutes les modifs
  const saveAll = async () => {
    if (dirtyCount === 0) return;
    setSaving(true); setSaveErr("");
    const ops = Object.keys(dirty).map(id => ({
      invite_id: id,
      gite_id: dirty[id].gite_id || "",
      chambre_id: dirty[id].chambre_id || "",
    }));
    try {
      const res = await adminBulkAssign(ops);
      if (!res?.ok) {
        setSaveErr(res?.error || "Erreur inconnue");
        setSaving(false);
        return;
      }
      // Repere les ops qui ont échoué (chambre réservée par ex.) et garde-les en dirty
      const failed = (res.results || []).filter(r => !r.ok);
      if (failed.length > 0) {
        const failedIds = new Set(failed.map(f => f.invite_id));
        setDirty(prev => {
          const next = {};
          for (const id of Object.keys(prev)) if (failedIds.has(id)) next[id] = prev[id];
          return next;
        });
        setSaveErr(`${failed.length} affectation(s) refusée(s) : ${failed.map(f => f.error).join(", ")}`);
      } else {
        setDirty({});
      }
      setSaving(false);
      onReload?.();
    } catch (e) {
      setSaveErr("Réseau : " + e.message);
      setSaving(false);
    }
  };

  const cancelAll = () => {
    if (dirtyCount === 0) return;
    if (!confirm(`Annuler les ${dirtyCount} modification(s) en attente ?`)) return;
    setDirty({});
  };

  return (
    <AdPage
      eyebrow={`${placable.length} invités confirmés · ${dispoCapa} places dispo au château`}
      title="Hébergement"
      actions={
        <button className="btn sm" onClick={() => {
          if (dirtyCount > 0 && !confirm(`${dirtyCount} modification(s) en attente seront écrasées. Recharger quand même ?`)) return;
          if (dirtyCount > 0) setDirty({});
          onReload?.();
        }}>↻ Recharger</button>
      }
    >
      {/* KPIs */}
      <div style={{ display: "flex", gap: 12, marginBottom: 14, flexWrap: "wrap", alignItems: "center" }}>
        <KpiPill value={`${placedChateau}/${dispoCapa}`} label="Au château" tone={placedChateau >= dispoCapa ? "alert" : "ok"}/>
        <KpiPill value={placedHors} label="Hors château" tone="warn"/>
        <KpiPill value={unassigned.length} label="Non placés" tone={unassigned.length > 0 ? "warn" : "ok"}/>
        <KpiPill value={gitesSatures} label="Gîtes saturés" tone={gitesSatures > 0 ? "alert" : "ok"}/>
        <KpiPill value={gitesVides} label="Gîtes vides" tone="mute"/>
      </div>

      {/* Filtres */}
      <div style={{ display: "flex", gap: 14, marginBottom: 18, flexWrap: "wrap", fontFamily: AD.mono, fontSize: 9, letterSpacing: 1.2, color: AD.inkSoft, textTransform: "uppercase", alignItems: "center" }}>
        <FilterGroup label="Côté"     value={coteFilter} setValue={setCoteFilter} options={[["all","Tout"],["E","Enora"],["A","Antoine"]]}/>
        <FilterGroup label="Lien"     value={catFilter}  setValue={setCatFilter}  options={[["all","Tout"],["Famille","Famille"],["Amis","Amis"]]}/>
        <FilterGroup label="Enfants"  value={enfFilter}  setValue={setEnfFilter}  options={[["all","Tout"],["avec","Avec"],["sans","Sans"]]}/>
      </div>

      {/* Grille principale : 2 colonnes */}
      <div style={{ display: "grid", gridTemplateColumns: "minmax(240px, 280px) 1fr", gap: 16, alignItems: "start" }}>
        {/* ─── Colonne gauche : non placés + hors château ─── */}
        <div style={{ display: "flex", flexDirection: "column", gap: 12, position: "sticky", top: 12 }}>
          <UnassignedColumn
            invites={unassigned.filter(passesFilters)}
            totalCount={unassigned.length}
            chambres={chambres}
            horsBuckets={HORS_BUCKETS}
            onMove={moveInvite}
          />
          <HorsChateauZone
            buckets={HORS_BUCKETS}
            byHors={byHors}
            chambres={chambres}
            horsBuckets={HORS_BUCKETS}
            passesFilters={passesFilters}
            onMove={moveInvite}
          />
        </div>

        {/* ─── Colonne droite : 9 gîtes en grille 3×3 ─── */}
        <div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
          {gites.map(g => {
            const gChambres = chambres.filter(c => c.gite_id === g.id);
            const gOcc = placable.filter(inv => eff(inv).gite_id === g.id).length;
            const gCapa = Number(g.lits) || gChambres.reduce((s, c) => s + (Number(c.capacite) || 0), 0);
            const overcap = gOcc > gCapa;
            return (
              <GiteCard
                key={g.id}
                gite={g}
                chambres={gChambres}
                byChambre={byChambre}
                ghostInvites={byGiteNoChambre[g.id] || []}
                allInvites={invites}
                chambresAll={chambres}
                horsBuckets={HORS_BUCKETS}
                passesFilters={passesFilters}
                occCount={gOcc}
                capa={gCapa}
                overcap={overcap}
                onMove={moveInvite}
                onEdit={() => setEditingGiteId(g.id)}
              />
            );
          })}
        </div>
      </div>

      {pendingSuggest && (
        <SuggestModal
          suggest={pendingSuggest}
          chambres={chambres}
          gites={gites}
          horsBuckets={HORS_BUCKETS}
          onConfirm={confirmSuggest}
          onCancel={() => setPendingSuggest(null)}
        />
      )}
      {editingGiteId && (
        <GiteEditModal
          gite={gites.find(g => g.id === editingGiteId)}
          onClose={() => setEditingGiteId(null)}
          onSaved={() => { setEditingGiteId(null); onReload?.(); }}
        />
      )}

      {/* Bandeau sticky bas — visible uniquement quand il y a des modifs en attente */}
      {dirtyCount > 0 && (
        <AdSaveBar count={dirtyCount} saving={saving} err={saveErr}
          onSave={saveAll} onCancel={cancelAll} label="Modification"/>
      )}
    </AdPage>
  );
}

// (SaveBar déplacé dans components.jsx → AdSaveBar)

// Tri partagé : famille en premier, puis +1, puis Enfant, et par prénom
function sortInvites_(arr) {
  const order = { "Famille": 0, "Amis": 1, "+1": 2, "Enfant": 3 };
  arr.sort((a, b) => {
    const oa = order[a.categorie] ?? 9;
    const ob = order[b.categorie] ?? 9;
    if (oa !== ob) return oa - ob;
    return (a.prenom || "").localeCompare(b.prenom || "");
  });
}

// Cherche les invités liés (partner_id symétrique + enfants où plus_one_of===inv.id + +1)
function findLinkedInvites_(inv, invites) {
  const linked = [];
  // Partner
  if (inv.partner_id) {
    const p = invites.find(i => i.id === inv.partner_id);
    if (p && p.rsvp_statut === "oui") linked.push(p);
  }
  // Enfants & +1 liés (plus_one_of)
  invites.forEach(i => {
    if (i.plus_one_of === inv.id && i.rsvp_statut === "oui") linked.push(i);
  });
  // Dédup
  const seen = new Set();
  return linked.filter(x => {
    if (seen.has(x.id)) return false;
    seen.add(x.id); return true;
  });
}

// ─── Sous-composants ──────────────────────────────────────────────────

function FilterGroup({ label, value, setValue, options }) {
  return (
    <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
      <span>{label}</span>
      {options.map(([id, l]) => (
        <button key={id} onClick={() => setValue(id)} className="btn sm"
          style={{
            background: value === id ? AD.ink : "transparent",
            color: value === id ? AD.white : AD.inkSoft,
            borderColor: value === id ? AD.ink : AD.ruleSoft,
            padding: "4px 9px",
          }}>{l}</button>
      ))}
    </div>
  );
}

function KpiPill({ value, label, tone }) {
  const colors = {
    ok:    { bg: AD.sagePale,  fg: AD.sage },
    warn:  { bg: AD.orPale,    fg: AD.orDeep },
    alert: { bg: AD.rougePale, fg: AD.rougeDeep },
    mute:  { bg: AD.paperDeep, fg: AD.inkMute },
  };
  const c = colors[tone] || colors.ok;
  return (
    <div style={{ padding: "6px 12px", background: c.bg, borderLeft: `3px solid ${c.fg}`, display: "flex", alignItems: "baseline", gap: 8 }}>
      <span style={{ fontFamily: AD.display, fontWeight: 700, fontSize: 18, color: c.fg, lineHeight: 1 }}>{value}</span>
      <span style={{ fontFamily: AD.mono, fontSize: 9, letterSpacing: 1.5, color: c.fg, textTransform: "uppercase" }}>{label}</span>
    </div>
  );
}

// ─── Colonne "Non placés" ────────────────────────────────────────────
function UnassignedColumn({ invites, totalCount, chambres, horsBuckets, onMove }) {
  const [dragOver, setDragOver] = React.useState(false);
  return (
    <div
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={(e) => {
        e.preventDefault(); setDragOver(false);
        const inviteId = e.dataTransfer.getData("text/invite_id");
        if (inviteId) onMove(inviteId, { unassigned: true });
      }}
      style={{
        background: dragOver ? AD.orPale : AD.paperDeep,
        border: `1px solid ${dragOver ? AD.or : AD.ruleSoft}`,
        borderLeft: `4px solid ${AD.inkMute}`,
        display: "flex", flexDirection: "column",
      }}>
      <div style={{ padding: "10px 12px", borderBottom: `1px solid ${AD.ruleSoft}` }}>
        <div className="display-bold" style={{ fontSize: 13 }}>Non placés</div>
        <div style={{ fontFamily: AD.mono, fontSize: 9, color: AD.inkMute, letterSpacing: 1 }}>
          {invites.length}{invites.length !== totalCount ? `/${totalCount}` : ""} invités à placer
        </div>
      </div>
      <div style={{ padding: 8, display: "flex", flexDirection: "column", gap: 4, maxHeight: "calc(100vh - 280px)", overflowY: "auto" }}>
        {invites.length === 0 ? (
          <div style={{ padding: "20px 8px", textAlign: "center", fontFamily: AD.italic, fontStyle: "italic", fontSize: 11, color: AD.inkMute }}>
            {totalCount === 0 ? "Tout est placé 🎉" : "Aucun invité ne passe le filtre"}
          </div>
        ) : (
          invites.map(inv => (
            <InviteCard key={inv.id} inv={inv} chambres={chambres} horsBuckets={horsBuckets} onMove={onMove} compact/>
          ))
        )}
      </div>
    </div>
  );
}

// ─── Zone "Hors château" (3 sous-buckets) ────────────────────────────
function HorsChateauZone({ buckets, byHors, chambres, horsBuckets, passesFilters, onMove }) {
  return (
    <div style={{ background: AD.white, border: `1px solid ${AD.ruleSoft}`, borderLeft: `4px solid ${AD.rouge}` }}>
      <div style={{ padding: "10px 12px", borderBottom: `1px solid ${AD.ruleSoft}` }}>
        <div className="display-bold" style={{ fontSize: 13 }}>Hors château</div>
        <div style={{ fontFamily: AD.mono, fontSize: 9, color: AD.inkMute, letterSpacing: 1 }}>
          {buckets.reduce((s, b) => s + (byHors[b.id]?.length || 0), 0)} invités
        </div>
      </div>
      <div style={{ display: "flex", flexDirection: "column" }}>
        {buckets.map(b => (
          <HorsBucket key={b.id} bucket={b} invites={(byHors[b.id] || []).filter(passesFilters)} totalCount={byHors[b.id]?.length || 0}
            chambres={chambres} horsBuckets={horsBuckets} onMove={onMove}/>
        ))}
      </div>
    </div>
  );
}

function HorsBucket({ bucket, invites, totalCount, chambres, horsBuckets, onMove }) {
  const [dragOver, setDragOver] = React.useState(false);
  return (
    <div
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={(e) => {
        e.preventDefault(); setDragOver(false);
        const inviteId = e.dataTransfer.getData("text/invite_id");
        if (inviteId) onMove(inviteId, { gite_id: bucket.id });
      }}
      style={{
        borderTop: `1px solid ${AD.ruleSoft}`,
        background: dragOver ? AD.orPale : "transparent",
        padding: "8px 12px",
        transition: "background .12s",
      }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
        <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
          <span style={{ fontSize: 14 }}>{bucket.icon}</span>
          <span style={{ fontFamily: AD.sans, fontSize: 12, color: AD.ink, fontWeight: 500 }}>{bucket.label}</span>
        </div>
        <span style={{ fontFamily: AD.mono, fontSize: 9, color: AD.inkMute }}>{totalCount}</span>
      </div>
      <div style={{ fontFamily: AD.italic, fontStyle: "italic", fontSize: 10, color: AD.inkMute, marginBottom: 6 }}>{bucket.desc}</div>
      {invites.length === 0 ? (
        <div style={{ padding: "4px 0", fontFamily: AD.italic, fontStyle: "italic", fontSize: 10, color: AD.inkMute }}>Glisser ici</div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
          {invites.map(inv => (
            <InviteCard key={inv.id} inv={inv} chambres={chambres} horsBuckets={horsBuckets} onMove={onMove} compact mini/>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── Carte d'un gîte (avec chambres en sous-cards) ───────────────────
function GiteCard({ gite, chambres, byChambre, ghostInvites, allInvites, chambresAll, horsBuckets, passesFilters, occCount, capa, overcap, onMove, onEdit }) {
  const reserved = chambres.some(c => c.reserve);
  return (
    <div style={{
      background: AD.white,
      border: `1px solid ${overcap ? AD.rouge : AD.ruleSoft}`,
      borderLeft: `4px solid ${overcap ? AD.rouge : (reserved ? AD.or : AD.sage)}`,
      display: "flex", flexDirection: "column",
    }}>
      {/* Header gîte */}
      <div style={{ padding: "8px 10px", borderBottom: `1px solid ${AD.ruleSoft}`, display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6 }}>
        <div style={{ minWidth: 0 }}>
          <div className="display-bold" style={{ fontSize: 14, color: overcap ? AD.rougeDeep : AD.ink, lineHeight: 1.1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
            {gite.nom}
          </div>
          <div style={{ fontFamily: AD.mono, fontSize: 8, color: AD.inkMute, letterSpacing: 0.5, marginTop: 2 }}>{gite.notes || ""}</div>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
          <span style={{ fontFamily: AD.mono, fontSize: 10, color: overcap ? AD.rougeDeep : AD.inkSoft, fontWeight: 600 }}>{occCount}/{capa}</span>
          <button onClick={onEdit} title="Éditer" style={{
            background: "transparent", border: "none", padding: 2, cursor: "pointer",
            fontFamily: AD.mono, fontSize: 11, color: AD.inkMute,
          }}>✎</button>
        </div>
      </div>

      {/* Chambres */}
      <div style={{ padding: 6, display: "flex", flexDirection: "column", gap: 5 }}>
        {chambres.map(c => (
          <ChambreCard key={c.id} chambre={c} invites={(byChambre[c.id] || []).filter(passesFilters)} totalCount={byChambre[c.id]?.length || 0}
            chambresAll={chambresAll} horsBuckets={horsBuckets} onMove={onMove}/>
        ))}
        {ghostInvites.length > 0 && (
          <div style={{ padding: "5px 6px", background: AD.paperDeep, border: `1px dashed ${AD.ruleSoft}` }}>
            <div style={{ fontFamily: AD.italic, fontStyle: "italic", fontSize: 9, color: AD.inkMute }}>Dans le gîte sans chambre précise :</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 2, marginTop: 3 }}>
              {ghostInvites.filter(passesFilters).map(inv => (
                <InviteCard key={inv.id} inv={inv} chambres={chambresAll} horsBuckets={horsBuckets} onMove={onMove} compact mini/>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ─── Carte d'une chambre (drop target) ───────────────────────────────
function ChambreCard({ chambre, invites, totalCount, chambresAll, horsBuckets, onMove }) {
  const [dragOver, setDragOver] = React.useState(false);
  const capa = Number(chambre.capacite) || 0;
  const overcap = invites.length > capa;
  const isReserved = !!chambre.reserve;

  return (
    <div
      onDragOver={(e) => { if (!isReserved) { e.preventDefault(); setDragOver(true); } }}
      onDragLeave={() => setDragOver(false)}
      onDrop={(e) => {
        e.preventDefault(); setDragOver(false);
        if (isReserved) return;
        const inviteId = e.dataTransfer.getData("text/invite_id");
        if (inviteId) onMove(inviteId, { chambre_id: chambre.id });
      }}
      style={{
        background: isReserved ? AD.orPale : (dragOver ? AD.sagePale : (invites.length === 0 ? AD.paperDeep : AD.white)),
        border: `1px solid ${dragOver ? AD.sage : (overcap ? AD.rouge : AD.ruleSoft)}`,
        padding: "5px 7px",
        transition: "background .12s",
        opacity: isReserved ? 0.85 : 1,
      }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6 }}>
        <div style={{ minWidth: 0 }}>
          <span style={{ fontFamily: AD.mono, fontSize: 9, fontWeight: 700, color: AD.ink, letterSpacing: 1 }}>{chambre.numero}</span>
          <span style={{ fontFamily: AD.sans, fontSize: 10, color: AD.inkSoft, marginLeft: 6 }}>{chambre.type}</span>
        </div>
        <span style={{ fontFamily: AD.mono, fontSize: 8, color: overcap ? AD.rougeDeep : AD.inkMute }}>{invites.length}/{capa}</span>
      </div>
      {isReserved ? (
        <div style={{ marginTop: 4, padding: "3px 6px", background: AD.orPale, fontFamily: AD.italic, fontStyle: "italic", fontSize: 10, color: AD.orDeep }}>
          ★ Réservé · {chambre.reserve}
        </div>
      ) : invites.length === 0 ? (
        <div style={{ marginTop: 4, fontFamily: AD.italic, fontStyle: "italic", fontSize: 10, color: AD.inkMute }}>Glisser ici</div>
      ) : (
        <div style={{ marginTop: 4, display: "flex", flexDirection: "column", gap: 2 }}>
          {invites.map(inv => (
            <InviteCard key={inv.id} inv={inv} chambres={chambresAll} horsBuckets={horsBuckets} onMove={onMove} compact mini/>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── Carte invité draggable ──────────────────────────────────────────
function InviteCard({ inv, chambres, horsBuckets, onMove, compact, mini }) {
  const [menuOpen, setMenuOpen] = React.useState(false);
  const coteColor = (inv.cote || "").toLowerCase().startsWith("e") ? AD.sage : AD.rouge;
  const isPlusOne = inv.categorie === "+1";
  const isEnfant  = inv.categorie === "Enfant";
  const hasPartner = !!inv.partner_id;

  return (
    <div
      draggable
      onDragStart={(e) => { e.dataTransfer.setData("text/invite_id", inv.id); e.dataTransfer.effectAllowed = "move"; }}
      style={{
        background: AD.white,
        border: `1px solid ${AD.ruleSoft}`,
        borderLeft: `3px solid ${coteColor}`,
        padding: mini ? "3px 5px" : "5px 7px",
        cursor: "grab",
        position: "relative",
        fontSize: mini ? 10.5 : 11.5,
      }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 4 }}>
        <div style={{ minWidth: 0, flex: 1, fontFamily: AD.sans, fontWeight: 500, color: AD.ink, lineHeight: 1.2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
          {isPlusOne && <span style={{ color: AD.or, fontFamily: AD.mono, fontSize: 8, marginRight: 3 }}>+1</span>}
          {isEnfant && <span style={{ color: AD.or, fontFamily: AD.mono, fontSize: 8, marginRight: 3 }}>★</span>}
          {inv.prenom} {(inv.nom || "").charAt(0)}{inv.nom ? "." : ""}
          {hasPartner && <span style={{ color: AD.inkMute, marginLeft: 4, fontSize: 9 }}>⚭</span>}
        </div>
        <button onClick={() => setMenuOpen(v => !v)} title="Déplacer vers…"
          style={{ background: "transparent", border: "none", padding: 1, color: AD.inkMute, cursor: "pointer", fontFamily: AD.mono, fontSize: 10, flexShrink: 0 }}>↪</button>
      </div>

      {menuOpen && (
        <div style={{ position: "absolute", top: "100%", right: 0, zIndex: 50, background: AD.white, border: `1px solid ${AD.ruleSoft}`, marginTop: 2, minWidth: 220, maxHeight: 320, overflowY: "auto", boxShadow: "0 4px 14px rgba(0,0,0,0.12)" }}>
          <MenuItem label="Non placé" onClick={() => { onMove(inv.id, { unassigned: true }); setMenuOpen(false); }}/>
          <MenuHeader>Hors château</MenuHeader>
          {horsBuckets.map(b => (
            <MenuItem key={b.id} label={`${b.icon} ${b.label}`} onClick={() => { onMove(inv.id, { gite_id: b.id }); setMenuOpen(false); }}/>
          ))}
          <MenuHeader>Chambres château</MenuHeader>
          {chambres.filter(c => !c.reserve).map(c => (
            <MenuItem key={c.id} label={`${c.numero} · ${c.type}`} onClick={() => { onMove(inv.id, { chambre_id: c.id }); setMenuOpen(false); }}/>
          ))}
        </div>
      )}
    </div>
  );
}

function MenuHeader({ children }) {
  return <div style={{ padding: "4px 8px", fontFamily: AD.mono, fontSize: 8, letterSpacing: 1.5, color: AD.inkMute, textTransform: "uppercase", background: AD.paperDeep }}>{children}</div>;
}
function MenuItem({ label, onClick }) {
  return (
    <button onClick={onClick} style={{
      display: "block", width: "100%", textAlign: "left",
      padding: "5px 10px", background: "transparent", border: "none",
      fontFamily: AD.sans, fontSize: 11, color: AD.ink, cursor: "pointer",
    }}
      onMouseEnter={(e) => { e.currentTarget.style.background = AD.paperDeep; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}>
      {label}
    </button>
  );
}

// ─── Modal de suggestion (placer partner + enfants au même endroit) ──
function SuggestModal({ suggest, chambres, gites, horsBuckets, onConfirm, onCancel }) {
  const { anchor, linked, target } = suggest;
  let targetLabel = "—";
  if (target.chambre_id) {
    const c = chambres.find(x => x.id === target.chambre_id);
    const g = gites.find(x => x.id === c?.gite_id);
    targetLabel = `${g?.nom || ""} · ${c?.numero || ""} (${c?.type || ""})`;
  } else if (target.gite_id) {
    const b = horsBuckets.find(x => x.id === target.gite_id);
    if (b) targetLabel = b.icon + " " + b.label;
    else targetLabel = (gites.find(x => x.id === target.gite_id)?.nom) || target.gite_id;
  }
  return (
    <div onClick={onCancel} style={{
      position: "fixed", inset: 0, background: "rgba(26,22,18,0.55)", zIndex: 1000,
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: AD.white, maxWidth: 480, width: "100%",
        border: `1px solid ${AD.ruleSoft}`, padding: 28,
      }}>
        <div className="eyebrow">★ Suggestion</div>
        <div className="display-bold" style={{ fontSize: 20, marginTop: 4, marginBottom: 12 }}>
          Placer aussi ses proches&nbsp;?
        </div>
        <div style={{ fontFamily: AD.sans, fontSize: 13, color: AD.inkSoft, lineHeight: 1.5 }}>
          <strong>{anchor.prenom} {anchor.nom}</strong> vient d'être placé(e) dans <em>{targetLabel}</em>.
          <br/>Les invités suivants y sont liés et ne sont pas (encore) au même endroit :
        </div>
        <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 6 }}>
          {linked.map(p => (
            <div key={p.id} style={{ padding: "6px 10px", background: AD.paperDeep, fontFamily: AD.sans, fontSize: 12, display: "flex", justifyContent: "space-between" }}>
              <span><strong>{p.prenom} {p.nom}</strong></span>
              <span style={{ fontFamily: AD.mono, fontSize: 9, color: AD.inkMute, letterSpacing: 1 }}>
                {p.categorie === "Enfant" ? "ENFANT" : p.categorie === "+1" ? "+1" : (p.partner_id === anchor.id ? "PARTENAIRE" : "LIÉ")}
              </span>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 22, display: "flex", gap: 8, justifyContent: "flex-end" }}>
          <button className="btn" onClick={onCancel}>Non merci</button>
          <button className="btn gold" onClick={onConfirm}>✓ Placer aussi {linked.length === 1 ? "cette personne" : "ces personnes"}</button>
        </div>
      </div>
    </div>
  );
}

// ─── Modal édition gîte (capa = lits + nom + notes) ──────────────────
function GiteEditModal({ gite, onClose, onSaved }) {
  const [nom, setNom] = React.useState(gite?.nom || "");
  const [lits, setLits] = React.useState(Number(gite?.lits) || 0);
  const [notes, setNotes] = React.useState(gite?.notes || "");
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState("");

  const save = async () => {
    setSaving(true); setErr("");
    const res = await adminUpdateGite(gite.id, { nom: nom.trim(), lits: Number(lits) || 0, notes: notes.trim() });
    setSaving(false);
    if (res?.ok) onSaved?.();
    else setErr(res?.error || "Erreur inconnue");
  };

  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(26,22,18,0.55)", zIndex: 1000,
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: AD.white, maxWidth: 460, width: "100%",
        border: `1px solid ${AD.ruleSoft}`, padding: 28,
      }}>
        <div className="eyebrow">Éditer le gîte</div>
        <div className="display-bold" style={{ fontSize: 22, marginTop: 4, marginBottom: 18 }}>{gite.nom || gite.id}</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <Label l="Nom"><input type="text" value={nom} onChange={(e) => setNom(e.target.value)} style={editInput()}/></Label>
          <Label l="Capacité (lits)"><input type="number" min="0" max="20" value={lits} onChange={(e) => setLits(e.target.value)} style={editInput()}/></Label>
          <Label l="Notes"><textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows="2" style={{ ...editInput(), resize: "vertical", minHeight: 60 }}/></Label>
        </div>
        {err && <div style={{ marginTop: 12, padding: "8px 12px", background: AD.rougePale, color: AD.rougeDeep, fontFamily: AD.mono, fontSize: 11 }}>⚠ {err}</div>}
        <div style={{ marginTop: 22, display: "flex", gap: 8, justifyContent: "flex-end" }}>
          <button className="btn" onClick={onClose} disabled={saving}>Annuler</button>
          <button className="btn gold" onClick={save} disabled={saving}>{saving ? "…" : "✓ Enregistrer"}</button>
        </div>
      </div>
    </div>
  );
}

function Label({ l, children }) {
  return (
    <label>
      <div style={{ fontFamily: AD.mono, fontSize: 10, letterSpacing: 1.5, color: AD.inkSoft, marginBottom: 4, textTransform: "uppercase" }}>{l}</div>
      {children}
    </label>
  );
}
function editInput() {
  return {
    width: "100%", padding: "10px 12px",
    background: AD.paper, border: `1px solid ${AD.ruleSoft}`,
    fontFamily: AD.sans, fontSize: 14, color: AD.ink, outline: "none",
  };
}

Object.assign(window, { GitesPage });
