/* global React, autoLoadFlows */
// ============================================================
// Campagne d'automatisation — orchestration de flux dans le temps
// Phase 1 : store + liste + éditeur (Planification + étapes Flux). Moteur = Phase 2.
// ============================================================

const CAMPAIGNS_KEY = "phonelabs.campaigns";

const CampaignStore = {
  load() { try { return JSON.parse(localStorage.getItem(CAMPAIGNS_KEY)) || []; } catch (e) { return []; } },
  save(list) { try { localStorage.setItem(CAMPAIGNS_KEY, JSON.stringify(list)); } catch (e) {} },
  add() {
    const c = { id: "camp_" + Date.now(), name: "Nouvelle campagne", createdAt: new Date().toISOString(),
      schedule: { enabled: true, inputTz: "Europe/Paris", days: [1, 2, 3, 4, 5], times: ["09:00"] }, steps: [] };
    const list = [...this.load(), c]; this.save(list); return c;
  },
  update(id, patch) { const list = this.load().map(c => c.id === id ? { ...c, ...patch, updatedAt: new Date().toISOString() } : c); this.save(list); return list; },
  remove(id) { const list = this.load().filter(c => c.id !== id); this.save(list); return list; },
  dup(c) { const copy = { ...c, id: "camp_" + Date.now(), name: c.name + " (copie)", createdAt: new Date().toISOString(), steps: (c.steps || []).map(s => ({ ...s })) }; const list = [...this.load(), copy]; this.save(list); return copy; },
};

// ─── Fuseaux horaires ───
const CAMP_TZS = [
  { id: "Europe/Paris", label: "🇫🇷 France (Paris)" },
  { id: "America/Los_Angeles", label: "🇺🇸 Los Angeles (Pacifique)" },
  { id: "America/New_York", label: "🇺🇸 New York (Est)" },
  { id: "Europe/London", label: "🇬🇧 Londres" },
];
const CAMP_TARGET_TZ = "America/Los_Angeles";
const CAMP_DAYS = [["Lun", 1], ["Mar", 2], ["Mer", 3], ["Jeu", 4], ["Ven", 5], ["Sam", 6], ["Dim", 0]];

