// Tableau de bord — toile libre : dessin, notes, médias, vignettes de comptes.
//
// POURQUOI : un espace pour poser ce qu'on compte faire, tester des concepts et GARDER LA TRACE
// de ce qui a été testé (capture d'écran + note à côté) — pour ne jamais refaire deux fois la
// même expérience en ayant oublié le résultat.
//
// Choix d'interaction (demandés explicitement) :
//   · déplacement = CLIC MOLETTE maintenu (jamais le clic gauche) -> aucun conflit possible
//   · clic gauche = dessin, et UNIQUEMENT quand le mode dessin est activé ; sinon il déplace
//     les blocs. Séparer les deux évite les gestes ambigus.
//
// Persistance : les toiles vont en kv backend (clé dashboard_boards), partagée entre Mac et
// Windows par kvsync. Les médias ne sont PAS embarqués dans ce JSON (il deviendrait énorme) :
// ils sont déposés dans la médiathèque, donc suivis par la synchro Backblaze, et la toile n'en
// garde que le chemin.

const DASH_DIR = "Tableau de bord";
const DASH_COLORS = ["#f85149", "#f0a020", "#3fb950", "#58a6ff", "#c9a9ff", "#ffffff"];
const dashId = () => Math.random().toString(36).slice(2, 9);

function dashNewBoard(name) {
  return { id: dashId(), name: name || "Sans titre", strokes: [], blocks: [], created: Date.now() };
}

