/* global React, Backend */
// ============================================================
// Phone Cloud — liste des cloud phones MoreLogin (statut on/off + power)
// ============================================================

const CC_FLAG = { US: "🇺🇸", GB: "🇬🇧", FR: "🇫🇷", DE: "🇩🇪", NL: "🇳🇱", JP: "🇯🇵", CA: "🇨🇦", AU: "🇦🇺", SG: "🇸🇬", BR: "🇧🇷", IT: "🇮🇹", ES: "🇪🇸", IN: "🇮🇳" };

// Ordre personnalisé des chips de catégorie du Pilotage cloud (glisser-déposer), persisté.
const CLOUD_CAT_ORDER_KEY = "phonelabs.cloud_cat_order";

// Couleur stable par catégorie (même nom -> même couleur) pour reconnaître au coup d'œil
const _CAT_PALETTE = ["#3b82f6", "#34d399", "#f0a020", "#a78bfa", "#f472b6", "#22d3ee", "#f87171", "#84cc16", "#fb923c", "#c084fc"];
function catColor(name) {
  if (!name) return "#8b949e";
  let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
  return _CAT_PALETTE[h % _CAT_PALETTE.length];
}
function CatBadge({ group, size }) {
  if (!group) return <span style={{ fontSize: (size || 11) + .5, color: "var(--faint)" }}>— sans catégorie —</span>;
  const c = catColor(group);
  return <span style={{ fontSize: size || 11, fontWeight: 800, padding: "3px 10px", borderRadius: 99, background: c + "22", color: c, border: "1px solid " + c + "66", whiteSpace: "nowrap" }}>{group}</span>;
}

// Phase = étape du workflow (MÊME liste + MÊME persistance que les iPhones physiques : Backend.patchMeta
// écrit dans localStorage "farm_phases" par udid + tente le PATCH backend, no-op inoffensif pour un id cloud).
const CLOUD_PHASES = [
  { k: "", label: "— Libre —" },
  { k: "creation_compte", label: "Création de compte" },
  { k: "login_compte", label: "Login de compte" },
  { k: "echauffement", label: "Échauffement" },
  { k: "publication", label: "Publication" },
];
function CloudPhaseSelect({ cp }) {
  const { useState, useEffect } = React;
  const initial = (window.Backend && window.Backend._localPhases && window.Backend._localPhases[cp.id]) || "";
  const [val, setVal] = useState(initial);
  const [saving, setSaving] = useState(false);
  useEffect(() => { setVal((window.Backend && window.Backend._localPhases && window.Backend._localPhases[cp.id]) || ""); }, [cp.id]);
  const onChange = async (e) => {
    const v = e.target.value;
    setVal(v);
    setSaving(true);
    try { if (window.Backend && window.Backend.patchMeta) await window.Backend.patchMeta(cp.id, { phase: v || null }); } catch (_) {}
    setSaving(false);
  };
  const isCreation = val === "creation_compte";
  return (
    <select value={val} onChange={onChange} disabled={saving}
      title="Étape du workflow de ce cloud phone"
      style={{ padding: "5px 8px", borderRadius: 8, fontSize: 11.5, fontWeight: 600, outline: "none",
        cursor: saving ? "wait" : "pointer", opacity: saving ? .6 : 1,
        background: isCreation ? "var(--accent-soft, rgba(29,186,180,.12))" : "var(--surface-2)",
        color: isCreation ? "var(--accent,#1dbab4)" : "var(--muted)",
        border: "1px solid " + (isCreation ? "var(--accent-line, rgba(29,186,180,.4))" : "var(--border)"),
        maxWidth: 140 }}>
      {CLOUD_PHASES.map(p => <option key={p.k} value={p.k}>{p.label}</option>)}
    </select>
  );
}

// Modal de duplication d'un profil (⚠️ FACTURÉ : crée un nouveau cloud phone)
function DuplicateProfileModal({ list, onClose, onDone }) {
  const { useState, useEffect } = React;
  const first = list[0];
  const [src, setSrc] = useState(first ? first.id : "");
  const [name, setName] = useState("");
  const [count, setCount] = useState(1);
  const [group, setGroup] = useState("");
  const [groups, setGroups] = useState([]);
  const [proxyId, setProxyId] = useState("");
  const [proxies, setProxies] = useState([]);
  const [phase, setPhase] = useState("login_compte");   // template/phase du profil à la création (défaut : Login de compte)
  const [busy, setBusy] = useState(false);
  const [prog, setProg] = useState("");
  const [res, setRes] = useState(null);
  useEffect(() => {
    let on = true;
    if (window.Backend && window.Backend.cloudphoneGroups) {
      window.Backend.cloudphoneGroups().then(r => {
        if (!on) return;
        const gs = ((r && r.groups) || r || []).map(g => typeof g === "string" ? g : (g && (g.name || g.groupName || g.label))).filter(Boolean);
        setGroups(gs); if (gs.length) setGroup(g0 => g0 || gs[0]);
      }).catch(() => {});
    }
    if (window.Backend && window.Backend.moreLoginProxies) {
      window.Backend.moreLoginProxies().then(l => { if (on) setProxies(l || []); }).catch(() => {});
    }
    return () => { on = false; };
  }, []);
  // filet anti-blocage : si un appel backend ne répond jamais, on rejette après ms au lieu de rester sur « … »
  const _withTimeout = (p, ms, label) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error((label || "opération") + " : délai dépassé — le backend/MoreLogin ne répond pas")), ms))]);
  const create = async () => {
    const n = Math.max(1, Math.min(20, parseInt(count) || 1));
    // prochain numéro CP-N d'après les profils qui EXISTENT (pas le compteur interne MoreLogin)
    let maxNum = 0;
    (list || []).forEach(p => { const m = String(p.name || "").match(/CP[-\s_]?(\d+)/i); if (m) maxNum = Math.max(maxNum, parseInt(m[1]) || 0); });
    setBusy(true); setRes(null);
    let ok = 0, fail = 0, lastErr = "";
    try {
      for (let i = 0; i < n; i++) {
        const nm = name ? (n > 1 ? name + " " + (i + 1) : name) : ("CP-" + (maxNum + 1 + i));
        setProg("Création " + (i + 1) + "/" + n + " (" + nm + ")…");
        // QUOTA DE CLONAGE (VA) : réserve 1 slot de façon atomique côté serveur AVANT de cloner.
        // Owner/Manager -> illimité. Si le quota est atteint, on stoppe (pas de cloud phone facturé créé).
        const q = window.Backend.cloneConsume ? await window.Backend.cloneConsume() : { ok: true };
        if (!(q && q.ok)) { fail++; lastErr = (q && q.error) || "quota de clonage atteint"; break; }
        let r = { ok: false };
        try { r = window.Backend.cloudphoneDuplicate ? await _withTimeout(window.Backend.cloudphoneDuplicate(src, nm, group || "IG"), 90000, "Création du profil") : { ok: false, error: "backend indisponible" }; }
        catch (e) { r = { ok: false, error: e.message }; }
        if (r && r.ok) {
          ok++;
          // MoreLogin auto-nomme → on FORCE le nom CP-N via edit/batch
          if (r.id && window.Backend.cloudphoneRename) { try { await _withTimeout(window.Backend.cloudphoneRename(r.id, nm), 30000, "Renommage"); } catch (e) {} }
          // proxy choisi manuellement -> on le bind sur le profil fraîchement créé
          if (r.id && proxyId && window.Backend.cloudphoneBindProxy) { try { await _withTimeout(window.Backend.cloudphoneBindProxy(r.id, proxyId), 30000, "Bind proxy"); } catch (e) {} }
          // phase/template : sauvegarde locale IMMÉDIATE (synchrone) + sync backend best-effort SANS bloquer la création
          if (r.id && phase && window.Backend.patchMeta) { try { window.Backend.patchMeta(r.id, { phase }); } catch (e) {} }
        } else { fail++; lastErr = (r && (r.error || r.msg)) || ("échec (code " + (r && r.code) + ")"); }
      }
    } finally {
      setBusy(false); setProg("");   // le bouton se débloque TOUJOURS, même en cas d'erreur/timeout
    }
    setRes({ ok: fail === 0 && ok > 0, made: ok, fail, error: lastErr });
    if (ok > 0) { if (window.SoundBoard) window.SoundBoard.playForce("cloud_power"); setTimeout(() => { onDone && onDone(); }, 1400); }
  };
  const inp = { width: "100%", boxSizing: "border-box", padding: "9px 11px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text)", fontSize: 13, outline: "none" };
  const lbl = { fontSize: 11, color: "var(--muted)", fontWeight: 600, margin: "12px 0 4px", display: "block" };
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", display: "grid", placeItems: "center", zIndex: 400 }}>
      <div onClick={e => e.stopPropagation()} style={{ width: "min(440px, 92vw)", background: "var(--bg,#0d1117)", border: "1px solid var(--border)", borderRadius: 16, padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }}>
        <div style={{ fontSize: 17, fontWeight: 800, marginBottom: 4 }}>➕ Ajouter un profil (dupliquer)</div>
        <div style={{ fontSize: 12.5, color: "var(--faint)", marginBottom: 12 }}>Crée un nouveau cloud phone avec le même forfait que la source.</div>
        <div style={{ background: "rgba(240,160,32,.12)", border: "1px solid rgba(240,160,32,.4)", borderRadius: 10, padding: "9px 12px", fontSize: 12, color: "#f0a020", marginBottom: 8 }}>
          ⚠️ <b>Action facturée</b> : crée un <b>nouveau cloud phone payant</b> chez MoreLogin (facturation à l'usage).
        </div>
        <label style={lbl}>Profil source (forfait copié)</label>
        <select value={src} onChange={e => setSrc(e.target.value)} style={inp}>
          {list.map(p => <option key={p.id} value={p.id}>{p.name || p.id}</option>)}
        </select>
        <label style={lbl}>Nom (vide = auto CP-N d'après les profils existants)</label>
        <input value={name} onChange={e => setName(e.target.value)} style={inp} placeholder="auto : prochain CP-N" />
        <label style={lbl}>Nombre de profils</label>
        <input type="number" min="1" max="20" value={count} onChange={e => setCount(e.target.value)} style={inp} />
        <label style={lbl}>Catégorie (groupe MoreLogin)</label>
        {groups.length
          ? <select value={group} onChange={e => setGroup(e.target.value)} style={inp}>{groups.map(g => <option key={g} value={g}>{g}</option>)}</select>
          : <input value={group} onChange={e => setGroup(e.target.value)} style={inp} placeholder="IG" />}
        <label style={lbl}>Phase / template du profil</label>
        <select value={phase} onChange={e => setPhase(e.target.value)} style={inp}>
          {CLOUD_PHASES.map(p => <option key={p.k} value={p.k}>{p.label}</option>)}
        </select>
        <label style={lbl}>Proxy (optionnel — vide = auto/aucun)</label>
        <select value={proxyId} onChange={e => setProxyId(e.target.value)} style={inp}>
          <option value="">— auto (pas de bind manuel) —</option>
          {proxies.map(p => <option key={p.id} value={p.id}>{p.name || p.id}{p.country ? (" · " + p.country) : ""}</option>)}
        </select>
        {res && !res.ok && <div style={{ marginTop: 12, fontSize: 12.5, color: "#f87171" }}>⚠️ {res.made} créé(s), {res.fail} échec(s) — {res.error}</div>}
        {res && res.ok && <div style={{ marginTop: 12, fontSize: 12.5, color: "#34d399" }}>✓ {res.made} profil(s) créé(s) dans « {group} ».</div>}
        <div style={{ display: "flex", gap: 10, marginTop: 18, justifyContent: "flex-end" }}>
          <button onClick={onClose} style={{ padding: "9px 16px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 600, fontSize: 13, cursor: "pointer" }}>Annuler</button>
          <button onClick={create} disabled={busy || !src}
            style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: busy ? "var(--surface-2)" : "#f0a020", color: busy ? "var(--muted)" : "#1a1206", fontWeight: 800, fontSize: 13, cursor: busy ? "default" : "pointer" }}>
            {busy ? (prog || "Création…") : "➕ Créer (facturé)"}
          </button>
        </div>
      </div>
    </div>
  );
}