function _tzParts(date, tz) {
  const dtf = new Intl.DateTimeFormat("en-US", { timeZone: tz, hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" });
  const o = {}; dtf.formatToParts(date).forEach(p => { if (p.type !== "literal") o[p.type] = p.value; }); return o;
}
function _tzOffsetMin(date, tz) {
  const p = _tzParts(date, tz);
  const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
  return (asUTC - date.getTime()) / 60000;
}
// Convertit une heure "HH:MM" (mur) d'un fuseau vers un autre (réf = aujourd'hui, gère l'offset DST courant)
function convertWallTime(hhmm, fromTz, toTz) {
  if (!/^\d{1,2}:\d{2}$/.test(hhmm || "")) return "";
  const [h, m] = hhmm.split(":").map(Number);
  const now = new Date();
  const guess = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0);
  const off = _tzOffsetMin(new Date(guess), fromTz);
  const instant = guess - off * 60000;
  const tp = _tzParts(new Date(instant), toTz);
  return String(tp.hour).padStart(2, "0") + ":" + String(tp.minute).padStart(2, "0");
}

// ─── Moteur d'exécution (Phase 2) ───
// Handoff via localStorage : on écrit pl.campaign.run, on ouvre le popup du flux (qui s'auto-lance
// sur le device), puis on attend pl.campaign.done.<signalId>. Tourne tant que l'app est ouverte.
function _campWaitSignal(key, timeoutMs) {
  return new Promise(resolve => {
    const start = Date.now();
    const iv = setInterval(() => {
      let v = null; try { v = localStorage.getItem(key); } catch (e) {}
      if (v) { clearInterval(iv); try { localStorage.removeItem(key); } catch (e) {} let ok = true; try { ok = JSON.parse(v).ok !== false; } catch (e) {} resolve(ok); }
      else if (Date.now() - start > timeoutMs) { clearInterval(iv); resolve(false); }
    }, 1000);
  });
}
function _campOpenFlow(flowId, device, signalId) {
  // PARALLÈLE : device + signalId passés dans l'URL → chaque popup est autonome (on peut en lancer N à la fois).
  if (device || signalId) {
    const url = "http://127.0.0.1:8787/?popup=automation&flow=" + encodeURIComponent(flowId)
      + (device ? "&device=" + encodeURIComponent(device) : "")
      + (signalId ? "&signal=" + encodeURIComponent(signalId) : "");
    if (window.electronGoogle && window.electronGoogle.openAutomationPopupUrl) window.electronGoogle.openAutomationPopupUrl(url);
    else window.open(url, "cflow_" + (signalId || flowId), "width=1120,height=800");
    return;
  }
  // SÉQUENTIEL (ancien) : handoff via localStorage (pl.campaign.run)
  if (window.electronGoogle && window.electronGoogle.openAutomationPopup) window.electronGoogle.openAutomationPopup(flowId);
  else window.open("http://127.0.0.1:8787/?popup=automation&flow=" + encodeURIComponent(flowId), "cflow_" + flowId, "width=1180,height=820");
}

// Exécute un flux sur un POOL de téléphones EN PARALLÈLE, par vagues (plafond = parallélisme = nb de proxys dispo).
// onProgress({status, ...}) — status: empty|wave|run|ok|timeout|cooldown|finished
async function runCampaignParallel(campaign, onProgress) {
  const p = campaign.parallel || {};
  const flowId = p.flowId;
  const targets = (p.targets || []).filter(Boolean);
  const conc = Math.max(1, parseInt(p.parallelism || 3) || 3);
  const cooldownMs = Math.max(0, parseInt(p.cooldownSec || 0) || 0) * 1000;
  if (!flowId || !targets.length) { onProgress && onProgress({ status: "empty" }); return; }
  let done = 0; const total = targets.length; const results = [];
  const waves = Math.ceil(total / conc);
  for (let i = 0; i < total; i += conc) {
    const wave = targets.slice(i, i + conc);
    onProgress && onProgress({ status: "wave", wave: Math.floor(i / conc) + 1, waves, size: wave.length, done, total });
    await Promise.all(wave.map(async (udid) => {
      const signalId = "par_" + i + "_" + String(udid).slice(-5) + "_" + Date.now();
      onProgress && onProgress({ status: "run", udid, done, total });
      _campOpenFlow(flowId, udid, signalId);
      const ok = await _campWaitSignal("pl.campaign.done." + signalId, 12 * 60 * 1000);
      done++; results.push({ udid, ok });
      onProgress && onProgress({ status: ok ? "ok" : "timeout", udid, done, total });
    }));
    if (i + conc < total && cooldownMs > 0) {
      onProgress && onProgress({ status: "cooldown", ms: cooldownMs, done, total });
      await new Promise(r => setTimeout(r, cooldownMs));
    }
  }
  onProgress && onProgress({ status: "finished", total, results, okCount: results.filter(r => r.ok).length });
}
window.runCampaignParallel = runCampaignParallel;
// Exécute les étapes en séquentiel. onStep({index,total,step,status}) — status: running|done|timeout|finished|empty
async function runCampaignSteps(campaign, onStep) {
  const steps = (campaign.steps || []).filter(s => s.flowId && s.deviceUdid);
  const total = steps.length;
  if (!total) { onStep && onStep({ status: "empty" }); return; }
  for (let i = 0; i < total; i++) {
    const s = steps[i];
    const signalId = "sig_" + i + "_" + (campaign.id || "c") + "_" + Date.now();
    onStep && onStep({ index: i, total, step: s, status: "running" });
    try { localStorage.setItem("pl.campaign.run", JSON.stringify({ flowId: s.flowId, device: s.deviceUdid, signalId, at: Date.now() })); } catch (e) {}
    _campOpenFlow(s.flowId);
    const ok = await _campWaitSignal("pl.campaign.done." + signalId, 8 * 60 * 1000);
    onStep && onStep({ index: i, total, step: s, status: ok ? "done" : "timeout" });
  }
  onStep && onStep({ status: "finished", total });
}
window.runCampaignSteps = runCampaignSteps;

// Scheduler global : déclenche les campagnes activées à leurs horaires (tant que l'app est ouverte).
function CampaignScheduler() {
  React.useEffect(() => {
    let busy = false;
    const tick = async () => {
      if (busy) return;
      let camps = []; try { camps = CampaignStore.load(); } catch (e) { return; }
      const now = new Date();
      for (const c of camps) {
        const sch = c.schedule; if (!sch || sch.enabled === false) continue;
        const tz = sch.inputTz || "Europe/Paris";
        let p; try { p = _tzParts(now, tz); } catch (e) { continue; }
        const dateKey = p.year + "-" + p.month + "-" + p.day;
        const dow = new Date(Date.UTC(+p.year, +p.month - 1, +p.day)).getUTCDay(); // 0=dim..6=sam
        if (!(sch.days || []).includes(dow)) continue;
        const hhmm = p.hour + ":" + p.minute;
        for (const t of (sch.times || [])) {
          if (t !== hhmm) continue;
          const fireKey = "pl.campaign.fired." + c.id + "." + t + "." + dateKey;
          let already = false; try { already = !!localStorage.getItem(fireKey); } catch (e) {}
          if (already) continue;
          try { localStorage.setItem(fireKey, "1"); } catch (e) {}
          busy = true;
          try { await runCampaignSteps({ ...c }, () => {}); } catch (e) {}
          busy = false;
        }
      }
    };
    const iv = setInterval(tick, 30000);
    tick();
    return () => clearInterval(iv);
  }, []);
  return null;
}
window.CampaignScheduler = CampaignScheduler;

// ─── Root : liste ↔ éditeur ───
function CampaignBuilder() {
  const { useState } = React;
  const [campaigns, setCampaigns] = useState(CampaignStore.load);
  const [editing, setEditing] = useState(null); // campaign id
  const refresh = () => setCampaigns(CampaignStore.load());

  const cur = editing ? campaigns.find(c => c.id === editing) : null;
  if (cur) return <CampaignEditor campaign={cur} onBack={() => { refresh(); setEditing(null); }} onChange={refresh} />;

  return (
    <div style={{ padding: 24, maxWidth: 1020, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "center", marginBottom: 22 }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 700, color: "#e6edf3" }}>🗓️ Campagnes d'automatisation</div>
          <div style={{ fontSize: 12, color: "#8b949e", marginTop: 3 }}>Orchestre plusieurs flux dans le temps · planification récurrente · plusieurs iPhones</div>
        </div>
        <button onClick={() => { const c = CampaignStore.add(); refresh(); setEditing(c.id); }}
          style={{ marginLeft: "auto", padding: "8px 18px", background: "#238636", border: "none", borderRadius: 8, color: "#fff", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>
          + Nouvelle campagne
        </button>
      </div>
      {campaigns.length === 0 ? (
        <div style={{ textAlign: "center", padding: "72px 0", color: "#8b949e", border: "1px dashed #30363d", borderRadius: 12 }}>
          <div style={{ fontSize: 38, marginBottom: 14 }}>🗓️</div>
          <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 6, color: "#e6edf3" }}>Aucune campagne</div>
          <div style={{ fontSize: 12, marginBottom: 18 }}>Crée une campagne pour enchaîner tes flux automatiquement à des horaires précis</div>
        </div>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(270px, 1fr))", gap: 14 }}>
          {campaigns.map(c => <CampaignCard key={c.id} campaign={c}
            onEdit={() => setEditing(c.id)}
            onDelete={() => { CampaignStore.remove(c.id); refresh(); }}
            onDuplicate={() => { CampaignStore.dup(c); refresh(); }}
            onRename={(name) => { CampaignStore.update(c.id, { name }); refresh(); }} />)}
        </div>
      )}
    </div>
  );
}