function Dashboard({ onGo }) {
  const { useState, useEffect, useRef, useCallback } = React;
  const [boards, setBoards] = useState(null);      // null = chargement
  const [curId, setCurId] = useState(null);
  const [adding, setAdding] = useState(false);
  const [newName, setNewName] = useState("");
  const [drawOn, setDrawOn] = useState(false);
  const [color, setColor] = useState(DASH_COLORS[3]);
  const [erase, setErase] = useState(false);
  const [view, setView] = useState({ x: 0, y: 0, z: 1 });
  const [pickAcc, setPickAcc] = useState(false);
  const [saving, setSaving] = useState(false);
  const wrapRef = useRef(null);
  const strokeRef = useRef(null);                  // trait en cours de tracé
  const panRef = useRef(null);                     // pan molette en cours
  // REF et non state : les premiers mousemove arrivent AVANT le re-rendu, un state serait
  // encore a null et les premiers pixels du deplacement seraient perdus (meme piege que le trace).
  const dragRef = useRef(null);                    // bloc en cours de deplacement
  const resizeRef = useRef(null);                  // bloc en cours de redimension
  // Fil en cours de tirage depuis une poignee. REF pour les gestionnaires de souris,
  // state pour dessiner l'apercu (meme raison que le reste : la closure serait perimee).
  const wireRef = useRef(null);                    // { from, side }
  const [wire, setWire] = useState(null);          // { from, side, x, y } -> apercu
  const saveT = useRef(null);
  const fileRef = useRef(null);

  // ── chargement ───────────────────────────────────────────────────────────────
  useEffect(() => {
    (async () => {
      let b = null;
      try { b = window.Backend.getDashboard ? await window.Backend.getDashboard() : null; } catch (e) {}
      if (!b || !b.length) b = [dashNewBoard("Général")];
      setBoards(b); setCurId(b[0].id);
    })();
  }, []);

  // ── modifications ────────────────────────────────────────────────────────────
  // TOUJOURS en forme fonctionnelle : pendant une salve de mousemove, React n'a pas encore
  // re-rendu, donc lire `boards` du rendu courant renverrait un état périmé et écraserait le
  // trait ajouté au mousedown. C'est exactement le bug constaté (0 trait enregistré).
  const dirty = useRef(false);
  const persist = useCallback((updater) => {
    dirty.current = true;
    setBoards(prev => (typeof updater === "function" ? updater(prev) : updater));
  }, []);

  const cur = boards && boards.find(b => b.id === curId);
  const patchCur = (fn) => persist(prev => (prev || []).map(b => b.id === curId ? fn(b) : b));

  // sauvegarde différée : 700 ms après la dernière modification, jamais à chaque point tracé
  useEffect(() => {
    if (!boards || !dirty.current) return;
    if (saveT.current) clearTimeout(saveT.current);
    saveT.current = setTimeout(async () => {
      setSaving(true);
      await window.Backend.saveDashboard(boards);
      setSaving(false);
    }, 700);
    return () => { if (saveT.current) clearTimeout(saveT.current); };
  }, [boards]);

  // ── conversion écran -> coordonnées de la toile ───────────────────────────────
  const toWorld = (e) => {
    const r = wrapRef.current.getBoundingClientRect();
    return { x: (e.clientX - r.left - view.x) / view.z, y: (e.clientY - r.top - view.y) / view.z };
  };

  // ── sélection ────────────────────────────────────────────────────────────────
  // `sel` sert au rendu (contour), `selRef` aux gestionnaires de souris : ceux-ci lisent la
  // closure du rendu, qui serait périmée pendant une salve de mousemove.
  const [sel, setSelState] = useState([]);
  const selRef = useRef([]);
  const setSel = (ids) => { selRef.current = ids; setSelState(ids); };
  const [band, setBand] = useState(null);        // rectangle de sélection en cours
  const bandRef = useRef(null);

  // groupe : cliquer un bloc groupé sélectionne tout son groupe
  const expandSel = (blocks, ids) => {
    const gs = new Set(blocks.filter(k => ids.indexOf(k.id) >= 0 && k.g).map(k => k.g));
    return gs.size ? blocks.filter(k => ids.indexOf(k.id) >= 0 || (k.g && gs.has(k.g))).map(k => k.id) : ids;
  };

  // ── souris ────────────────────────────────────────────────────────────────────
  const onDown = (e) => {
    if (e.button === 1) {                    // CLIC MOLETTE = déplacement de la toile
      e.preventDefault();
      panRef.current = { sx: e.clientX, sy: e.clientY, ox: view.x, oy: view.y };
      return;
    }
    if (e.button !== 0) return;
    const p = toWorld(e);
    if (!drawOn) {
      // clic gauche sur le vide, hors mode dessin -> rectangle de sélection
      bandRef.current = { x0: p.x, y0: p.y, x1: p.x, y1: p.y, add: e.shiftKey };
      setBand(bandRef.current);
      if (!e.shiftKey) setSel([]);
      return;
    }
    if (erase) { patchCur(b => ({ ...b, strokes: b.strokes.filter(s => !dashHit(s, p)) })); return; }
    strokeRef.current = { id: dashId(), color, w: 3, pts: [[p.x, p.y]] };
    patchCur(b => ({ ...b, strokes: b.strokes.concat(strokeRef.current) }));
  };
  const onMove = (e) => {
    if (panRef.current) {
      const d = panRef.current;
      setView(v => ({ ...v, x: d.ox + (e.clientX - d.sx), y: d.oy + (e.clientY - d.sy) }));
      return;
    }
    if (resizeRef.current) {
      const d = resizeRef.current, p = toWorld(e);
      const w = Math.max(90, Math.round(d.w0 + (p.x - d.ox)));
      patchCur(b => ({ ...b, blocks: b.blocks.map(k => k.id !== d.id ? k
        // media : la hauteur suit le ratio, sinon on redimensionne librement
        : { ...k, w, h: k.ar ? Math.round(w / k.ar) + DASH_HEAD : Math.max(70, Math.round(d.h0 + (p.y - d.oy))) }) }));
      return;
    }
    if (dragRef.current) {
      const d = dragRef.current, p = toWorld(e);
      const dx = p.x - d.ox, dy = p.y - d.oy;                 // déplacement depuis le point de prise
      patchCur(b => ({ ...b, blocks: b.blocks.map(k => {
        const o = d.start[k.id];
        return o ? { ...k, x: o.x + dx, y: o.y + dy } : k;    // toute la sélection bouge ensemble
      }) }));
      return;
    }
    if (wireRef.current) {
      const p = toWorld(e);
      setWire(w => (w ? { ...w, x: p.x, y: p.y } : w));
      return;
    }
    if (bandRef.current) {
      const p = toWorld(e);
      bandRef.current = { ...bandRef.current, x1: p.x, y1: p.y };
      setBand(bandRef.current);
      return;
    }
    if (!strokeRef.current) return;
    const p = toWorld(e);
    const st = strokeRef.current;
    st.pts.push([p.x, p.y]);
    patchCur(b => ({ ...b, strokes: b.strokes.map(s => s.id === st.id ? { ...s, pts: st.pts.slice() } : s) }));
  };
  const onUp = () => {
    // relache dans le vide -> le fil est abandonne (le lien se cree dans onPortUp du bloc cible)
    if (wireRef.current) { wireRef.current = null; setWire(null); }
    if (bandRef.current && cur) {
      const b = bandRef.current;
      const x0 = Math.min(b.x0, b.x1), x1 = Math.max(b.x0, b.x1);
      const y0 = Math.min(b.y0, b.y1), y1 = Math.max(b.y0, b.y1);
      // un simple clic (rectangle minuscule) ne sélectionne rien : il désélectionne
      if (Math.abs(x1 - x0) > 4 || Math.abs(y1 - y0) > 4) {
        const hit = (cur.blocks || []).filter(k => k.x < x1 && k.x + (k.w || 0) > x0 && k.y < y1 && k.y + (k.h || 0) > y0).map(k => k.id);
        const next = b.add ? Array.from(new Set(selRef.current.concat(hit))) : hit;
        setSel(expandSel(cur.blocks || [], next));
      }
    }
    bandRef.current = null; setBand(null);
    panRef.current = null; strokeRef.current = null; dragRef.current = null; resizeRef.current = null;
  };
  const onWheel = (e) => {
    e.preventDefault();
    const r = wrapRef.current.getBoundingClientRect();
    const mx = e.clientX - r.left, my = e.clientY - r.top;
    setView(v => {
      const z = Math.min(3, Math.max(0.2, v.z * (e.deltaY < 0 ? 1.12 : 0.89)));
      // zoom centré sur le curseur : le point sous la souris ne bouge pas
      return { z, x: mx - (mx - v.x) * (z / v.z), y: my - (my - v.y) * (z / v.z) };
    });
  };

  // ── blocs ────────────────────────────────────────────────────────────────────
  const addBlock = (blk) => {
    const r = wrapRef.current.getBoundingClientRect();
    const c = { x: (r.width / 2 - view.x) / view.z, y: (r.height / 2 - view.y) / view.z };
    patchCur(b => {
      // Décalage en cascade : sans lui, coller 5 captures d'affilée les empile EXACTEMENT au
      // même endroit et on croit n'en avoir collé qu'une.
      const n = (b.blocks || []).length % 8;
      return { ...b, blocks: b.blocks.concat({ id: dashId(), x: c.x - 90 + n * 26, y: c.y - 50 + n * 22, w: 200, h: 110, ...blk }) };
    });
  };
  // Liens entre blocs : un trait qui dit « ceci correspond a cela ». Les liens dont un bout
  // disparait sont nettoyes a la suppression du bloc.
  const addLink = (a, aSide, b, bSide) => patchCur(bd => {
    const links = bd.links || [];
    if (links.some(l => (l.a === a && l.b === b) || (l.a === b && l.b === a))) return bd;
    return { ...bd, links: links.concat({ id: dashId(), a, aSide, b, bSide }) };
  });
  const delLink = (id) => patchCur(bd => ({ ...bd, links: (bd.links || []).filter(l => l.id !== id) }));
  const delBlock = (id) => patchCur(b => ({ ...b, blocks: b.blocks.filter(k => k.id !== id), links: (b.links || []).filter(l => l.a !== id && l.b !== id) }));
  const setBlock = (id, patch) => patchCur(b => ({ ...b, blocks: b.blocks.map(k => k.id === id ? { ...k, ...patch } : k) }));

  // médias : déposés dans la médiathèque -> la toile ne garde que le chemin
  const upload = async (files) => {
    for (const f of Array.from(files || [])) {
      const dir = DASH_DIR + "/" + curId;
      // Nom TOUJOURS rendu unique : une capture collee s'appelle "image.png" a chaque fois et
      // ecraserait la precedente. On garde le nom d'origine pour l'affichage.
      const orig = f.name || (String(f.type).startsWith("video") ? "video.mp4" : "capture.png");
      const dot = orig.lastIndexOf(".");
      const ext = dot > 0 ? orig.slice(dot) : (String(f.type).startsWith("video") ? ".mp4" : ".png");
      const stored = (dot > 0 ? orig.slice(0, dot) : orig).slice(0, 32) + "-" + dashId() + ext;
      let file = f;
      try { file = new File([f], stored, { type: f.type }); } catch (e) { }
      const r = await window.Backend.libUpload(dir, file);
      if (!r) continue;
      const img = /\.(png|jpe?g|gif|webp|bmp)$/i.test(ext);
      addBlock({ type: img ? "image" : "video", path: dir + "/" + stored, name: orig, w: 260, h: 300 });
    }
  };
  const onDrop = (e) => { e.preventDefault(); if (e.dataTransfer.files && e.dataTransfer.files.length) upload(e.dataTransfer.files); };

  // Ctrl+V : capture d'ecran ou video depuis le presse-papiers, directement sur la toile.
  useEffect(() => {
    const onPaste = (e) => {
      const items = (e.clipboardData && e.clipboardData.items) || [];
      const files = [];
      for (const it of items) {
        if (it.kind === "file") { const f = it.getAsFile(); if (f) files.push(f); }
      }
      if (!files.length) return;            // texte colle dans une note : on laisse passer
      e.preventDefault();
      upload(files);
    };
    document.addEventListener("paste", onPaste);
    return () => document.removeEventListener("paste", onPaste);
  }, [curId, view.x, view.y, view.z]);

  // ── actions sur la selection ─────────────────────────────────────────────────
  // Recuperer un media pose sur la toile : l'operation inverse du collage.
  const saveSel = () => {
    const ids = selRef.current;
    const items = (cur.blocks || []).filter(k => ids.indexOf(k.id) >= 0 && k.path);
    if (!items.length) { window.alert("Sélectionne d'abord une image ou une vidéo sur la toile."); return; }
    items.forEach(k => {
      const a = document.createElement("a");
      a.href = window.Backend.libFileUrl(k.path);
      a.download = k.name || "media";
      document.body.appendChild(a); a.click(); a.remove();
    });
  };
  const groupSel = () => {
    if (selRef.current.length < 2) return;
    const g = dashId();
    patchCur(b => ({ ...b, blocks: b.blocks.map(k => selRef.current.indexOf(k.id) >= 0 ? { ...k, g } : k) }));
  };
  const ungroupSel = () => patchCur(b => ({ ...b, blocks: b.blocks.map(k => selRef.current.indexOf(k.id) >= 0 ? { ...k, g: null } : k) }));
  const delSel = () => {
    const ids = selRef.current;
    if (!ids.length) return;
    if (!window.confirm("Retirer " + ids.length + " élément(s) de la toile ?\n\nLes médias restent dans la médiathèque.")) return;
    patchCur(b => ({ ...b, blocks: b.blocks.filter(k => ids.indexOf(k.id) < 0) }));
    setSel([]);
  };

  if (!boards) return <div style={{ padding: 40, color: "var(--muted)" }}>Chargement…</div>;

  const tBtn = (on, extra) => ({
    padding: "6px 11px", borderRadius: 8, fontSize: 12, fontWeight: 700, cursor: "pointer",
    background: on ? "var(--accent-soft)" : "var(--surface-2)",
    border: "1px solid " + (on ? "var(--accent-line)" : "var(--border)"),
    color: on ? "var(--accent)" : "var(--muted)", ...(extra || {}),
  });

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "calc(100vh - 90px)" }}>
      {/* ── bulles : une par toile ── */}
      <div style={{ display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap", marginBottom: 9 }}>
        {boards.map(b => (
          <div key={b.id} onClick={() => setCurId(b.id)} title={b.name}
            style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 12px", borderRadius: 99, cursor: "pointer", fontSize: 12.5, fontWeight: 700, maxWidth: 240,
              background: b.id === curId ? "var(--accent-soft)" : "var(--surface-2)",
              border: "1px solid " + (b.id === curId ? "var(--accent-line)" : "var(--border)"),
              color: b.id === curId ? "var(--accent)" : "var(--muted)" }}>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{b.name}</span>
            {boards.length > 1 && (
              <span onClick={e => { e.stopPropagation();
                if (!window.confirm("Supprimer la toile « " + b.name + " » ?\n\nSon dessin, ses notes et ses blocs seront perdus.\nLes médias déposés restent dans la médiathèque.")) return;
                const next = boards.filter(x => x.id !== b.id);
                persist(next); if (curId === b.id) setCurId(next[0].id);   // tableau direct : hors salve, pas de course
              }} title="Supprimer cette toile" style={{ opacity: .55, fontSize: 12 }}>✕</span>
            )}
          </div>
        ))}
        {adding
          ? <input autoFocus value={newName} onChange={e => setNewName(e.target.value)}
              onKeyDown={e => {
                if (e.key === "Escape") { setAdding(false); setNewName(""); }
                if (e.key === "Enter") {
                  const nb = dashNewBoard(newName.trim() || "Sans titre");
                  persist(prev => (prev || []).concat(nb)); setCurId(nb.id); setAdding(false); setNewName("");
                }
              }}
              onBlur={() => { setAdding(false); setNewName(""); }}
              placeholder="Nom de la toile puis Entrée"
              style={{ padding: "6px 12px", borderRadius: 99, border: "1px solid var(--accent-line)", background: "var(--surface)", color: "var(--text)", fontSize: 12.5, width: 210, outline: "none" }} />
          : <button onClick={() => setAdding(true)} style={tBtn(false, { borderRadius: 99 })}>+ Nouvelle toile</button>}
        <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--faint)" }}>
          {saving ? "Enregistrement…" : "Enregistré"} · molette = déplacer, roulette = zoom
        </span>
      </div>

      {/* ── barre d'outils ── */}
      <div style={{ display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap", marginBottom: 9 }}>
        <button onClick={() => { setDrawOn(d => !d); setErase(false); }} style={tBtn(drawOn)}>✏️ Dessin {drawOn ? "ON" : "OFF"}</button>
        <button onClick={() => { setErase(e => !e); setDrawOn(true); }} disabled={!drawOn && !erase} style={tBtn(erase)}>🧽 Gomme</button>
        {DASH_COLORS.map(c => (
          <span key={c} onClick={() => { setColor(c); setErase(false); setDrawOn(true); }} title={c}
            style={{ width: 18, height: 18, borderRadius: 99, background: c, cursor: "pointer", border: color === c ? "2px solid var(--text)" : "1px solid var(--border)" }} />
        ))}
        <span style={{ width: 1, height: 20, background: "var(--border)", margin: "0 3px" }} />
        <button onClick={() => addBlock({ type: "title", text: "", w: 340, h: 64 })} style={tBtn(false)} title="Un titre pour delimiter une section de la toile">🔠 Titre</button>
        <button onClick={() => addBlock({ type: "note", text: "", w: 220, h: 130 })} style={tBtn(false)}>📝 Note</button>
        <button onClick={() => fileRef.current && fileRef.current.click()} style={tBtn(false)}>🖼 Image / 🎬 Vidéo</button>
        <button onClick={() => setPickAcc(true)} style={tBtn(false)}>👤 Compte</button>
        <input ref={fileRef} type="file" multiple accept="image/*,video/*" style={{ display: "none" }}
          onChange={e => { upload(e.target.files); e.target.value = ""; }} />
        {sel.length > 0 && (
          <>
            <span style={{ width: 1, height: 20, background: "var(--border)", margin: "0 3px" }} />
            <span style={{ fontSize: 11.5, fontWeight: 800, color: "var(--accent)" }}>{sel.length} sélectionné{sel.length > 1 ? "s" : ""}</span>
            <button onClick={saveSel} title="Télécharger les médias sélectionnés" style={tBtn(false)}>⬇ Récupérer</button>
            <button onClick={groupSel} disabled={sel.length < 2} title="Lier ces éléments : ils se déplaceront ensemble" style={tBtn(false)}>🔗 Grouper</button>
            <button onClick={ungroupSel} style={tBtn(false)}>⛓ Dégrouper</button>
            <button onClick={delSel} style={tBtn(false, { color: "var(--danger,#ff5c6c)", borderColor: "rgba(255,92,108,.45)" })}>🗑 Retirer</button>
          </>
        )}
        <button onClick={() => setView({ x: 0, y: 0, z: 1 })} style={tBtn(false, { marginLeft: "auto" })}>⌖ Recentrer</button>
        <span style={{ fontSize: 11, color: "var(--faint)" }}>{Math.round(view.z * 100)} %</span>
      </div>

      {/* ── la toile ── */}
      <div ref={wrapRef} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}
        onWheel={onWheel} onDrop={onDrop} onDragOver={e => e.preventDefault()}
        onContextMenu={e => e.preventDefault()}
        style={{ position: "relative", flex: 1, minHeight: 380, overflow: "hidden", borderRadius: 12,
          border: "1px solid var(--border)", background: "var(--surface)",
          backgroundImage: "radial-gradient(var(--border) 1px, transparent 1px)",
          backgroundSize: (22 * view.z) + "px " + (22 * view.z) + "px",
          backgroundPosition: view.x + "px " + view.y + "px",
          cursor: drawOn ? (erase ? "cell" : "crosshair") : "default" }}>
        <div style={{ position: "absolute", left: 0, top: 0, transform: `translate(${view.x}px, ${view.y}px) scale(${view.z})`, transformOrigin: "0 0" }}>
          <svg width="8000" height="8000" style={{ position: "absolute", left: -4000, top: -4000, pointerEvents: "none", overflow: "visible" }}>
            <g transform="translate(4000,4000)">
              {(cur.strokes || []).map(s => (
                <polyline key={s.id} points={s.pts.map(p => p[0] + "," + p[1]).join(" ")}
                  fill="none" stroke={s.color} strokeWidth={s.w} strokeLinecap="round" strokeLinejoin="round" />
              ))}
            </g>
          </svg>
          {/* liens entre blocs — SVG distinct : celui des traits ignore la souris */}
          <svg width="8000" height="8000" style={{ position: "absolute", left: -4000, top: -4000, overflow: "visible", pointerEvents: "none" }}>
            <g transform="translate(4000,4000)">
              {(cur.links || []).map(l => {
                const A = (cur.blocks || []).find(k => k.id === l.a), B = (cur.blocks || []).find(k => k.id === l.b);
                if (!A || !B) return null;
                // `aSide`/`bSide` absents = lien cree avant les poignees -> droite vers gauche
                const a = dashPort(A, l.aSide || "r"), b = dashPort(B, l.bSide || "l");
                const d = dashWire(a, b);
                return (
                  <g key={l.id}>
                    <path d={d} fill="none" stroke="var(--accent)" strokeWidth="2" opacity=".8" />
                    <path d={d} fill="none" stroke="transparent" strokeWidth="16"
                      style={{ pointerEvents: drawOn ? "none" : "stroke", cursor: "pointer" }}
                      onClick={() => { if (window.confirm("Supprimer ce lien ?")) delLink(l.id); }}>
                      <title>Cliquer pour supprimer ce lien</title>
                    </path>
                  </g>
                );
              })}
              {wire && (() => {
                const A = (cur.blocks || []).find(k => k.id === wire.from);
                if (!A) return null;
                return <path d={dashWire(dashPort(A, wire.side), { x: wire.x, y: wire.y })}
                  fill="none" stroke="var(--accent)" strokeWidth="2" strokeDasharray="5 4" opacity=".9" />;
              })()}
            </g>
          </svg>
          {band && Math.abs(band.x1 - band.x0) > 2 && (
            <div style={{ position: "absolute", pointerEvents: "none",
              left: Math.min(band.x0, band.x1), top: Math.min(band.y0, band.y1),
              width: Math.abs(band.x1 - band.x0), height: Math.abs(band.y1 - band.y0),
              border: "1px dashed var(--accent)", background: "rgba(88,166,255,.08)", borderRadius: 4 }} />
          )}
          {(cur.blocks || []).map(k => (
            <DashBlock key={k.id} blk={k} drawOn={drawOn} selected={sel.indexOf(k.id) >= 0}
              onGrab={(e) => {
                const p = toWorld(e);
                const all = cur.blocks || [];
                // prendre un bloc hors selection bascule dessus ; Maj ajoute a la selection
                let ids = selRef.current.indexOf(k.id) >= 0 ? selRef.current.slice() : expandSel(all, [k.id]);
                if (e.shiftKey) ids = Array.from(new Set(selRef.current.concat(expandSel(all, [k.id]))));
                setSel(ids);
                const start = {};
                all.forEach(x => { if (ids.indexOf(x.id) >= 0) start[x.id] = { x: x.x, y: x.y }; });
                dragRef.current = { id: k.id, ox: p.x, oy: p.y, start };
              }}
              onDel={() => delBlock(k.id)} onSet={(patch) => setBlock(k.id, patch)} onGo={onGo}
              wiring={!!wire}
              onPortDown={(side, e) => {
                const p = toWorld(e);
                wireRef.current = { from: k.id, side };
                setWire({ from: k.id, side, x: p.x, y: p.y });
              }}
              onPortUp={(side) => {
                const w = wireRef.current;
                if (!w || w.from === k.id) return;          // un bloc ne se relie pas a lui-meme
                addLink(w.from, w.side, k.id, side);
                wireRef.current = null; setWire(null);
              }}
              onResizeStart={(e, mode) => {
                if (mode === "fit") { setBlock(k.id, { w: 260, h: k.ar ? Math.round(260 / k.ar) + DASH_HEAD : 300 }); return; }
                const p = toWorld(e);
                resizeRef.current = { id: k.id, ox: p.x, oy: p.y, w0: k.w, h0: k.h };
              }} />
          ))}
        </div>
        {(cur.strokes || []).length === 0 && (cur.blocks || []).length === 0 && (
          <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", color: "var(--faint)", fontSize: 12.5, textAlign: "center", lineHeight: 1.8 }}>
            Toile vide.<br />Active ✏️ <b>Dessin</b> pour tracer, ou pose une note, une capture, une vidéo, un compte.<br />
            <span style={{ fontSize: 11 }}>Clic molette maintenu pour te déplacer · roulette pour zoomer · <b>Ctrl+V</b> pour coller une capture · glisse-dépose tes fichiers ici</span>
          </div>
        )}
      </div>

      {pickAcc && <DashAccountPicker onPick={(a) => { addBlock({ type: "account", acc: a, w: 230, h: 86 }); setPickAcc(false); }} onClose={() => setPickAcc(false)} />}
    </div>
  );
}