// Ouvre le profil Instagram d'un @username dans le navigateur de l'OS (hors app), repli window.open.
function _openInstagram(username) {
  const u = (username || "").trim(); if (!u) return;
  const url = "https://www.instagram.com/" + encodeURIComponent(u) + "/";
  if (window.Backend && window.Backend.openUrl) window.Backend.openUrl(url).then(r => { if (!(r && r.ok)) window.open(url, "_blank", "noopener,noreferrer"); }).catch(() => window.open(url, "_blank", "noopener,noreferrer"));
  else window.open(url, "_blank", "noopener,noreferrer");
}

function PhoneCloudList() {
  const { useState, useEffect, useRef } = React;
  const [list, setList] = useState([]);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState({});          // id -> true pendant power
  const [err, setErr] = useState("");
  const [dupOpen, setDupOpen] = useState(false);
  const [groups, setGroups] = useState([]);
  const [catBusy, setCatBusy] = useState({});
  const [usage, setUsage] = useState({});   // id -> {seconds, lastActive}
  const [rate, setRate] = useState(0.006);  // $/min quand allumé (source de vérité = backend)
  const mounted = useRef(true);

  const refresh = async () => {
    if (!window.Backend || !window.Backend.cloudphones) { setErr("Backend indisponible"); setLoading(false); return; }
    try {
      const l = await window.Backend.cloudphones();
      if (mounted.current) { setList(l || []); setErr(""); }
    } catch (e) { if (mounted.current) setErr(e.message); }
    if (mounted.current) setLoading(false);
  };

  useEffect(() => {
    mounted.current = true;
    refresh();
    if (window.Backend && window.Backend.cloudphoneGroups) window.Backend.cloudphoneGroups().then(r => { if (mounted.current) setGroups(((r && r.groups) || r || []).map(g => typeof g === "string" ? g : (g && (g.name || g.groupName || g.label))).filter(Boolean)); }).catch(() => {});
    const refreshUsage = () => { if (window.Backend && window.Backend.cloudphoneUsage) window.Backend.cloudphoneUsage().then(r => { if (mounted.current) { setUsage((r && r.usage) || {}); if (r && r.rate_per_min) setRate(r.rate_per_min); } }).catch(() => {}); };
    refreshUsage();
    const t = setInterval(refresh, 8000);
    const tu = setInterval(refreshUsage, 8000);
    return () => { mounted.current = false; clearInterval(t); clearInterval(tu); };
  }, []);

  const _relTime = (ts) => {
    if (!ts) return null;
    const s = Math.round(Date.now() / 1000 - ts);
    if (s < 5) return "à l'instant";
    if (s < 60) return "il y a " + s + "s";
    if (s < 3600) return "il y a " + Math.round(s / 60) + "min";
    if (s < 86400) return "il y a " + Math.round(s / 3600) + "h";
    return "il y a " + Math.round(s / 86400) + "j";
  };

  const setCat = async (cp, g) => {
    if (!g || g === cp.group) return;
    setCatBusy(b => ({ ...b, [cp.id]: true }));
    try { await window.Backend.cloudphoneSetGroup(cp.id, g); } catch (e) {}
    setTimeout(async () => { await refresh(); if (mounted.current) setCatBusy(b => ({ ...b, [cp.id]: false })); }, 1300);
  };
  // Assigne (ou libère) ce cloud phone à un VA -> operator_id en device-meta (D1). Le VA ne verra/pilotera que les siens.
  const setAssignee = async (cp, opId) => {
    if (String(cp.operator_id || "") === String(opId || "")) return;
    try { await window.Backend.patchMeta(cp.id, { operator_id: opId || null }); } catch (e) {}
    setTimeout(() => { if (mounted.current) refresh(); }, 400);
  };
  const teamVAs = (window.DATA && DATA.realTeam) ? DATA.realTeam().filter(o => o && (o.role === "Opérateur" || o.role === "Manager")) : [];

  const deleteProfile = async (cp) => {
    if (!window.confirm("Supprimer définitivement « " + (cp.name || cp.id) + " » ?\n\nCette action est IRRÉVERSIBLE et supprime le cloud phone chez MoreLogin (données, apps, historique perdus).")) return;
    setBusy(b => ({ ...b, [cp.id]: true }));
    const r = window.Backend.cloudphoneDelete ? await window.Backend.cloudphoneDelete(cp.id) : { ok: false };
    if (!r.ok) alert("⚠️ Suppression impossible : " + (r.error || r.msg || "échec (code " + r.code + ") — MoreLogin ouvert et à jour (≥ 2.9.0) ?"));
    setTimeout(async () => { await refresh(); if (mounted.current) setBusy(b => ({ ...b, [cp.id]: false })); }, 1000);
  };

  const power = async (cp, on) => {
    setBusy(b => ({ ...b, [cp.id]: true }));
    if (window.SoundBoard) {
      if (on) { window.SoundBoard.playForce("cloud_power"); if (window.SoundBoard.genEleven) window.SoundBoard.genEleven("cloud_power", "short futuristic power-on button beep, sci-fi UI", 1.2); }
      else { window.SoundBoard.playForce("cloud_off"); if (window.SoundBoard.genEleven) window.SoundBoard.genEleven("cloud_off", "short power-down shutdown sound, descending sci-fi tone", 1.2); }
    }
    try { await window.Backend.cloudphonePower(cp.id, on); } catch (e) {}
    // laisse le temps au statut de bouger cote MoreLogin
    setTimeout(async () => { await refresh(); setBusy(b => ({ ...b, [cp.id]: false })); }, 2500);
  };

  const statusOf = (cp) => {
    if (cp.envStatus === 4) return { label: "Allumé", color: "#34d399", bg: "rgba(52,211,153,.12)", dot: "#34d399" };
    if (cp.envStatus === 2) return { label: "Éteint", color: "#8b949e", bg: "var(--surface-2)", dot: "#6e7681" };
    return { label: "Transition…", color: "#f0a020", bg: "rgba(240,160,32,.12)", dot: "#f0a020" };
  };

  return (
    <div style={{ padding: 24, maxWidth: 1680, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", flexWrap: "wrap", gap: 12, marginBottom: 18 }}>
        <div>
          <div style={{ fontSize: 21, fontWeight: 800 }}>☁️ Phone Cloud</div>
          <div style={{ fontSize: 13, color: "var(--faint)", marginTop: 4 }}>
            {list.length} cloud phone(s) MoreLogin · auto-off après 5 min d'inactivité · $0,006/min quand allumé
          </div>
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <button onClick={async () => {
              // IMPORTE les groupes DÉJÀ créés dans MoreLogin comme catégories de l'app (ne crée rien)
              const r = window.Backend.cloudphoneGroups ? await window.Backend.cloudphoneGroups() : null;
              const gs = ((r && r.groups) || r || []).map(g => typeof g === "string" ? g : (g && (g.name || g.groupName || g.label))).filter(Boolean);
              if (!gs.length) { alert("Aucun groupe trouvé dans MoreLogin (app ouverte ?)."); return; }
              const cats = gs.map(name => ({ label: name, emoji: "" }));
              try { localStorage.setItem("phonelabs.categories", JSON.stringify(cats)); } catch (e) {}
              try { window.setCampaignAccountCats && window.setCampaignAccountCats(); } catch (e) {}
              try { window.dispatchEvent(new Event("storage")); } catch (e) {}
              alert("✓ " + gs.length + " groupe(s) MoreLogin importé(s) comme catégories :\n" + gs.join(", "));
            }}
            title="Récupère les groupes déjà créés dans MoreLogin comme catégories de l'app (n'en crée aucun)"
            style={{ display: "flex", alignItems: "center", gap: 6, padding: "9px 14px", borderRadius: 10, background: "var(--surface)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 600, fontSize: 13, cursor: "pointer" }}>
            🗂️ Importer les groupes
          </button>
          <button onClick={() => setDupOpen(true)} disabled={list.length === 0}
            title="Duplique un profil existant (⚠️ crée un nouveau cloud phone facturé)"
            style={{ display: "flex", alignItems: "center", gap: 6, padding: "9px 14px", borderRadius: 10, background: list.length ? "#238636" : "var(--surface-2)", border: "none", color: list.length ? "#fff" : "var(--muted)", fontWeight: 700, fontSize: 13, cursor: list.length ? "pointer" : "default" }}>
            ➕ Ajouter un profil
          </button>
          <button onClick={() => { setLoading(true); refresh(); }}
            style={{ display: "flex", alignItems: "center", gap: 6, padding: "9px 14px", borderRadius: 10, background: "var(--surface)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 600, fontSize: 13, cursor: "pointer" }}>
            <span style={{ display: "inline-block", animation: loading ? "spin 1s linear infinite" : "none", fontSize: 15 }}>↻</span> Refresh
          </button>
        </div>
      </div>

      {err && <div style={{ background: "rgba(248,81,73,.1)", border: "1px solid rgba(248,81,73,.3)", borderRadius: 10, padding: "10px 14px", color: "var(--danger,#f87171)", fontSize: 13, marginBottom: 14 }}>⚠️ {err}</div>}

      {(!loading && list.length === 0) ? (
        <div style={{ textAlign: "center", padding: "60px 0", color: "var(--faint)", border: "1px dashed var(--border)", borderRadius: 12 }}>
          <div style={{ fontSize: 34, marginBottom: 12 }}>☁️</div>
          <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)", marginBottom: 6 }}>Aucun cloud phone détecté</div>
          <div style={{ fontSize: 12.5 }}>Vérifie que l'app MoreLogin est ouverte et que le backend a été redémarré.</div>
        </div>
      ) : (
        <div style={{ overflowX: "auto", border: "1px solid var(--border)", borderRadius: 12, position: "relative" }}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
            <thead>
              <tr style={{ background: "var(--surface-2)", textAlign: "left", color: "var(--faint)" }}>
                {["ID", "Profil", "Catégorie", "Phase", "Compte", "Assigné à (VA)", "Plug", "Proxy", "ADB", "Statut", "Dern. conn.", "Coût", "Actions"].map((h, i, arr) => (
                  <th key={i} style={{ padding: "8px 10px", fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: ".04em", whiteSpace: "nowrap", ...(i === arr.length - 1 ? { position: "sticky", right: 0, background: "var(--surface-2)", textAlign: "right", boxShadow: "-8px 0 8px -8px rgba(0,0,0,.45)", zIndex: 2 } : {}) }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {list.map((cp, i) => {
                const st = statusOf(cp);
                const flag = cp.countryCode ? (CC_FLAG[cp.countryCode] || "") : "";
                const isBusy = !!busy[cp.id];
                const acc = ((window.AccountCreationStore && window.AccountCreationStore.list) ? window.AccountCreationStore.list() : []).find(a => String(a.attributed_udid) === String(cp.id));
                return (
                  <tr key={cp.id} style={{ borderTop: "1px solid var(--border)" }}>
                    <td style={{ padding: "7px 9px" }}><span className="mono" style={{ fontSize: 11.5, fontWeight: 800, color: "var(--accent)" }}>CP#{i + 1}</span></td>
                    <td style={{ padding: "7px 9px" }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
                        <span style={{ width: 8, height: 8, borderRadius: "50%", background: st.dot, flex: "none", boxShadow: cp.envStatus === 4 ? "0 0 6px " + st.dot : "none" }} />
                        <span style={{ fontWeight: 700, whiteSpace: "nowrap" }}>{cp.name || "Sans nom"}</span>
                      </div>
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      <select value={cp.group || ""} disabled={!!catBusy[cp.id]} onChange={e => setCat(cp, e.target.value)}
                        title="Changer le groupe MoreLogin de ce profil"
                        style={{ background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text)", fontSize: 12, fontWeight: 600, padding: "5px 7px", cursor: catBusy[cp.id] ? "default" : "pointer", maxWidth: 128, opacity: catBusy[cp.id] ? 0.5 : 1 }}>
                        {(cp.group && groups.indexOf(cp.group) < 0 ? [cp.group].concat(groups) : (groups.length ? groups : [cp.group || "— sans catégorie —"])).map(g => <option key={g} value={g}>{g}</option>)}
                      </select>
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      <CloudPhaseSelect cp={cp} />
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      {acc
                        ? <button onClick={() => _openInstagram(acc.username)} title={"Ouvrir @" + (acc.username || "") + " sur Instagram (hors app)"}
                            style={{ fontSize: 12, color: "var(--accent)", fontWeight: 600, whiteSpace: "nowrap", background: "none", border: "none", padding: 0, cursor: "pointer", textDecoration: "underline", textUnderlineOffset: 2, display: "inline-flex", alignItems: "center", gap: 3 }}>@{acc.username || "?"} <span style={{ fontSize: 10, opacity: .8 }}>↗</span></button>
                        : <span style={{ color: "var(--faint)", fontSize: 12 }}>—</span>}
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      <select value={cp.operator_id || ""} onChange={e => setAssignee(cp, e.target.value)}
                        title="Assigner ce cloud phone à un VA (il ne verra/pilotera que les siens)"
                        style={{ background: cp.operator_id ? "var(--accent-soft, rgba(29,186,180,.12))" : "var(--surface-2)", border: "1px solid " + (cp.operator_id ? "var(--accent-line, rgba(29,186,180,.4))" : "var(--border)"), borderRadius: 8, color: cp.operator_id ? "var(--accent,#1dbab4)" : "var(--muted)", fontSize: 11.5, fontWeight: 600, padding: "5px 7px", cursor: "pointer", maxWidth: 140 }}>
                        <option value="">— libre —</option>
                        {teamVAs.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
                        {cp.operator_id && !teamVAs.some(o => String(o.id) === String(cp.operator_id)) && <option value={cp.operator_id}>{(cp.operator && cp.operator.name) || "VA " + cp.operator_id}</option>}
                      </select>
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      {acc
                        ? (acc.ig_plug
                            ? <span style={{ display: "inline-block", fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "rgba(52,211,153,.15)", color: "#34d399", border: "1px solid rgba(52,211,153,.4)", whiteSpace: "nowrap" }}>🔌 Plug</span>
                            : <span style={{ display: "inline-block", fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "var(--surface-2)", color: "var(--faint)", border: "1px solid var(--border)", whiteSpace: "nowrap" }} title="Pas plug">—</span>)
                        : <span style={{ color: "var(--faint)", fontSize: 12 }}>—</span>}
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      {cp.proxyName
                        ? <div style={{ display: "flex", flexDirection: "column", lineHeight: 1.22, maxWidth: 140 }}>
                            <span style={{ fontSize: 12, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={cp.proxyName}>🔌 {flag ? flag + " " : ""}{cp.proxyName}</span>
                            {cp.exportIp ? <span className="mono" style={{ color: "var(--faint)", fontSize: 10, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={cp.exportIp}>{cp.exportIp}</span> : null}
                          </div>
                        : <span style={{ color: "var(--faint)", fontSize: 12 }}>— sans proxy —</span>}
                    </td>
                    <td style={{ padding: "7px 9px", textAlign: "center" }}><span style={{ fontSize: 11.5, color: cp.enableAdb ? "var(--ok,#34d399)" : "var(--faint)" }}>{cp.enableAdb ? "✓" : "✗"}</span></td>
                    <td style={{ padding: "7px 9px" }}>
                      <span style={{ fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: st.bg, color: st.color, border: "1px solid " + st.color + "55", whiteSpace: "nowrap" }}>{st.label}</span>
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      {(() => { const t = _relTime(usage[cp.id] && usage[cp.id].lastActive); return t
                        ? <span className="mono" style={{ fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap" }}>{t}</span>
                        : <span style={{ color: "var(--faint)", fontSize: 12 }}>—</span>; })()}
                    </td>
                    <td style={{ padding: "7px 9px" }}>
                      {(() => { const secs = (usage[cp.id] && usage[cp.id].seconds) || 0; return secs > 0
                        ? <span className="mono" style={{ fontSize: 11, color: "var(--text)" }}>${(secs / 60 * rate).toFixed(3)}</span>
                        : <span style={{ color: "var(--faint)", fontSize: 12 }}>—</span>; })()}
                    </td>
                    <td style={{ padding: "6px 9px", textAlign: "right", position: "sticky", right: 0, background: "var(--bg)", boxShadow: "-8px 0 8px -8px rgba(0,0,0,.45)", zIndex: 1 }}>
                      <div style={{ display: "flex", gap: 5, justifyContent: "flex-end", alignItems: "center" }}>
                        {acc && window.BanVerifButton && React.createElement(window.BanVerifButton, { udid: cp.id })}
                        {cp.envStatus === 4 ? (
                          <button onClick={() => power(cp, false)} disabled={isBusy}
                            style={{ padding: "6px 10px", borderRadius: 8, border: "1px solid rgba(248,81,73,.35)", background: "rgba(248,81,73,.1)", color: "#f87171", fontWeight: 700, fontSize: 12, cursor: isBusy ? "default" : "pointer", whiteSpace: "nowrap" }}>
                            {isBusy ? "…" : "⏻ Éteindre"}
                          </button>
                        ) : (
                          <button onClick={() => power(cp, true)} disabled={isBusy}
                            style={{ padding: "6px 10px", borderRadius: 8, border: "none", background: isBusy ? "var(--surface-2)" : "#238636", color: isBusy ? "var(--muted)" : "#fff", fontWeight: 700, fontSize: 12, cursor: isBusy ? "default" : "pointer", whiteSpace: "nowrap" }}>
                            {isBusy ? "…" : "▶ Allumer"}
                          </button>
                        )}
                        <button onClick={() => deleteProfile(cp)} disabled={isBusy} title="Supprimer définitivement ce profil"
                          style={{ padding: "6px 8px", borderRadius: 8, border: "1px solid var(--border)", background: "var(--surface-2)", color: "var(--faint)", fontWeight: 700, fontSize: 12, cursor: isBusy ? "default" : "pointer" }}>
                          🗑
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {dupOpen && <DuplicateProfileModal list={list} onClose={() => setDupOpen(false)} onDone={() => { setDupOpen(false); setTimeout(refresh, 800); }} />}
    </div>
  );
}

window.PhoneCloudList = PhoneCloudList;

// ============================================================
// Pilotage cloud — MÊME interface que Pilotage iPhone :
// grille des cloud phones (allumés / éteints) → clic → écran live + contrôles
// ============================================================

function _cloudStatus(cp) {
  if (cp.envStatus === 4) return { key: "online", label: "Allumé", color: "#34d399", bg: "rgba(52,211,153,.12)", dot: "#34d399" };
  if (cp.envStatus === 2) return { key: "offline", label: "Éteint", color: "#8b949e", bg: "var(--surface-2)", dot: "#6e7681" };
  return { key: "busy", label: "Transition…", color: "#f0a020", bg: "rgba(240,160,32,.12)", dot: "#f0a020" };
}

// Carte façon DeviceCard (grille Pilotage iPhone) mais pour un cloud phone
function _fmtLockMs(ms) { const s = Math.floor(ms / 1000), h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; return (h ? h + "h " : "") + (h || m ? m + "m " : "") + sec + "s"; }
// Badge cadenas qui égrène le cooldown en temps réel (ou affiche la plage horaire)
function CloudLockBadge({ lock }) {
  const { useEffect, useReducer } = React;
  const [, tick] = useReducer(x => x + 1, 0);
  useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); }, []);
  let txt = "🔒 verrouillé";
  if (lock.reason === "cooldown") { const ms = lock.until - Date.now(); txt = "🔒 " + (ms > 0 ? _fmtLockMs(ms) : "0s"); }
  else if (lock.reason === "window") txt = "🔒 hors créneau";
  return <span title="Verrouillé (cooldown / plage horaire)" style={{ fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "rgba(240,160,32,.14)", color: "#f0a020", border: "1px solid rgba(240,160,32,.4)", whiteSpace: "nowrap" }}>{txt}</span>;
}

function CloudCard({ cp, onOpen, groups, onChangeGroup }) {
  const { useState } = React;
  const [busy, setBusy] = useState(false);
  const st = _cloudStatus(cp);
  const flag = cp.countryCode ? (CC_FLAG[cp.countryCode] || "") : "";
  // compte IG attribué à ce profil cloud -> statut Plug / Pas plug (pour repérer les profils branchés)
  const acc = ((window.AccountCreationStore && window.AccountCreationStore.list) ? window.AccountCreationStore.list() : []).find(a => String(a.attributed_udid) === String(cp.id));
  const isPlug = !!(acc && acc.ig_plug);
  // parcours en cours sur ce profil -> phase actuelle + verrou (cooldown/plage horaire)
  const _prog = window.ParcoursProgress ? window.ParcoursProgress.get(cp.id) : null;
  const _parc = (_prog && window.ParcoursStore) ? window.ParcoursStore.get(_prog.parcoursId) : null;
  const _curPhase = (_parc && !_prog.done && window.pcCurrentPhase) ? window.pcCurrentPhase(_parc, _prog) : null;
  const _lock = (_parc && !_prog.done && window.pcLockState) ? window.pcLockState(_parc, _prog) : { locked: false };
  return (
    <div onClick={onOpen} style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "var(--r-lg,14px)",
      overflow: "hidden", cursor: "pointer", transition: "transform .16s, border-color .16s, box-shadow .16s", opacity: st.key === "offline" ? .74 : 1 }}
      onMouseEnter={e => { e.currentTarget.style.transform = "translateY(-3px)"; e.currentTarget.style.borderColor = "var(--border-2)"; e.currentTarget.style.boxShadow = "var(--shadow)"; }}
      onMouseLeave={e => { e.currentTarget.style.transform = "none"; e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.boxShadow = "none"; }}>
      <div style={{ position: "relative", aspectRatio: "9 / 14", containerType: "inline-size", background: "#0b1020", borderBottom: "1px solid var(--border)" }}>
        <div style={{ position: "absolute", inset: 0, background: "linear-gradient(165deg, #263a55 0%, #1a2740 55%, #121a2a 100%)", display: "grid", placeItems: "center", overflow: "hidden" }}>
          <div style={{ position: "absolute", width: "75%", height: "48%", left: "-18%", top: "-12%", borderRadius: "50%", background: "rgba(90,160,220,.20)", filter: "blur(30px)" }} />
          <div style={{ position: "absolute", width: "65%", height: "42%", right: "-15%", bottom: "-10%", borderRadius: "50%", background: "rgba(70,130,200,.14)", filter: "blur(34px)" }} />
          <div style={{ textAlign: "center", zIndex: 2, padding: "0 12px", maxWidth: "100%" }}>
            <div style={{ fontSize: "clamp(30px, 22cqw, 46px)", lineHeight: 1 }}>☁️</div>
            <div style={{ marginTop: 8, fontSize: "clamp(15px, 12cqw, 26px)", fontWeight: 800, color: "#e9edf6", textShadow: "0 2px 16px rgba(0,0,0,.45)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{cp.name || "Cloud phone"}</div>
            <div className="mono" style={{ marginTop: 7, fontSize: 11, color: "rgba(195,205,225,.5)" }}>{st.key === "online" ? "cliquer pour piloter" : st.key === "busy" ? "démarrage…" : "cliquer pour allumer"}</div>
          </div>
        </div>
        <div style={{ position: "absolute", top: 9, left: 9, right: 9, display: "flex", justifyContent: "space-between", alignItems: "flex-start", zIndex: 6 }}>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 10.5, fontWeight: 700, padding: "3px 9px", borderRadius: 99, background: st.bg, color: st.color, border: "1px solid " + st.color + "55" }}>
            <span style={{ width: 7, height: 7, borderRadius: "50%", background: st.dot }} /> {st.label}
          </span>
          <span style={{ fontSize: 10, fontWeight: 700, padding: "2px 7px", borderRadius: 99, background: "rgba(120,160,220,.2)", color: "#9cc4f0" }}>ANDROID</span>
        </div>
      </div>
      <div style={{ padding: "13px 14px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
          <div style={{ fontWeight: 700, fontSize: 14.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "none" }}>{cp.name || "Cloud phone"}</div>
          {/* Proxy — pastille visible (à la place de l'ADB) */}
          {cp.proxyName ? (
            <span title={"Proxy : " + cp.proxyName} style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, fontWeight: 800, color: "var(--accent,#1dbab4)", background: "var(--accent-soft, rgba(29,186,180,.12))", border: "1px solid var(--accent-line, rgba(29,186,180,.4))", borderRadius: 99, padding: "4px 11px", maxWidth: 160, minWidth: 0 }}>
              🔌 {flag ? flag + " " : ""}<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{cp.proxyName}</span>
            </span>
          ) : <span style={{ fontSize: 11, color: "var(--faint)" }}>— sans proxy —</span>}
        </div>
        <div style={{ marginTop: 10 }} onClick={e => e.stopPropagation()}>
          <select value={cp.group || ""} disabled={busy}
            onChange={async e => {
              const g = e.target.value;
              setBusy(true);
              try { await onChangeGroup(cp, g); } finally { setBusy(false); }
            }}
            style={{ width: "100%", fontSize: 11.5, fontWeight: 700, padding: "5px 8px", borderRadius: 8,
              background: cp.group ? (catColor(cp.group) + "22") : "var(--surface-2)",
              color: cp.group ? catColor(cp.group) : "var(--faint)",
              border: "1px solid " + (cp.group ? (catColor(cp.group) + "66") : "var(--border)"),
              cursor: busy ? "default" : "pointer", opacity: busy ? .6 : 1 }}>
            <option value="">— sans catégorie —</option>
            {(cp.group && groups.indexOf(cp.group) < 0 ? [cp.group, ...groups] : groups).map(g => <option key={g} value={g}>{g}</option>)}
          </select>
        </div>
        {/* Statut Plug IG — savoir en un coup d'œil si ce profil cloud est branché à un compte IG */}
        {(_curPhase || _lock.locked) && (
          <div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
            {_curPhase && <span title="Phase actuelle du parcours" style={{ fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "rgba(137,87,229,.14)", color: "#bc8cff", border: "1px solid rgba(137,87,229,.4)", whiteSpace: "nowrap" }}>🧭 {_curPhase.name}</span>}
            {_lock.locked && <CloudLockBadge lock={_lock} />}
          </div>
        )}
        <div style={{ marginTop: 8, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
          {acc
            ? (isPlug
                ? <span style={{ display: "inline-block", fontSize: 11, fontWeight: 700, padding: "3px 10px", borderRadius: 99, background: "rgba(52,211,153,.15)", color: "#34d399", border: "1px solid rgba(52,211,153,.4)", whiteSpace: "nowrap" }}>🔌 Plug</span>
                : <span style={{ display: "inline-block", fontSize: 11, fontWeight: 700, padding: "3px 10px", borderRadius: 99, background: "var(--surface-2)", color: "var(--faint)", border: "1px solid var(--border)", whiteSpace: "nowrap" }}>— Pas plug —</span>)
            : <span style={{ fontSize: 11, color: "var(--faint)" }}>— pas de compte —</span>}
          {acc && acc.username && <span style={{ fontSize: 11.5, color: "var(--muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }} title={"Compte @" + acc.username}>@{acc.username}</span>}
        </div>
      </div>
    </div>
  );
}

// Mappe un cloud phone MoreLogin -> objet "device" compris par DeviceView (interface iPhone)
function cloudToDevice(cp, screen) {
  // Phase du cloud phone (choisie dans la colonne « Phase », persistée par Backend.patchMeta dans
  // localStorage "farm_phases"). On la transmet à DeviceView pour qu'il applique le TEMPLATE de blocs
  // conçu dans le Phase Builder pour cette phase — même principe que les iPhones physiques.
  const _phase = (window.Backend && window.Backend._localPhases && window.Backend._localPhases[cp.id]) || "";
  return {
    udid: cp.id,
    id: cp.id,
    tag: cp.name || "Cloud phone",
    name: cp.androidId || cp.id,
    model: "Cloud",
    status: "online",     // on force online : DeviceView lance le stream (ml_ensure_connected allume au besoin)
    live: true,           // -> pipeline H.264 (même que HC BOX), routé côté backend vers le cloud
    battery: 100,
    proxy: cp.proxyId || undefined,        // proxy MoreLogin du profil (pour la rotation d'IP dans le pilote)
    proxyName: cp.proxyName || "",
    proxyUrl: cp.refreshUrl || "",         // URL de rotation (souvent null côté MoreLogin -> repli par nom)
    proxyCountry: cp.countryCode || "",
    operator: cp.operator || null,          // VA assigné à ce cloud phone (via operator_id / device-meta)
    operator_id: cp.operator_id || null,
    phase: _phase,        // -> getPhaseLayout(phase) dans PhaseColumn = le layout conçu au Phase Builder
    accounts: [],
    _cloud: true,
    _screen: (screen && screen.length === 2) ? screen : null,   // [w,h] réel -> cadre au bon ratio
  };
}

// Étapes de connexion (barre de progression déterministe pour rassurer l'utilisateur)
const CLOUD_STEPS = ["Démarrage…", "Allumage du cloud phone…", "Activation ADB…", "Établissement du tunnel sécurisé…", "Attente du démarrage d'Android…", "Prêt"];
function _stepIndex(msg) { const i = CLOUD_STEPS.indexOf(msg); return i < 0 ? 0 : i; }

// Écran de connexion cloud : poll /connect, affiche l'étape réelle, monte DeviceView quand prêt
function CloudConnecting({ cp, onReady, onBack }) {
  const { useState, useEffect, useRef } = React;
  const [prog, setProg] = useState({ state: "connecting", msg: "Démarrage…" });
  const [secs, setSecs] = useState(0);
  const [tryKey, setTryKey] = useState(0);     // bump -> relance une tentative (Réessayer)
  const onceReady = useRef(false);

  useEffect(() => {
    let on = true;
    setSecs(0);
    const poll = async () => {
      const r = await window.Backend.cloudphoneConnect(cp.id);
      if (!on) return;
      setProg(r || { state: "connecting", msg: "Connexion…" });
      if (r && r.state === "ready" && !onceReady.current) {
        onceReady.current = true;
        if (window.SoundBoard) {   // son "phone prêt / lancé" (notification)
          window.SoundBoard.playForce("cloud_ready");
          if (window.SoundBoard.genEleven) window.SoundBoard.genEleven("cloud_ready", "pleasant short success notification chime, positive ding", 1.5);
        }
        onReady(r.size);
      }
      // erreur (ex: solde insuffisant) -> on ARRÊTE de poller (pas de boucle), on attend "Réessayer"
      if (r && r.state === "error") { on = false; clearInterval(t); clearInterval(s); }
    };
    const t = setInterval(poll, 1500);
    const s = setInterval(() => on && setSecs(x => x + 1), 1000);
    poll();
    return () => { on = false; clearInterval(t); clearInterval(s); };
  }, [cp.id, tryKey]);

  const idx = _stepIndex(prog.msg);
  const pct = prog.state === "ready" ? 100 : Math.min(90, Math.round((idx / (CLOUD_STEPS.length - 1)) * 100) + Math.min(8, secs));
  const err = prog.state === "error";

  return (
    <div style={{ padding: 24, maxWidth: 460, margin: "40px auto 0" }}>
      <button onClick={onBack} style={{ padding: "8px 14px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginBottom: 24 }}>← Retour</button>
      <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 16, padding: "28px 26px", textAlign: "center" }}>
        <div style={{ fontSize: 40, marginBottom: 8 }}>☁️</div>
        <div style={{ fontSize: 17, fontWeight: 800, marginBottom: 4 }}>{cp.name || "Cloud phone"}</div>
        {err ? (
          <>
            <div style={{ color: "#f87171", fontSize: 13.5, margin: "14px 0", lineHeight: 1.5 }}>⚠️ {prog.msg || "Échec de connexion"}</div>
            <button onClick={async () => { onceReady.current = false; setProg({ state: "connecting", msg: "Démarrage…" }); await window.Backend.cloudphoneConnect(cp.id, true); setTryKey(k => k + 1); }}
              style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: "#238636", color: "#fff", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>↻ Réessayer</button>
          </>
        ) : (
          <>
            <div style={{ fontSize: 13, color: "var(--muted)", margin: "16px 0 14px", minHeight: 20, fontWeight: 600 }}>{prog.msg || "Connexion…"}</div>
            <div style={{ height: 8, borderRadius: 99, background: "var(--surface-2)", overflow: "hidden", marginBottom: 10 }}>
              <div style={{ height: "100%", width: pct + "%", background: "linear-gradient(90deg,#3b82f6,#34d399)", borderRadius: 99, transition: "width .5s" }} />
            </div>
            <div className="mono" style={{ fontSize: 11, color: "var(--faint)" }}>{pct}% · {secs}s · étape {idx + 1}/{CLOUD_STEPS.length}</div>
            <div style={{ fontSize: 11.5, color: "var(--faint)", marginTop: 14, lineHeight: 1.5 }}>Première connexion : ~15-40s (tunnel sécurisé + démarrage Android). Les suivantes sont instantanées.</div>
          </>
        )}
      </div>
    </div>
  );
}

// Détail : DEVICEVIEW EXACT (comme Pilotage iPhone) sur un cloud phone.
// Le panneau s'affiche DIRECTEMENT (phone éteint) — le démarrage n'est PLUS automatique : l'utilisateur
// clique « ▶ Allumer le cloud phone » (dans DeviceView), ce qui affiche l'overlay de progression ci-dessous
// jusqu'à ce que le phone soit prêt, puis l'écran live apparaît.
function CloudPilotDetail({ cp, onBack, user }) {
  const { useEffect, useMemo, useState, useReducer } = React;
  const [launched, setLaunched] = useState(false);     // lancé dans CETTE session ? -> pilote le streaming
  const [launching, setLaunching] = useState(false);   // overlay « démarrage du cloud phone » affiché ?
  const [screen, setScreen] = useState(null);          // [w,h] réel du cloud phone (récupéré à la 1re connexion)
  const [bypassed, setBypassed] = useState(false);     // Owner a forcé l'ouverture malgré le verrou
  const [, _tick] = useReducer(x => x + 1, 0);
  // Phase LIVE du cloud phone : le parcours (pcApplyTemplate) écrit dans Backend._localPhases via patchMeta
  // SANS émettre d'événement -> on la relit à chaque render (le _tick 1s force le refresh) et on l'inclut
  // dans les deps du memo pour que le layout (getPhaseLayout) suive la phase appliquée par le parcours.
  const _livePhase = (window.Backend && window.Backend._localPhases && window.Backend._localPhases[cp.id]) || "";
  const device = useMemo(() => cloudToDevice(cp, screen), [cp.id, screen && screen.join("x"), _livePhase]);
  useEffect(() => {
    // keepalive (anti auto-off) SEULEMENT une fois lancé — avant, on ne touche pas au phone.
    if (!launched) return;
    const ka = setInterval(() => { if (window.Backend.cloudphoneKeepalive) window.Backend.cloudphoneKeepalive(cp.id); }, 20000);
    return () => clearInterval(ka);
  }, [cp.id, launched]);
  useEffect(() => { const id = setInterval(_tick, 1000); return () => clearInterval(id); }, []);   // countdown du verrou

  // Verrou de parcours (cooldown / plage horaire) : ça SORT du panneau tant que le profil est verrouillé.
  const _prog = window.ParcoursProgress ? window.ParcoursProgress.get(cp.id) : null;
  const _parc = (_prog && window.ParcoursStore) ? window.ParcoursStore.get(_prog.parcoursId) : null;
  const lock = (_parc && !_prog.done && window.pcLockState) ? window.pcLockState(_parc, _prog) : { locked: false };
  const isOwner = window.ROLE_RANK ? window.ROLE_RANK(user && user.role) >= 3 : false;
  if (lock.locked && !bypassed) {
    return (
      <div style={{ padding: 24, maxWidth: 440, margin: "40px auto 0" }}>
        <button onClick={onBack} style={{ padding: "8px 14px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 700, fontSize: 12.5, cursor: "pointer", marginBottom: 22 }}>← Retour</button>
        <div style={{ background: "var(--surface)", border: "1px solid rgba(240,160,32,.35)", borderRadius: 16, padding: "30px 26px", textAlign: "center" }}>
          <div style={{ fontSize: 44, marginBottom: 10 }}>🔒</div>
          <div style={{ fontSize: 17, fontWeight: 800, color: "#f0a020", marginBottom: 6 }}>{cp.name || "Cloud phone"} verrouillé</div>
          {lock.reason === "cooldown"
            ? <div style={{ fontSize: 13.5, color: "var(--muted)" }}>En cooldown de parcours — revient dans <b className="mono">{_fmtLockMs(Math.max(0, lock.until - Date.now()))}</b></div>
            : <div style={{ fontSize: 13.5, color: "var(--muted)" }}>Hors plage horaire · <b className="mono">{lock.window.start}–{lock.window.end}</b> 🇺🇸LA · <b className="mono">{window.pcConvertWall ? window.pcConvertWall(lock.window.start, "America/Los_Angeles", "Europe/Paris") : lock.window.start}–{window.pcConvertWall ? window.pcConvertWall(lock.window.end, "America/Los_Angeles", "Europe/Paris") : lock.window.end}</b> 🇫🇷Paris</div>}
          {isOwner && (
            <button onClick={() => { if (window.confirm("⚠️ Es-tu sûr de vouloir ouvrir malgré le verrou ? (déconseillé, action Owner)")) { setBypassed(true); if (window.ParcoursProgress) window.ParcoursProgress.patch(cp.id, p => { p.overrides = p.overrides || []; p.overrides.push({ by: (window.__FARM_USER__ && window.__FARM_USER__.name) || "?", at: Date.now(), reason: "ouverture panneau forcée" }); p._winOverrideUntil = Date.now() + 2 * 3600 * 1000; p.cooldownUntil = null; }); } }}
              style={{ marginTop: 16, padding: "9px 18px", borderRadius: 9, border: "1px solid rgba(248,81,73,.4)", background: "rgba(248,81,73,.1)", color: "#f87171", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>🔓 Ouvrir quand même (Owner)</button>
          )}
        </div>
      </div>
    );
  }

  if (!window.DeviceView) return <div style={{ padding: 40, color: "var(--muted)" }}>DeviceView indisponible.</div>;
  return (
    <>
      {React.createElement(window.DeviceView, { device, user, admin: true, onBack, cloudLaunched: launched, onCloudLaunch: () => setLaunching(true) })}
      {launching && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(2,6,15,.8)", backdropFilter: "blur(3px)", zIndex: 600, display: "grid", placeItems: "center", overflowY: "auto" }}>
          <div style={{ width: "min(460px, 92vw)" }}>
            <CloudConnecting cp={cp}
              onReady={(sz) => { if (sz) setScreen(sz); setLaunched(true); setLaunching(false); }}
              onBack={() => setLaunching(false)} />
          </div>
        </div>
      )}
    </>
  );
}

function PhoneCloudPilotage({ user }) {
  const { useState, useEffect, useRef } = React;
  const [list, setList] = useState([]);
  const [ready, setReady] = useState(false);
  const [q, setQ] = useState("");
  const [cat, setCat] = useState("");   // filtre catégorie (groupe MoreLogin)
  const [openId, setOpenId] = useState(null);
  const [adding, setAdding] = useState(false);
  const [mlGroups, setMlGroups] = useState([]);   // tous les groupes MoreLogin (même ceux sans profil actuellement)
  const [catOrder, setCatOrder] = useState(() => { try { return JSON.parse(localStorage.getItem(CLOUD_CAT_ORDER_KEY) || "[]"); } catch (e) { return []; } });
  const [dragOverCat, setDragOverCat] = useState(null);
  const dragCatIdx = useRef(null);
  const mounted = useRef(true);

  const refresh = async () => {
    if (!window.Backend || !window.Backend.cloudphones) return;
    try { const l = await window.Backend.cloudphones(); if (mounted.current) { setList(l || []); setReady(true); } } catch (e) {}
  };
  useEffect(() => {
    mounted.current = true; refresh(); const t = setInterval(refresh, 8000);
    if (window.Backend && window.Backend.cloudphoneGroups) {
      window.Backend.cloudphoneGroups().then(r => {
        if (!mounted.current) return;
        const gs = ((r && r.groups) || r || []).map(g => typeof g === "string" ? g : (g && (g.name || g.groupName || g.label))).filter(Boolean);
        setMlGroups(gs);
      }).catch(() => {});
    }
    return () => { mounted.current = false; clearInterval(t); };
  }, []);

  // Change la catégorie d'un cloud phone : MoreLogin (source de vérité groupe) + Stockage de comptes
  // (si un compte est attribué à ce profil, sa colonne « Catégorie » suit pour rester cohérente).
  const changeGroup = async (cp, newGroup) => {
    setList(prev => prev.map(c => c.id === cp.id ? { ...c, group: newGroup } : c));   // optimiste
    if (window.Backend && window.Backend.cloudphoneSetGroup) {
      try { await window.Backend.cloudphoneSetGroup(cp.id, newGroup); } catch (e) {}
    }
    const S = window.AccountCreationStore;
    if (S && S.setAttributedCategory) { try { S.setAttributedCategory(String(cp.id), newGroup); } catch (e) {} }
    refresh();
  };

  const openCp = openId ? list.find(c => c.id === openId) : null;
  if (openCp) return <CloudPilotDetail cp={openCp} user={user} onBack={() => { refresh(); setOpenId(null); }} />;

  const online = list.filter(c => c.envStatus === 4).length;
  const rawCats = [...new Set([...list.map(c => c.group), ...mlGroups].filter(Boolean))].sort();
  // applique l'ordre personnalisé (glisser-déposer) : d'abord les catégories rangées, puis les nouvelles à la fin
  const cats = (() => {
    const known = catOrder.filter(c => rawCats.includes(c));
    const rest = rawCats.filter(c => !known.includes(c));
    return [...known, ...rest];
  })();
  const filtered = list.filter(c => (c.name || "").toLowerCase().includes(q.toLowerCase()) && (!cat || c.group === cat));

  const reorderCats = (from, to) => {
    if (from == null || to == null || from === to) return;
    const arr = [...cats];
    const [moved] = arr.splice(from, 1);
    arr.splice(to, 0, moved);
    setCatOrder(arr);
    try { localStorage.setItem(CLOUD_CAT_ORDER_KEY, JSON.stringify(arr)); } catch (e) {}
  };

  const addProfile = async () => {
    setAdding(true);
    const r = window.Backend.cloudphoneOpenApp ? await window.Backend.cloudphoneOpenApp() : { ok: false };
    setAdding(false);
    if (!r.ok) alert(r.msg || "Ouvre l'app MoreLogin pour créer un profil, puis reviens ici (Refresh).");
  };

  return (
    <div style={{ padding: 24, maxWidth: 1100, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", flexWrap: "wrap", gap: 12, marginBottom: 18 }}>
        <div>
          <div style={{ fontSize: 21, fontWeight: 800 }}>☁️ Pilotage cloud</div>
          <div style={{ fontSize: 13, color: "var(--faint)", marginTop: 4 }}>
            prenez la main sur n'importe quel cloud phone · {online} allumé(s) sur {list.length}
          </div>
        </div>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Rechercher un cloud phone…"
            style={{ padding: "9px 12px", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 9, color: "var(--text)", fontSize: 13, outline: "none", width: 220 }} />
          <button onClick={addProfile} disabled={adding}
            style={{ padding: "9px 14px", borderRadius: 9, border: "none", background: "#238636", color: "#fff", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>
            {adding ? "…" : "➕ Ajouter un profil"}
          </button>
          <button onClick={refresh}
            style={{ padding: "9px 14px", borderRadius: 9, background: "var(--surface)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 600, fontSize: 13, cursor: "pointer" }}>↻ Refresh</button>
        </div>
      </div>

      {/* Filtre par catégorie (groupe MoreLogin) — glisse les chips à gauche/droite pour les réordonner */}
      {cats.length > 0 && (
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 16 }}>
          {/* « Toutes » toujours en tête, non déplaçable */}
          <button onClick={() => setCat("")} data-sfx="none"
            style={{ padding: "6px 14px", borderRadius: 99, fontSize: 12.5, fontWeight: 700, cursor: "pointer",
              border: "1px solid " + (cat === "" ? "var(--accent)" : "var(--border)"),
              background: cat === "" ? "var(--accent-soft)" : "var(--surface)",
              color: cat === "" ? "var(--accent)" : "var(--muted)" }}>
            Toutes <span style={{ opacity: .6, fontWeight: 500 }}>{list.length}</span>
          </button>
          {cats.map((c, i) => {
            const active = cat === c;
            const cnt = list.filter(x => x.group === c).length;
            const over = dragOverCat === i;
            return (
              <button key={c} onClick={() => setCat(c)} data-sfx="none"
                draggable
                onDragStart={e => { dragCatIdx.current = i; e.dataTransfer.effectAllowed = "move"; }}
                onDragOver={e => { e.preventDefault(); if (dragOverCat !== i) setDragOverCat(i); }}
                onDragLeave={() => setDragOverCat(o => o === i ? null : o)}
                onDrop={e => { e.preventDefault(); reorderCats(dragCatIdx.current, i); dragCatIdx.current = null; setDragOverCat(null); }}
                onDragEnd={() => { dragCatIdx.current = null; setDragOverCat(null); }}
                title="Glisse pour réordonner"
                style={{ padding: "6px 14px", borderRadius: 99, fontSize: 12.5, fontWeight: 700, cursor: "grab",
                  border: "1px solid " + (over ? "var(--accent)" : active ? "var(--accent)" : "var(--border)"),
                  background: active ? "var(--accent-soft)" : "var(--surface)",
                  color: active ? "var(--accent)" : "var(--muted)",
                  boxShadow: over ? "inset 3px 0 0 var(--accent)" : "none",
                  transition: "box-shadow .12s, border-color .12s" }}>
                {c} <span style={{ opacity: .6, fontWeight: 500 }}>{cnt}</span>
              </button>
            );
          })}
        </div>
      )}

      {(ready && list.length === 0) ? (
        <div style={{ textAlign: "center", padding: "60px 0", color: "var(--faint)", border: "1px dashed var(--border)", borderRadius: 12 }}>
          <div style={{ fontSize: 34, marginBottom: 12 }}>☁️</div>
          <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)", marginBottom: 6 }}>Aucun cloud phone détecté</div>
          <div style={{ fontSize: 12.5 }}>Ouvre MoreLogin (bouton « Ajouter un profil ») puis Refresh.</div>
        </div>
      ) : filtered.length === 0 ? (
        <div style={{ textAlign: "center", padding: "70px 0", color: "var(--faint)" }}>Aucun cloud phone ne correspond.</div>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(210px, 1fr))", gap: 16 }}>
          {filtered.map(cp => <CloudCard key={cp.id} cp={cp} onOpen={() => setOpenId(cp.id)} groups={cats} onChangeGroup={changeGroup} />)}
        </div>
      )}
    </div>
  );
}

window.PhoneCloudPilotage = PhoneCloudPilotage;
window.PhoneCloudPilot = PhoneCloudPilotage;   // alias rétro-compat

// ---- Overviews ----
function _Kpi({ label, value, sub, color }) {
  return (
    <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, padding: "18px 20px", minWidth: 150, flex: 1 }}>
      <div style={{ fontSize: 11, color: "var(--faint)", fontWeight: 700, textTransform: "uppercase", letterSpacing: ".05em" }}>{label}</div>
      <div style={{ fontSize: 30, fontWeight: 800, color: color || "var(--text)", marginTop: 6 }}>{value}</div>
      {sub && <div style={{ fontSize: 11.5, color: "var(--faint)", marginTop: 2 }}>{sub}</div>}
    </div>
  );
}

// Petit graphique à barres horizontales (autonome, sans dépendance)
function _MiniBars({ items }) {
  const max = Math.max(1, ...items.map(i => i.value));
  return (
    <div style={{ display: "grid", gap: 9 }}>
      {items.map((it, i) => (
        <div key={i} style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <div title={it.label} style={{ width: 120, fontSize: 12, color: "var(--muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: "none" }}>{it.label}</div>
          <div style={{ flex: 1, height: 18, background: "var(--surface-2)", borderRadius: 6, overflow: "hidden" }}>
            <div style={{ width: Math.max(2, it.value / max * 100) + "%", height: "100%", background: it.color || "#3b82f6", borderRadius: 6, transition: "width .4s" }} />
          </div>
          <div className="mono" style={{ width: 78, textAlign: "right", fontSize: 11.5, color: "var(--faint)", flex: "none" }}>{it.display}</div>
        </div>
      ))}
      {items.length === 0 && <div style={{ color: "var(--faint)", fontSize: 12.5, textAlign: "center", padding: 24 }}>Aucune donnée pour l'instant.</div>}
    </div>
  );
}

function _Card({ title, sub, children }) {
  return (
    <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, padding: "18px 20px", marginTop: 16 }}>
      <div style={{ fontSize: 14.5, fontWeight: 700, marginBottom: sub ? 2 : 14 }}>{title}</div>
      {sub && <div style={{ fontSize: 11.5, color: "var(--faint)", marginBottom: 14 }}>{sub}</div>}
      {children}
    </div>
  );
}

function OverviewCloud() {
  const { useState, useEffect } = React;
  const [list, setList] = useState([]);
  const [usage, setUsage] = useState({ usage: {}, total_seconds: 0, cost_usd: 0, rate_per_min: 0.006 });
  useEffect(() => {
    let on = true;
    const load = () => {
      if (window.Backend.cloudphones) window.Backend.cloudphones().then(l => { if (on) setList(l || []); }).catch(() => {});
      if (window.Backend.cloudphoneUsage) window.Backend.cloudphoneUsage().then(u => { if (on && u) setUsage(u); }).catch(() => {});
    };
    load(); const t = setInterval(load, 10000); return () => { on = false; clearInterval(t); };
  }, []);
  const onCount = list.filter(c => c.envStatus === 4).length;
  const off = list.length - onCount;
  const costH = (onCount * 0.006 * 60).toFixed(2);
  const totalMin = Math.round((usage.total_seconds || 0) / 60);
  // graphique : temps d'activité cumulé par cloud phone (top 10)
  const bars = Object.values(usage.usage || {})
    .map(u => ({ name: u.name || "?", mins: Math.round((u.seconds || 0) / 60) }))
    .sort((a, b) => b.mins - a.mins).slice(0, 10)
    .map(u => ({ label: u.name, value: u.mins, display: u.mins + " min · $" + (u.mins * 0.006).toFixed(2), color: "#3b82f6" }));
  return (
    <div style={{ padding: 24, maxWidth: 1000, margin: "0 auto" }}>
      <div style={{ fontSize: 21, fontWeight: 800, marginBottom: 4 }}>☁️ Overview — Phone Cloud</div>
      <div style={{ fontSize: 13, color: "var(--faint)", marginBottom: 18 }}>MoreLogin · $0,006/min quand allumé · auto-off 5 min</div>
      <div style={{ display: "flex", gap: 14, flexWrap: "wrap" }}>
        <_Kpi label="Total" value={list.length} />
        <_Kpi label="Allumés" value={onCount} color="#34d399" sub={onCount ? "~$" + costH + "/h en cours" : "—"} />
        <_Kpi label="Éteints" value={off} color="#8b949e" />
        <_Kpi label="Temps cumulé" value={totalMin >= 60 ? (totalMin / 60).toFixed(1) + " h" : totalMin + " min"} sub="depuis le début" color="#9cc4f0" />
        <_Kpi label="Coût réel cumulé" value={"$" + (usage.cost_usd || 0).toFixed(2)} sub="facturation cloud" color="#f0a020" />
      </div>
      <_Card title="⏱ Temps d'activité par cloud phone" sub="temps allumé cumulé · coût réel ($0,006/min) — data facturation & analytics">
        <_MiniBars items={bars} />
      </_Card>
    </div>
  );
}

function OverviewIphone() {
  const { useState, useEffect } = React;
  const [list, setList] = useState([]);
  useEffect(() => {
    let on = true;
    const load = () => { if (window.Backend && window.Backend.devices) window.Backend.devices().then(l => { if (on) setList(l || []); }).catch(() => {}); };
    load(); const t = setInterval(load, 10000); return () => { on = false; clearInterval(t); };
  }, []);
  const cnt = (s) => list.filter(d => d.status === s).length;
  const online = cnt("online"), busy = cnt("busy"), locked = cnt("locked"), offline = list.length - online - busy - locked;
  const bars = [
    { label: "🟢 En ligne", value: online, display: String(online), color: "#34d399" },
    { label: "🟠 Occupé", value: busy, display: String(busy), color: "#f0a020" },
    { label: "🔒 Verrouillé", value: locked, display: String(locked), color: "#8b949e" },
    { label: "⚫ Hors-ligne", value: offline, display: String(offline), color: "#6e7681" },
  ];
  return (
    <div style={{ padding: 24, maxWidth: 1000, margin: "0 auto" }}>
      <div style={{ fontSize: 21, fontWeight: 800, marginBottom: 4 }}>📱 Overview — iPhone physique</div>
      <div style={{ fontSize: 13, color: "var(--faint)", marginBottom: 18 }}>Parc physique (HC BOX / go-ios)</div>
      <div style={{ display: "flex", gap: 14, flexWrap: "wrap" }}>
        <_Kpi label="Total iPhones" value={list.length} />
        <_Kpi label="En ligne" value={online} color="#34d399" />
        <_Kpi label="Hors-ligne" value={list.length - online} color="#8b949e" />
      </div>
      <_Card title="📊 Répartition du parc iPhone" sub="par statut · temps réel">
        <_MiniBars items={bars} />
      </_Card>
    </div>
  );
}

window.OverviewCloud = OverviewCloud;
window.OverviewIphone = OverviewIphone;