function CampaignCard({ campaign, onEdit, onDelete, onDuplicate, onRename }) {
  const { useState } = React;
  const [editing, setEditing] = useState(false);
  const [nm, setNm] = useState(campaign.name || "");
  const save = () => { onRename((nm.trim() || campaign.name || "Sans nom")); setEditing(false); };
  const sch = campaign.schedule || {};
  const nSteps = (campaign.steps || []).length;
  return (
    <div style={{ background: "#161b22", border: "1px solid #30363d", borderRadius: 10, padding: 14, display: "flex", flexDirection: "column", gap: 10 }}
      onMouseEnter={e => e.currentTarget.style.borderColor = "#8957e5"} onMouseLeave={e => e.currentTarget.style.borderColor = "#30363d"}>
      <div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          {editing
            ? <input autoFocus value={nm} data-sfx="none" onChange={e => setNm(e.target.value)}
                onKeyDown={e => { if (e.key === "Enter") save(); if (e.key === "Escape") { setNm(campaign.name || ""); setEditing(false); } }} onBlur={save}
                style={{ width: "100%", fontSize: 13, fontWeight: 700, background: "#0d1117", border: "1px solid #8957e5", borderRadius: 5, color: "#e6edf3", padding: "3px 6px", outline: "none", boxSizing: "border-box" }} />
            : <div onDoubleClick={() => { setNm(campaign.name || ""); setEditing(true); }} title="Double-clic pour renommer"
                style={{ fontSize: 13, fontWeight: 700, color: "#e6edf3", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", cursor: "text" }}>{campaign.name || "Sans nom"}</div>}
          <div style={{ fontSize: 10, color: "#8b949e", marginTop: 2 }}>{nSteps} étape(s) · {(sch.times || []).length} horaire(s)/jour · {(sch.days || []).length} jour(s)</div>
        </div>
        <div style={{ display: "flex", gap: 4, flex: "none" }}>
          <button onClick={() => { setNm(campaign.name || ""); setEditing(true); }} title="Renommer" data-sfx="none" style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 5, color: "#8b949e", cursor: "pointer", padding: "3px 7px", fontSize: 11 }}>✎</button>
          <button onClick={onDuplicate} title="Dupliquer" style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 5, color: "#8b949e", cursor: "pointer", padding: "3px 7px", fontSize: 11 }}>⎘</button>
          <button onClick={onDelete} title="Supprimer" style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 5, color: "#f85149", cursor: "pointer", padding: "3px 7px", fontSize: 11 }}>✕</button>
        </div>
      </div>
      <button onClick={onEdit}
        style={{ padding: "8px 0", background: "transparent", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontWeight: 700, fontSize: 12, cursor: "pointer" }}
        onMouseEnter={e => { e.currentTarget.style.background = "#21262d"; e.currentTarget.style.borderColor = "#8957e5"; }}
        onMouseLeave={e => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.borderColor = "#30363d"; }}>
        ✏️ Éditer la campagne
      </button>
    </div>
  );
}