// un trait est « touché » par la gomme si un de ses points est à moins de 12 px du curseur
function dashHit(s, p) {
  return (s.pts || []).some(q => Math.abs(q[0] - p.x) < 12 && Math.abs(q[1] - p.y) < 12);
}

const DASH_HEAD = 25;          // hauteur de la barre de titre d'un bloc

// Position d'une poignee (monde) et courbe de liaison entre deux points.
const dashPort = (k, side) => ({ x: side === "l" ? k.x : k.x + (k.w || 0), y: k.y + (k.h || 60) / 2 });
const dashWire = (a, b) => {
  const dx = Math.max(35, Math.abs(b.x - a.x) * 0.45);   // courbure horizontale, style cable
  return "M" + a.x + "," + a.y + " C" + (a.x + dx) + "," + a.y + " " + (b.x - dx) + "," + b.y + " " + b.x + "," + b.y;
};

function DashBlock({ blk, drawOn, selected, wiring, onGrab, onDel, onSet, onResizeStart, onPortDown, onPortUp, onGo }) {
  const { useState } = React;
  const [menu, setMenu] = useState(false);
  const [ren, setRen] = useState(null);
  const media = blk.type === "image" || blk.type === "video";

  const stop = e => e.stopPropagation();
  const outer = {
    // hauteur TOUJOURS explicite : les poignees de liaison s'ancrent au milieu du bloc,
    // une hauteur « auto » les placerait a cote du bord reel.
    position: "absolute", left: blk.x, top: blk.y, width: blk.w, height: blk.h,
    borderRadius: 10, background: "var(--surface-2)",
    border: "1px solid var(--border)",
    boxShadow: selected ? "0 0 0 2px var(--accent)" : "0 2px 10px rgba(0,0,0,.25)",
    // en mode dessin les blocs sont transparents a la souris : le trait passe par-dessus
    pointerEvents: drawOn ? "none" : "auto",
  };

  const title = blk.name || (blk.type === "note" ? "note" : blk.type === "account" ? "compte" : blk.type === "title" ? "titre" : blk.type);
  const head = (
    <div onMouseDown={e => { if (e.button !== 0) return; e.stopPropagation(); onGrab(e); }}
      style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 7px", cursor: "grab",
        // un titre de section ne doit pas ressembler a une boite : bandeau transparent
        background: blk.type === "title" ? "transparent" : "var(--surface-3)",
        borderBottom: blk.type === "title" ? "none" : "1px solid var(--border)", fontSize: 10.5, color: "var(--faint)",
        borderRadius: "9px 9px 0 0" }}>
      {ren === null
        ? <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{title}</span>
        : <input autoFocus value={ren} onChange={e => setRen(e.target.value)} onMouseDown={stop}
            onKeyDown={e => { if (e.key === "Enter") { onSet({ name: ren.trim() || title }); setRen(null); } if (e.key === "Escape") setRen(null); }}
            onBlur={() => { onSet({ name: (ren || "").trim() || title }); setRen(null); }}
            style={{ flex: 1, minWidth: 0, background: "var(--surface)", border: "1px solid var(--accent-line)", borderRadius: 5, color: "var(--text)", fontSize: 10.5, padding: "1px 5px", outline: "none" }} />}
      <span onMouseDown={stop} onClick={e => { e.stopPropagation(); setMenu(m => !m); }} title="Options" style={{ cursor: "pointer", padding: "0 3px", fontWeight: 800 }}>⋮</span>
      <span onMouseDown={stop} onClick={e => { e.stopPropagation(); onDel(); }} title="Retirer de la toile" style={{ cursor: "pointer" }}>✕</span>
    </div>
  );

  const menuBox = menu && (
    <div onMouseDown={stop} onClick={stop}
      style={{ position: "absolute", right: 4, top: DASH_HEAD + 2, zIndex: 5, minWidth: 150, padding: 4,
        background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 9, boxShadow: "0 6px 20px rgba(0,0,0,.45)" }}>
      <div onClick={() => { setRen(title); setMenu(false); }} style={dashMenuItem}>✏️ Renommer</div>
      {blk.path && <a href={window.Backend.libFileUrl(blk.path)} download={blk.name || "media"} onClick={() => setMenu(false)}
        style={{ ...dashMenuItem, display: "block", textDecoration: "none", color: "var(--text)" }}>⬇ Récupérer le fichier</a>}
      {media && <div onClick={() => { onResizeStart(null, "fit"); setMenu(false); }} style={dashMenuItem}>⤢ Taille d'origine</div>}
      {blk.type === "account" && blk.acc && blk.acc.username && (
        <a href={"https://www.instagram.com/" + blk.acc.username + "/"} target="_blank" rel="noopener" onClick={() => setMenu(false)}
          style={{ ...dashMenuItem, display: "block", textDecoration: "none", color: "var(--text)" }}>📸 Ouvrir sur Instagram</a>
      )}
      {blk.type === "account" && onGo && (
        <div onClick={() => { setMenu(false); onGo("overview_instagram"); }} style={dashMenuItem}>🧭 Voir dans Overview Insta</div>
      )}
      <div onClick={() => { setMenu(false); onDel(); }} style={{ ...dashMenuItem, color: "var(--danger,#ff5c6c)" }}>🗑 Retirer de la toile</div>
    </div>
  );

  // Poignees de liaison : on tire depuis l'une et on relache sur celle d'un autre bloc.
  const port = (side) => (
    <div onMouseDown={e => { if (e.button !== 0) return; e.stopPropagation(); onPortDown(side, e); }}
      onMouseUp={e => { e.stopPropagation(); onPortUp(side); }}
      title="Tire un fil vers un autre bloc"
      style={{ position: "absolute", top: "50%", [side === "l" ? "left" : "right"]: -7, marginTop: -6,
        width: 12, height: 12, borderRadius: 99, cursor: "crosshair", zIndex: 4,
        background: wiring ? "var(--accent)" : "var(--surface)",
        border: "2px solid var(--accent)", opacity: wiring || selected ? 1 : .55 }} />
  );

  const grip = (
    <div onMouseDown={e => { if (e.button !== 0) return; e.stopPropagation(); onResizeStart(e); }}
      title="Redimensionner"
      style={{ position: "absolute", right: -3, bottom: -3, width: 14, height: 14, cursor: "nwse-resize",
        borderRight: "2px solid var(--accent)", borderBottom: "2px solid var(--accent)", borderRadius: "0 0 8px 0", opacity: selected ? 1 : .45 }} />
  );

  const inner = { width: "100%", height: "calc(100% - " + DASH_HEAD + "px)", display: "block" };

  if (blk.type === "title") {
    // Titre de section : volontairement SANS cadre ni fond, sinon il ressemble a un bloc de
    // contenu au lieu de coiffer une zone de la toile. La taille du texte suit la hauteur,
    // donc la poignee de redimension sert aussi de reglage de taille.
    const fs = Math.max(13, Math.min(56, (blk.h - DASH_HEAD) * 0.62));
    return (
      <div style={{ ...outer, background: "transparent", border: "1px dashed " + (selected ? "var(--accent)" : "transparent"), boxShadow: "none" }}>
        {head}{menuBox}
        <textarea value={blk.text || ""} onChange={e => onSet({ text: e.target.value })} onMouseDown={stop}
          placeholder="Titre de section…"
          style={{ ...inner, border: "none", outline: "none", resize: "none", background: "transparent",
            color: "var(--text)", fontSize: fs, lineHeight: 1.15, fontWeight: 800, letterSpacing: "-.01em",
            padding: "0 4px", fontFamily: "inherit", boxSizing: "border-box", overflow: "hidden" }} />
        {grip}{port("l")}{port("r")}
      </div>
    );
  }
  if (blk.type === "note") {
    return (
      <div style={{ ...outer, background: "#3a2f0b", borderColor: "#7a6410" }}>
        {head}{menuBox}
        <textarea value={blk.text || ""} onChange={e => onSet({ text: e.target.value })} onMouseDown={stop}
          placeholder="Écris ici…"
          style={{ ...inner, border: "none", outline: "none", resize: "none", background: "transparent", color: "#fdf3c8", fontSize: 12.5, padding: "7px 9px", fontFamily: "inherit", boxSizing: "border-box" }} />
        {grip}{port("l")}{port("r")}
      </div>
    );
  }
  if (media) {
    // La hauteur suit le RATIO REEL du media : avant, tout tenait dans une boite fixe 260x300
    // et une capture verticale s'affichait minuscule entre deux enormes bandes noires.
    const common = { ...inner, objectFit: "contain", background: "#000", borderRadius: "0 0 9px 9px" };
    // Le ratio est signale par onLoad ET par la ref : une image deja en cache (ou en data-URL)
    // termine son chargement AVANT que React n'attache onLoad, et le bloc restait alors a sa
    // taille par defaut avec d'enormes bandes noires.
    const report = (w0, h0) => {
      if (!w0 || !h0) return;
      const ar = w0 / h0;
      if (blk.ar && Math.abs(blk.ar - ar) < 0.001) return;
      onSet({ ar, h: Math.round((blk.w || 260) / ar) + DASH_HEAD });
    };
    return (
      <div style={outer}>
        {head}{menuBox}
        {blk.type === "image"
          ? <img src={window.Backend.libFileUrl(blk.path)} alt="" draggable={false} style={common}
              ref={n => { if (n && n.complete) report(n.naturalWidth, n.naturalHeight); }}
              onLoad={e => report(e.currentTarget.naturalWidth, e.currentTarget.naturalHeight)} />
          : <video src={window.Backend.libFileUrl(blk.path)} controls onMouseDown={stop} style={common}
              ref={n => { if (n && n.readyState >= 1) report(n.videoWidth, n.videoHeight); }}
              onLoadedMetadata={e => report(e.currentTarget.videoWidth, e.currentTarget.videoHeight)} />}
        {grip}{port("l")}{port("r")}
      </div>
    );
  }
  // vignette de compte
  const a = blk.acc || {};
  return (
    <div style={outer}>
      {head}{menuBox}
      {/* le corps de la vignette ouvre le profil : « aller voir le compte » en un clic.
          Le deplacement et la selection restent sur la barre de titre, donc aucun conflit. */}
      <div onMouseDown={stop}
        onClick={() => { if (a.username) window.open("https://www.instagram.com/" + a.username + "/", "_blank", "noopener"); }}
        title={a.username ? "Ouvrir @" + a.username + " sur Instagram" : ""}
        style={{ display: "flex", alignItems: "center", gap: 9, padding: "9px 10px", cursor: a.username ? "pointer" : "default" }}>
        {a.pp
          ? <img src={window.Backend.mediaThumbUrl(a.pp)} alt="" style={{ width: 38, height: 38, borderRadius: 99, objectFit: "cover", flex: "none" }} onError={e => { e.currentTarget.style.display = "none"; }} />
          : <span style={{ width: 38, height: 38, borderRadius: 99, background: "var(--surface-3)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 15, flex: "none" }}>👤</span>}
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 12.5, fontWeight: 800, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>@{a.username}</div>
          <div style={{ fontSize: 10.5, color: "var(--faint)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
            {a.category || "sans catégorie"}{a.place ? " · " + a.place : ""}
          </div>
        </div>
      </div>
      {grip}{port("l")}{port("r")}
    </div>
  );
}

const dashMenuItem = { padding: "6px 9px", borderRadius: 7, fontSize: 12, cursor: "pointer", whiteSpace: "nowrap" };

function DashAccountPicker({ onPick, onClose }) {
  const { useState, useEffect } = React;
  const [q, setQ] = useState("");
  const [rows, setRows] = useState([]);

  useEffect(() => {
    (async () => {
      const S = window.AccountCreationStore;
      const store = (S && S.list) ? (S.list() || []) : [];
      let containers = {}, devs = [];
      try { containers = (await window.Backend.getContainers()) || {}; } catch (e) {}
      try { devs = (await window.Backend.devices()) || []; } catch (e) {}
      const devName = {}; devs.forEach(d => { if (d && d.udid) devName[String(d.udid)] = d.tag || d.label || d.name || ""; });
      const place = {};
      Object.keys(containers).forEach(udid => (containers[udid] || []).forEach(c => {
        if (c && c.account_id) place[c.account_id] = { tel: devName[String(udid)] || "", cont: c.name || "" };
      }));
      setRows(store.filter(a => a.username).map(a => {
        const p = place[a.id];
        return {
          id: a.id, username: a.username, category: a.category || "",
          place: p ? (p.tel + " · " + p.cont) : "",
          // la PP auto-générée vit dans le dossier du conteneur ; si elle n'existe pas, la vignette
          // tombera en erreur et on affichera l'icône par défaut (onError du <img>)
          pp: p && p.tel && p.cont ? ("Téléphones/" + p.tel + "/" + p.cont + "/PP unique/pp insta.png") : "",
        };
      }));
    })();
  }, []);

  const f = q.trim().toLowerCase();
  const shown = f ? rows.filter(r => (r.username + " " + r.category + " " + r.place).toLowerCase().indexOf(f) >= 0) : rows;

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 900, display: "flex", alignItems: "center", justifyContent: "center" }}>
      <div onClick={e => e.stopPropagation()} style={{ width: 460, maxHeight: "72vh", display: "flex", flexDirection: "column", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, padding: 16 }}>
        <div style={{ fontSize: 14, fontWeight: 800, marginBottom: 9 }}>👤 Poser un compte sur la toile</div>
        <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Chercher un compte, une catégorie, un conteneur…"
          style={{ padding: "8px 11px", borderRadius: 9, border: "1px solid var(--border)", background: "var(--surface-2)", color: "var(--text)", fontSize: 12.5, marginBottom: 9, outline: "none" }} />
        <div style={{ overflowY: "auto", display: "flex", flexDirection: "column", gap: 4 }}>
          {shown.map(r => (
            <button key={r.id} onClick={() => onPick(r)}
              style={{ flexShrink: 0, textAlign: "left", display: "flex", alignItems: "center", gap: 9, padding: "7px 9px", borderRadius: 8, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", cursor: "pointer" }}>
              <span style={{ fontSize: 14 }}>👤</span>
              <span style={{ minWidth: 0 }}>
                <span style={{ display: "block", fontSize: 12.5, fontWeight: 700 }}>@{r.username}</span>
                <span style={{ display: "block", fontSize: 10.5, color: "var(--faint)" }}>{r.category || "sans catégorie"}{r.place ? " · " + r.place : ""}</span>
              </span>
            </button>
          ))}
          {shown.length === 0 && <div style={{ fontSize: 12, color: "var(--faint)", padding: 14, textAlign: "center" }}>Aucun compte.</div>}
        </div>
        <button onClick={onClose} style={{ marginTop: 10, padding: "7px 12px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--muted)", fontSize: 12.5, fontWeight: 700, cursor: "pointer" }}>Fermer</button>
      </div>
    </div>
  );
}

window.Dashboard = Dashboard;