// ─── Éditeur d'une campagne : Planification + étapes flux ───
function CampaignEditor({ campaign, onBack, onChange }) {
  const { useState, useEffect } = React;
  const [name, setName] = useState(campaign.name || "");
  const [schedule, setSchedule] = useState(campaign.schedule || { enabled: true, inputTz: "Europe/Paris", days: [1, 2, 3, 4, 5], times: ["09:00"] });
  const [steps, setSteps] = useState(campaign.steps || []);
  const [flows] = useState(() => (typeof autoLoadFlows === "function" ? autoLoadFlows() : []));
  const [devices, setDevices] = useState([]);
  const [running, setRunning] = useState(null);
  // ── Mode parallèle (campagnes de flux à grande échelle) ──
  const [mode, setMode] = useState(campaign.mode || "sequence");
  const [parallel, setParallel] = useState(campaign.parallel || { flowId: "", targets: [], parallelism: 3, cooldownSec: 60 });
  const [cloudDevs, setCloudDevs] = useState([]);
  const [prun, setPrun] = useState(null);

  useEffect(() => { if (window.Backend && window.Backend.devices) window.Backend.devices().then(d => { if (d) setDevices(d); }).catch(() => {}); }, []);
  useEffect(() => { if (window.Backend && window.Backend.cloudphones) window.Backend.cloudphones().then(l => setCloudDevs((l || []).map(c => ({ udid: String(c.id), tag: c.name || String(c.id), group: c.group || "", status: c.envStatus === 4 ? "online" : "offline" })))).catch(() => {}); }, []);
  const pRunning = prun && prun.status !== "finished" && prun.status !== "empty";
  async function launchParallel() {
    if (pRunning) return;
    setPrun({ status: "starting" });
    await runCampaignParallel({ ...campaign, parallel }, ev => setPrun(ev));
  }
  const toggleTarget = (udid) => setParallel(p => ({ ...p, targets: (p.targets || []).includes(udid) ? p.targets.filter(x => x !== udid) : [...(p.targets || []), udid] }));

  const isRunning = running && (running.status === "running" || running.status === "starting");
  async function launchNow() {
    if (isRunning) return;
    if (!steps.filter(s => s.flowId && s.deviceUdid).length) { setRunning({ status: "empty" }); setTimeout(() => setRunning(null), 3500); return; }
    setRunning({ status: "starting" });
    await runCampaignSteps({ id: campaign.id, name, schedule, steps }, ev => setRunning(ev));
    setTimeout(() => setRunning(null), 6000);
  }
  // auto-sauvegarde
  useEffect(() => { CampaignStore.update(campaign.id, { name, schedule, steps, mode, parallel }); onChange && onChange(); }, [name, schedule, steps, mode, parallel]);

  const otherTz = schedule.inputTz === CAMP_TARGET_TZ ? "Europe/Paris" : CAMP_TARGET_TZ;
  const otherLabel = (CAMP_TZS.find(t => t.id === otherTz) || {}).label || otherTz;

  const toggleDay = (d) => setSchedule(s => ({ ...s, days: (s.days || []).includes(d) ? s.days.filter(x => x !== d) : [...(s.days || []), d] }));
  const setTime = (i, v) => setSchedule(s => ({ ...s, times: s.times.map((t, j) => j === i ? v : t) }));
  const addTime = () => setSchedule(s => ({ ...s, times: [...(s.times || []), "12:00"] }));
  const rmTime = (i) => setSchedule(s => ({ ...s, times: s.times.filter((_, j) => j !== i) }));

  const addStep = () => setSteps(st => [...st, { id: "st_" + Date.now(), type: "flux", flowId: "", deviceUdid: "" }]);
  const updStep = (id, patch) => setSteps(st => st.map(s => s.id === id ? { ...s, ...patch } : s));
  const rmStep = (id) => setSteps(st => st.filter(s => s.id !== id));
  const moveStep = (i, dir) => setSteps(st => { const n = [...st]; const j = i + dir; if (j < 0 || j >= n.length) return st; const tmp = n[i]; n[i] = n[j]; n[j] = tmp; return n; });

  const openFlow = (flowId) => {
    if (!flowId) return;
    if (window.electronGoogle && window.electronGoogle.openAutomationPopup) window.electronGoogle.openAutomationPopup(flowId);
    else window.open("http://127.0.0.1:8787/?popup=automation&flow=" + encodeURIComponent(flowId), "_blank", "width=1280,height=860,menubar=no,toolbar=no");
  };

  const card = { background: "#161b22", border: "1px solid #30363d", borderRadius: 12, padding: 16 };
  return (
    <div style={{ padding: 20, maxWidth: 900, margin: "0 auto", display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <button onClick={onBack} style={{ padding: "6px 12px", background: "#21262d", border: "1px solid #30363d", borderRadius: 8, color: "#e6edf3", fontWeight: 700, fontSize: 12, cursor: "pointer" }}>← Retour</button>
        <input value={name} onChange={e => setName(e.target.value)} style={{ flex: 1, background: "none", border: "none", color: "#e6edf3", fontSize: 17, fontWeight: 800, outline: "none", padding: "4px 8px", borderRadius: 6 }}
          onFocus={e => e.currentTarget.style.background = "#21262d"} onBlur={e => e.currentTarget.style.background = "none"} />
        <button onClick={launchNow} disabled={isRunning}
          style={{ padding: "8px 16px", background: isRunning ? "#21262d" : "#238636", border: "none", borderRadius: 8, color: isRunning ? "#8b949e" : "#fff", fontWeight: 700, fontSize: 12.5, cursor: isRunning ? "default" : "pointer" }}>
          {isRunning ? "⏳ En cours…" : "▶️ Lancer maintenant"}
        </button>
        <span style={{ fontSize: 11, color: "#484f58" }}>auto-sauvegardé</span>
      </div>

      {running && running.status !== "empty" && (
        <div style={{ background: running.status === "finished" ? "rgba(52,211,153,.1)" : "rgba(137,87,229,.1)", border: "1px solid " + (running.status === "finished" ? "rgba(52,211,153,.35)" : "rgba(137,87,229,.35)"), borderRadius: 10, padding: "10px 14px", fontSize: 13, fontWeight: 600, color: "#e6edf3" }}>
          {running.status === "starting" && "⏳ Démarrage de la campagne…"}
          {running.status === "running" && `▶️ Étape ${running.index + 1}/${running.total} — ${(flows.find(f => f.id === running.step.flowId) || {}).name || "flux"} sur ${(devices.find(d => d.udid === running.step.deviceUdid) || {}).tag || "iPhone"}`}
          {running.status === "done" && `✅ Étape ${running.index + 1}/${running.total} terminée`}
          {running.status === "timeout" && `⚠️ Étape ${running.index + 1}/${running.total} — délai dépassé (le flux n'a pas signalé sa fin)`}
          {running.status === "finished" && `🎉 Campagne terminée — ${running.total} étape(s) exécutée(s)`}
        </div>
      )}
      {running && running.status === "empty" && (
        <div style={{ background: "rgba(248,81,73,.1)", border: "1px solid rgba(248,81,73,.3)", borderRadius: 10, padding: "10px 14px", fontSize: 13, color: "#f85149" }}>
          Ajoute au moins une étape avec un flux ET un iPhone avant de lancer.
        </div>
      )}

      {/* Mode de campagne : séquence (planifiée) OU parallèle (grande échelle) */}
      <div style={{ display: "flex", gap: 8 }}>
        {[["sequence", "🗓️ Séquence (planifiée)"], ["parallel", "⚡ Parallèle (vagues multi-comptes)"]].map(([m, lbl]) => (
          <button key={m} onClick={() => setMode(m)} style={{ padding: "8px 16px", borderRadius: 9, fontSize: 12.5, fontWeight: 800, cursor: "pointer",
            border: "1px solid " + (mode === m ? "#8957e5" : "#30363d"), background: mode === m ? "rgba(137,87,229,.15)" : "#161b22", color: mode === m ? "#bc8cff" : "#8b949e" }}>{lbl}</button>
        ))}
      </div>

      {mode === "parallel" && (
        <ParallelPanel parallel={parallel} setParallel={setParallel} flows={flows} cloudDevs={cloudDevs}
          toggleTarget={toggleTarget} pRunning={pRunning} prun={prun} onLaunch={launchParallel} />
      )}

      {mode === "sequence" && <>
      {/* ⏰ Planification */}
      <div style={card}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: "#e6edf3" }}>⏰ Planification</div>
          <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "#8b949e", cursor: "pointer" }}>
            <input type="checkbox" checked={schedule.enabled !== false} onChange={e => setSchedule(s => ({ ...s, enabled: e.target.checked }))} /> Activée
          </label>
        </div>
        <div style={{ marginBottom: 12 }}>
          <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5 }}>Je saisis les heures en :</div>
          <select value={schedule.inputTz} onChange={e => setSchedule(s => ({ ...s, inputTz: e.target.value }))}
            style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontSize: 12.5, padding: "6px 10px", cursor: "pointer" }}>
            {CAMP_TZS.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
          </select>
        </div>
        <div style={{ marginBottom: 12 }}>
          <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5 }}>Jours</div>
          <div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
            {CAMP_DAYS.map(([lbl, d]) => {
              const on = (schedule.days || []).includes(d);
              return <button key={d} onClick={() => toggleDay(d)} data-sfx="none"
                style={{ padding: "5px 12px", borderRadius: 99, fontSize: 12, fontWeight: 700, cursor: "pointer", border: "1px solid " + (on ? "#8957e5" : "#30363d"), background: on ? "rgba(137,87,229,.15)" : "#21262d", color: on ? "#bc8cff" : "#8b949e" }}>{lbl}</button>;
            })}
          </div>
        </div>
        <div>
          <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5 }}>Heures (la campagne se lance à chacune)</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
            {(schedule.times || []).map((t, i) => (
              <div key={i} style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <input type="time" value={t} onChange={e => setTime(i, e.target.value)}
                  style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontSize: 13, padding: "5px 8px" }} />
                <span style={{ fontSize: 12, color: "#8b949e" }}>→ <b style={{ color: "#1dbab4" }}>{convertWallTime(t, schedule.inputTz, otherTz) || "—"}</b> <span style={{ color: "#484f58" }}>({otherLabel})</span></span>
                <button onClick={() => rmTime(i)} data-sfx="none" style={{ marginLeft: "auto", background: "none", border: "none", color: "#f85149", cursor: "pointer", fontSize: 13 }}>✕</button>
              </div>
            ))}
          </div>
          <button onClick={addTime} style={{ marginTop: 8, padding: "5px 12px", background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#8b949e", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>+ Ajouter une heure</button>
        </div>
      </div>

      {/* ▶️ Étapes */}
      <div style={card}>
        <div style={{ fontSize: 14, fontWeight: 700, color: "#e6edf3", marginBottom: 12 }}>▶️ Étapes (flux enchaînés)</div>
        {steps.length === 0 && <div style={{ fontSize: 12.5, color: "#484f58", marginBottom: 10 }}>Aucune étape. Ajoute un flux à lancer.</div>}
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {steps.map((s, i) => (
            <div key={s.id} style={{ display: "flex", alignItems: "center", gap: 8, background: "#0d1117", border: "1px solid #21262d", borderRadius: 9, padding: "8px 10px" }}>
              <span style={{ width: 22, height: 22, borderRadius: 5, background: "rgba(137,87,229,.15)", color: "#bc8cff", fontSize: 11, fontWeight: 700, display: "grid", placeItems: "center", flex: "none" }}>{i + 1}</span>
              <select value={s.flowId || ""} onChange={e => updStep(s.id, { flowId: e.target.value })}
                style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 6, color: "#e6edf3", fontSize: 12, padding: "4px 7px", cursor: "pointer", flex: 1, minWidth: 0 }}>
                <option value="">— Flux —</option>
                {flows.map(f => <option key={f.id} value={f.id}>{f.name || "Sans nom"}</option>)}
              </select>
              <select value={s.deviceUdid || ""} onChange={e => updStep(s.id, { deviceUdid: e.target.value })}
                style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 6, color: "#e6edf3", fontSize: 12, padding: "4px 7px", cursor: "pointer", flex: 1, minWidth: 0 }}>
                <option value="">— iPhone —</option>
                {devices.map(d => <option key={d.udid} value={d.udid}>📱 {d.tag || d.name}</option>)}
              </select>
              <button onClick={() => openFlow(s.flowId)} disabled={!s.flowId} title="Voir le flux" data-sfx="none"
                style={{ background: "#21262d", border: "1px solid #30363d", borderRadius: 6, color: s.flowId ? "#58a6ff" : "#484f58", cursor: s.flowId ? "pointer" : "default", padding: "4px 8px", fontSize: 12, flex: "none" }}>👁</button>
              <div style={{ display: "flex", flexDirection: "column", flex: "none" }}>
                <button onClick={() => moveStep(i, -1)} data-sfx="none" style={{ background: "none", border: "none", color: "#8b949e", cursor: "pointer", fontSize: 9, lineHeight: 1, padding: 0 }}>▲</button>
                <button onClick={() => moveStep(i, 1)} data-sfx="none" style={{ background: "none", border: "none", color: "#8b949e", cursor: "pointer", fontSize: 9, lineHeight: 1, padding: 0 }}>▼</button>
              </div>
              <button onClick={() => rmStep(s.id)} data-sfx="none" style={{ background: "none", border: "none", color: "#f85149", cursor: "pointer", fontSize: 13, flex: "none" }}>✕</button>
            </div>
          ))}
        </div>
        <button onClick={addStep} style={{ marginTop: 10, padding: "7px 14px", background: "rgba(137,87,229,.12)", border: "1px solid rgba(137,87,229,.35)", borderRadius: 8, color: "#bc8cff", fontSize: 12.5, fontWeight: 700, cursor: "pointer" }}>+ Ajouter une étape flux</button>
      </div>

      <div style={{ fontSize: 11.5, color: "#484f58", textAlign: "center", padding: "4px 0" }}>
        ⚙️ Le moteur d'exécution (lancement auto aux horaires + visuel live des iPhones + garde-fou proxy) arrive en Phase 2.
      </div>
      </>}
    </div>
  );
}

// ─── Panneau « Parallèle » : lance un Flux sur un pool de comptes/téléphones, N à la fois (vagues) ───
function ParallelPanel({ parallel, setParallel, flows, cloudDevs, toggleTarget, pRunning, prun, onLaunch }) {
  const card = { background: "#161b22", border: "1px solid #30363d", borderRadius: 12, padding: 16 };
  const p = parallel || {};
  const sel = (p.targets || []);
  const allOn = (on) => setParallel(pp => ({ ...pp, targets: on ? cloudDevs.map(d => d.udid) : [] }));
  const conc = Math.max(1, parseInt(p.parallelism || 3) || 3);
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      <div style={card}>
        <div style={{ fontSize: 14, fontWeight: 700, color: "#e6edf3", marginBottom: 4 }}>⚡ Campagne parallèle</div>
        <div style={{ fontSize: 11.5, color: "#8b949e", marginBottom: 14 }}>Lance le <b>même Flux</b> sur plusieurs comptes/téléphones <b>en même temps</b>, par vagues (max = ton nombre de proxys). Chaque téléphone s'ouvre dans sa propre fenêtre.</div>

        <div style={{ display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 14 }}>
          <div style={{ minWidth: 200 }}>
            <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5, fontWeight: 700 }}>FLUX À LANCER</div>
            <select value={p.flowId || ""} onChange={e => setParallel(pp => ({ ...pp, flowId: e.target.value }))}
              style={{ width: "100%", background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontSize: 12.5, padding: "6px 9px", cursor: "pointer" }}>
              <option value="">— Flux —</option>
              {flows.map(f => <option key={f.id} value={f.id}>{f.name || "Sans nom"}</option>)}
            </select>
          </div>
          <div>
            <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5, fontWeight: 700 }}>PARALLÉLISME (nb proxys)</div>
            <input type="number" min="1" max="20" value={conc} onChange={e => setParallel(pp => ({ ...pp, parallelism: parseInt(e.target.value) || 1 }))}
              style={{ width: 90, background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontSize: 13, padding: "6px 9px" }} />
          </div>
          <div>
            <div style={{ fontSize: 11, color: "#8b949e", marginBottom: 5, fontWeight: 700 }}>COOLDOWN ENTRE VAGUES (s)</div>
            <input type="number" min="0" value={p.cooldownSec == null ? 60 : p.cooldownSec} onChange={e => setParallel(pp => ({ ...pp, cooldownSec: parseInt(e.target.value) || 0 }))}
              style={{ width: 90, background: "#21262d", border: "1px solid #30363d", borderRadius: 7, color: "#e6edf3", fontSize: 13, padding: "6px 9px" }} />
          </div>
        </div>

        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6 }}>
          <div style={{ fontSize: 11, color: "#8b949e", fontWeight: 700 }}>☁️ TÉLÉPHONES / COMPTES CIBLES ({sel.length})</div>
          <span style={{ display: "flex", gap: 6 }}>
            <button onClick={() => allOn(true)} style={{ fontSize: 10.5, padding: "3px 8px", borderRadius: 6, background: "#21262d", border: "1px solid #30363d", color: "#8b949e", cursor: "pointer" }}>Tout</button>
            <button onClick={() => allOn(false)} style={{ fontSize: 10.5, padding: "3px 8px", borderRadius: 6, background: "#21262d", border: "1px solid #30363d", color: "#8b949e", cursor: "pointer" }}>Aucun</button>
          </span>
        </div>
        {cloudDevs.length === 0
          ? <div style={{ fontSize: 12, color: "#484f58", padding: 8 }}>Aucun cloud phone (MoreLogin ouvert ?).</div>
          : <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(180px,1fr))", gap: 6 }}>
              {cloudDevs.map(d => {
                const on = sel.includes(d.udid);
                return <label key={d.udid} style={{ display: "flex", alignItems: "center", gap: 7, padding: "6px 9px", borderRadius: 8, cursor: "pointer", fontSize: 12.5,
                  background: on ? "rgba(137,87,229,.14)" : "#0d1117", border: "1px solid " + (on ? "rgba(137,87,229,.45)" : "#21262d") }}>
                  <input type="checkbox" checked={on} onChange={() => toggleTarget(d.udid)} />
                  <span>☁️</span><span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.tag}</span>
                </label>;
              })}
            </div>}

        <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 16 }}>
          <button onClick={onLaunch} disabled={pRunning || !p.flowId || !sel.length}
            style={{ padding: "10px 20px", borderRadius: 9, border: "none", fontSize: 13, fontWeight: 800, cursor: (pRunning || !p.flowId || !sel.length) ? "default" : "pointer",
              background: (pRunning || !p.flowId || !sel.length) ? "#21262d" : "#8957e5", color: (pRunning || !p.flowId || !sel.length) ? "#484f58" : "#fff" }}>
            {pRunning ? "⏳ En cours…" : "⚡ Lancer sur " + sel.length + " compte(s)"}
          </button>
          <span style={{ fontSize: 11.5, color: "#8b949e" }}>{sel.length > 0 ? Math.ceil(sel.length / conc) + " vague(s) de " + conc + " max" : ""}</span>
        </div>

        {prun && prun.status !== "empty" && (
          <div style={{ marginTop: 14, background: prun.status === "finished" ? "rgba(52,211,153,.1)" : "rgba(137,87,229,.1)", border: "1px solid " + (prun.status === "finished" ? "rgba(52,211,153,.35)" : "rgba(137,87,229,.35)"), borderRadius: 10, padding: "10px 14px", fontSize: 12.5, color: "#e6edf3" }}>
            {prun.status === "starting" && "⏳ Démarrage…"}
            {prun.status === "wave" && "🌊 Vague " + prun.wave + "/" + prun.waves + " — " + prun.size + " téléphone(s) en parallèle… (" + prun.done + "/" + prun.total + " terminés)"}
            {prun.status === "run" && "▶️ Lancement… (" + prun.done + "/" + prun.total + ")"}
            {(prun.status === "ok" || prun.status === "timeout") && (prun.done + "/" + prun.total + " traités" + (prun.status === "timeout" ? " · ⚠️ un timeout" : ""))}
            {prun.status === "cooldown" && "⏸️ Cooldown " + Math.round(prun.ms / 1000) + "s avant la prochaine vague…"}
            {prun.status === "finished" && "🎉 Campagne terminée — " + (prun.okCount || 0) + "/" + prun.total + " réussis"}
          </div>
        )}
        {prun && prun.status === "empty" && <div style={{ marginTop: 12, color: "#f85149", fontSize: 12.5 }}>Choisis un Flux ET au moins un téléphone.</div>}
      </div>
    </div>
  );
}

window.CampaignBuilder = CampaignBuilder;
window.CampaignStore = CampaignStore;
