/* global React */
// ============================================================
// CampaignBuilder — module "Campagnes" du Pilotage auto.
// Reproduit la forme du lanceur de pool (Device/Pool/Model/Comptes/Reels par
// compte/Story lien dès/Destination) + le tableau des comptes produits, branché
// sur la vraie DB locale (/api/instagram/accounts) et la vraie liste d'iPhones.
// Le bouton "Lancer" déclenche une vraie exécution : pour chaque compte à
// produire, on ouvre une fenêtre popup ?popup=automation&flow=<id> (même
// mécanisme que "Ouvrir dans une fenêtre séparée" dans l'onglet Flux), on lui
// signale le device cible via localStorage ('pl.campaign.run'), et on attend
// son signal de fin ('pl.campaign.done.<signalId>') — ce pont est déjà géré
// côté AutomationEditor (automation_builder.jsx, ~ligne 2205). Pause/Stop
// agissent entre deux comptes (comme le "Pilotage auto" par batch).
// ============================================================
const { useState, useEffect, useCallback, useRef } = React;

const CAMPAGNE_POOLS = ['jailbreak'];
const CAMPAGNE_STORY_OPTS = ['J0', 'J1', 'J2', 'J3'];
function _sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
// Date locale "YYYY-MM-DD" (même format que today_str() côté farm_backend.py) -> sert à
// n'afficher reels_today que s'il correspond bien à AUJOURD'HUI (remise à zéro visuelle,
// sans dépendre d'un job de minuit côté serveur).
function _localToday() {
  var d = new Date();
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}
// Testé en direct sur le vrai iPhone : les 3 "raccourcis" (Videos/Reel, Photo Story Higlight,
// Photo de profil IG) ne sont PAS des Shortcuts avec résultat affiché -> ce sont des raccourcis
// qui ouvrent directement l'album Photos correspondant. Le nombre d'éléments est affiché juste
// sous chaque nom d'album dans la liste "Albums" de l'app Photos -> bien plus simple et fiable
// que de lancer/fermer 3 raccourcis : on ouvre juste Photos > Albums et on lit l'écran par OCR.
const CAMPAGNE_ALBUM_TARGETS = [
  { key: 'reels', re: /videos\s*\/?\s*reel/i },
  { key: 'story', re: /photo\s*story/i },
  { key: 'pdp', re: /photo\s*s?\s*de\s*profil\s*ig/i },
];
function _parseAlbumCounts(text) {
  // Essayé un instant une approche "position fixe" ancrée sur Recents (comptage séquentiel des
  // nombres après ce mot) : encore PIRE en test réel — un texte parasite dans la vignette d'un
  // Reel Instagram ("1-2", pagination de story) se fait compter comme un vrai chiffre d'album et
  // décale tout. Retour à l'approche par nom de ligne : on cherche la ligne qui contient le nom
  // de l'album cible, et on regarde une petite fenêtre de lignes juste après pour trouver son
  // compte (les 2 chiffres d'une ligne à 2 colonnes sont parfois sur la même ligne suivante,
  // parfois éclatés sur 2 lignes) — validé correct sur toutes les captures OCR réelles testées.
  // Reste un residu de risque si l'OCR rate carrément le nom d'un album sur un run donné (résultat
  // "?" pour cet album ce jour-là) mais ça n'affiche jamais un chiffre confiant mais faux.
  const lines = String(text || '').split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
  const result = {};
  for (var i = 0; i < lines.length; i++) {
    var matches = [];
    for (var t = 0; t < CAMPAGNE_ALBUM_TARGETS.length; t++) {
      const target = CAMPAGNE_ALBUM_TARGETS[t];
      if (result[target.key] != null) continue;
      const m = lines[i].match(target.re);
      if (m) matches.push({ key: target.key, index: m.index });
    }
    if (!matches.length) continue;
    matches.sort(function (a, b) { return a.index - b.index; });
    var nums = [];
    for (var j = i + 1; j < Math.min(i + 3, lines.length) && nums.length < matches.length; j++) {
      const found = lines[j].match(/\d+/g);
      if (found) nums = nums.concat(found);
    }
    if (!nums.length) continue;
    matches.forEach(function (mt, pos) { if (nums[pos] != null) result[mt.key] = nums[pos]; });
  }
  return result;
}

function _campSignalId() { return 'c' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }

// Lance UN run réel du flux `flowId` sur le device `udid`, via le pont localStorage déjà
// géré côté AutomationEditor (pl.campaign.run → applique le device → doRun(null) →
// pl.campaign.done.<signalId> → ferme la popup). Résout à true/false (succès ou non).
// `winRef` (optionnel) reçoit la référence de la fenêtre popup pour permettre un Stop immédiat.
function runOneAccount(udid, flowId, winRef) {
  return new Promise(function (resolve) {
    const signalId = _campSignalId();
    const doneKey = 'pl.campaign.done.' + signalId;
    const runKey = 'pl.campaign.run.' + signalId;   // clé DÉDIÉE à ce popup (pas une clé globale partagée)
    try { localStorage.removeItem(doneKey); } catch (e) {}
    // Écrit le signal AVANT d'ouvrir la fenêtre : côté AutomationEditor, l'effet qui consomme
    // pl.campaign.run.<signalId> ne le lit qu'UNE SEULE FOIS au montage (pas d'écoute continue
    // derrière) — s'il est écrit après coup, la popup a déjà fini son check et ne le voit jamais
    // passer. Clé scopée par signalId (au lieu d'une clé globale 'pl.campaign.run') : sinon,
    // lancer plusieurs iPhones EN PARALLÈLE (2 popups qui montent presque en même temps) fait que
    // l'une écrase la clé de l'autre avant qu'elle n'ait pu la lire -> ce popup-là ne démarre jamais.
    try {
      localStorage.setItem(runKey, JSON.stringify({ flowId: flowId, device: udid, signalId: signalId, at: Date.now() }));
    } catch (e) {}
    let win = null;
    try {
      win = window.open('http://127.0.0.1:8787/?popup=automation&flow=' + encodeURIComponent(flowId) + '&signal=' + encodeURIComponent(signalId),
        'camp_' + udid + '_' + signalId, 'width=430,height=780');
    } catch (e) {}
    if (winRef) winRef.current = win;
    let settled = false;
    let poll, winPoll, safety;
    const cleanup = function () {
      clearInterval(poll); clearInterval(winPoll); clearTimeout(safety);
      window.removeEventListener('storage', onStorage);
      try { localStorage.removeItem(doneKey); } catch (e) {}
      if (winRef) winRef.current = null;
    };
    const finish = function (ok) {
      if (settled) return; settled = true;
      cleanup();
      resolve(ok);
    };
    const onStorage = function (e) {
      if (e.key === doneKey && e.newValue) {
        try { finish(!!JSON.parse(e.newValue).ok); } catch (e2) { finish(true); }
      }
    };
    window.addEventListener('storage', onStorage);
    // Storage event de secours : au cas où l'event 'storage' ne se déclenche pas de façon fiable.
    poll = setInterval(function () {
      try { const raw = localStorage.getItem(doneKey); if (raw) finish(!!JSON.parse(raw).ok); } catch (e) {}
    }, 1000);
    // Si la popup se ferme sans avoir signalé (fermée manuellement, ou Stop forcé) → échec, on débloque la boucle.
    winPoll = setInterval(function () { if (win && win.closed) finish(false); }, 700);
    // Garde-fou : si rien ne se passe (popup bloquée, flux sans trigger_manual…), on n'attend pas indéfiniment.
    safety = setTimeout(function () { finish(false); }, 20 * 60 * 1000);
  });
}

function CampaignBuilder({ platform }) {
  const [tab, setTab] = useState('comptes');
  const [devices, setDevices] = useState([]);
  const [accounts, setAccounts] = useState([]);
  const [runs, setRuns] = useState([]);
  const [runsAt, setRunsAt] = useState('');
  const [loading, setLoading] = useState(true);
  const [launchMsg, setLaunchMsg] = useState('');

  // Plusieurs iPhones sélectionnables à la fois (au lieu d'un seul) -> chacun lance et fait
  // progresser SON propre pool en parallèle, indépendamment des autres.
  const [deviceIds, setDeviceIds] = useState([]);
  const [pool, setPool] = useState(CAMPAGNE_POOLS[0]);
  const [flows, setFlows] = useState([]);
  const [flowId, setFlowId] = useState('');
  const [model, setModel] = useState('');
  const [niche, setNiche] = useState('');
  const [niches, setNiches] = useState([]);
  const [addingNiche, setAddingNiche] = useState(false);
  const [newNiche, setNewNiche] = useState('');
  const [nbComptes, setNbComptes] = useState(1);
  const [reelsParCompte, setReelsParCompte] = useState(3);
  const [storyLienDes, setStoryLienDes] = useState('J0');
  const [destination, setDestination] = useState('');
  const [publierVraiment, setPublierVraiment] = useState(false);
  // Un run actif par udid : { runId, paused, msg } — remplace les anciens état/paused/runId
  // globaux (un seul à la fois) pour permettre plusieurs pools en parallèle sur des iPhones différents.
  const [activeRuns, setActiveRuns] = useState({});
  const [availCounts, setAvailCounts] = useState({});
  const [checkingAvail, setCheckingAvail] = useState(false);
  const [availAt, setAvailAt] = useState('');
  const [availErr, setAvailErr] = useState('');

  const checkAvailability = useCallback(async function (udid) {
    // Ouvre Photos > Albums et lit les compteurs par OCR (voir _parseAlbumCounts). Testé en
    // direct : "Videos/Reel" ouvre l'album vide direct (pas de chiffre affiché en vue album),
    // mais la LISTE des albums affiche bien "nom" puis "N" juste en dessous -> plus fiable.
    // ⚠️ Bug constaté en test réel : Photos rouvre sur N'IMPORTE QUELLE position de scroll
    // mémorisée (parfois la vue résumée "My Albums" avec carrousel, parfois déjà scrollé loin
    // dans "People & Places"/"Media Types") -> les mauvais chiffres étaient lus (ex: Recents=84
    // pris pour Videos/Reel). Fix : taper la BARRE DE STATUT ramène toujours en haut de la vue
    // courante (comportement standard iOS), puis taper "See All" (si présent, sinon ignoré)
    // pour rentrer dans la vraie liste complète scrollable, point de départ fiable et connu.
    if (!udid) { setAvailErr('Aucun iPhone sélectionné'); return; }
    if (!window.Backend) { setAvailErr('window.Backend indisponible (backend.js pas chargé ?)'); return; }
    setCheckingAvail(true);
    setAvailErr('');
    try {
      await window.Backend.action(udid, 'launchApp', { bundleId: 'com.apple.mobileslideshow' });
      await _sleep(1500);
      // Onglet "Albums" (3e des 4 icônes en bas) — au cas où Photos s'ouvre sur "Library".
      await window.Backend.action(udid, 'gesture', { points: [{ x: .625, y: .965, t: 0 }, { x: .625, y: .965, t: 60 }] });
      await _sleep(1000);
      await window.Backend.action(udid, 'tap', { x: .5, y: .02 });
      await _sleep(600);
      await window.Backend.action(udid, 'tapByOcr', { content: 'See All' }).catch(function () {});
      await _sleep(800);
      const r = await window.Backend.action(udid, 'readScreenText', {});
      let counts = _parseAlbumCounts((r && r.text) || '');
      // Si un des 3 comptes manque, tente un scroll (l'album peut être plus bas dans la liste).
      // ⚠️ La 2e lecture (après scroll) peut re-matcher un album déjà trouvé en 1ère lecture
      // (encore partiellement visible en haut) et lui donner une VALEUR FAUSSE (ex: reels
      // récupère la valeur de pdp) -> on ne complète que les clés qui MANQUENT encore, on ne
      // laisse jamais la 2e lecture écraser une valeur déjà trouvée par la 1ère.
      if (Object.keys(counts).length < 3) {
        await window.Backend.action(udid, 'swipe', { fromX: .5, fromY: .8, toX: .5, toY: .4, duration: .2 });
        await _sleep(700);
        const r2 = await window.Backend.action(udid, 'readScreenText', {});
        counts = Object.assign({}, _parseAlbumCounts((r2 && r2.text) || ''), counts);
      }
      setAvailCounts(counts);
      await window.Backend.action(udid, 'home', {});
    } catch (e) { setAvailCounts({}); setAvailErr(String((e && e.message) || e)); }
    setAvailAt(new Date().toLocaleString('fr-FR'));
    setCheckingAvail(false);
  }, []);

  const loadAccounts = useCallback(function () {
    fetch('/api/instagram/accounts').then(function (r) { return r.json(); })
      .then(function (d) { setAccounts(d.accounts || []); }).catch(function () {});
  }, []);

  const loadRuns = useCallback(function () {
    fetch('/api/campaign/runs').then(function (r) { return r.json(); })
      .then(function (d) {
        setRuns(d.runs || []);
        setRunsAt(new Date().toLocaleString('fr-FR'));
      }).catch(function () {});
  }, []);

  const deleteRun = useCallback(async function (runId) {
    // ne supprime jamais un run actuellement en cours d'exécution (sur n'importe quel iPhone)
    if (Object.keys(activeRunsRef.current).some(function (u) { return activeRunsRef.current[u].runId === runId; })) return;
    try {
      await fetch('/api/campaign/runs/' + encodeURIComponent(runId), { method: 'DELETE' });
      loadRuns();
    } catch (e) {}
  }, [loadRuns]);

  const loadNiches = useCallback(function () {
    fetch('/api/campaign/niches').then(function (r) { return r.json(); })
      .then(function (d) { setNiches(d.niches || []); }).catch(function () {});
  }, []);

  const loadDevices = useCallback(function (keepSelection) {
    setLoading(true);
    (window.Backend ? window.Backend.devices() : Promise.resolve([])).catch(function () { return []; })
      .then(function (list) {
        setDevices(list || []);
        if (!keepSelection && list && list[0]) setDeviceIds([list[0].udid]);
        setLoading(false);
      });
  }, []);
  const toggleDevice = useCallback(function (udid) {
    setDeviceIds(function (prev) { return prev.includes(udid) ? prev.filter(function (x) { return x !== udid; }) : prev.concat([udid]); });
  }, []);

  useEffect(function () {
    loadDevices(false);
    loadAccounts();
    loadRuns();
    loadNiches();
    // Liste des automatisations enregistrées (onglet "Flux") -> stockées en localStorage,
    // accessible depuis ce fichier car autoLoadFlows() est une fonction globale (automation_builder.jsx).
    const fl = (typeof autoLoadFlows === 'function') ? autoLoadFlows() : [];
    setFlows(fl || []);
    if (fl && fl[0] && !flowId) setFlowId(fl[0].id);
    // Rafraîchit les runs pendant qu'un pool tourne (progression mise à jour côté serveur).
    const iv = setInterval(loadRuns, 5000);
    return function () { clearInterval(iv); };
  }, [loadDevices, loadAccounts, loadRuns, loadNiches]);

  // Pas de sélection automatique : "Aucun" reste la valeur par défaut tant que l'utilisateur
  // ne choisit pas explicitement une niche.

  const submitNewNiche = useCallback(async function () {
    const name = newNiche.trim();
    if (!name) { setAddingNiche(false); return; }
    try {
      const r = await fetch('/api/campaign/niches', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: name })
      });
      const j = await r.json();
      setNiches(j.niches || []);
      setNiche(name);
    } catch (e) {}
    setNewNiche('');
    setAddingNiche(false);
  }, [newNiche]);

  const models = Array.from(new Set(accounts.map(function (a) { return a.model; }).filter(Boolean)));
  const etats = Array.from(new Set(accounts.map(function (a) { return a.tag_status || a.status; }).filter(Boolean)));

  const [filterModel, setFilterModel] = useState('toutes');
  const [filterEtat, setFilterEtat] = useState('tous');
  const filtered = accounts.filter(function (a) {
    if (filterModel !== 'toutes' && a.model !== filterModel) return false;
    if (filterEtat !== 'tous' && (a.tag_status || a.status) !== filterEtat) return false;
    return true;
  });

  // Contrôle par iPhone (udid -> { stop, paused, winRef }) : chaque pool lancé en parallèle a
  // ses propres drapeaux stop/pause et sa propre popup, indépendants des autres iPhones actifs.
  const runCtlRef = useRef({});
  const activeRunsRef = useRef({});

  const patchRun = useCallback(async function (runId, patch) {
    if (!runId) return;
    try {
      await fetch('/api/campaign/runs/' + encodeURIComponent(runId), {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch)
      });
      loadRuns();
    } catch (e) {}
  }, [loadRuns]);

  const setDeviceRun = function (udid, patch) {
    setActiveRuns(function (prev) {
      const next = Object.assign({}, prev);
      if (patch === null) delete next[udid]; else next[udid] = Object.assign({}, next[udid], patch);
      activeRunsRef.current = next;
      return next;
    });
  };

  // Lance le pool sur UN iPhone donné. Appelée une fois par iPhone sélectionné, sans être
  // attendue les unes après les autres -> elles tournent en parallèle, chacune avec sa propre
  // boucle de comptes et son propre run côté serveur.
  const launchOneDevice = useCallback(async function (udid) {
    if (activeRunsRef.current[udid]) return; // déjà en cours sur ce tel
    if (!flowId) { setLaunchMsg('Sélectionne une automatisation.'); setTimeout(function () { setLaunchMsg(''); }, 4000); return; }
    const dev = devices.find(function (d) { return d.udid === udid; });
    const fl = flows.find(function (f) { return f.id === flowId; });
    const total = Math.max(1, parseInt(nbComptes) || 1);
    const devTag = dev ? (dev.tag || dev.label || udid) : udid;
    let runId = null;
    try {
      const r = await fetch('/api/campaign/runs', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          device_udid: udid, device_tag: devTag,
          pool: pool, model: model, niche: niche, total: total,
          flow_id: flowId, flow_name: fl ? (fl.name || '') : ''
        })
      });
      const j = await r.json();
      if (!j.run) { setLaunchMsg('Erreur (' + devTag + ') : ' + (j.error || 'inconnue')); setTimeout(function () { setLaunchMsg(''); }, 8000); return; }
      runId = j.run.id;
    } catch (e) { setLaunchMsg('Erreur (' + devTag + ') : ' + e.message); setTimeout(function () { setLaunchMsg(''); }, 8000); return; }

    const ctl = { stop: false, paused: false, winRef: { current: null } };
    runCtlRef.current[udid] = ctl;
    setDeviceRun(udid, { runId: runId, paused: false, msg: '', devTag: devTag, total: total });
    loadRuns();

    let done = 0, attempts = 0;
    for (let i = 0; i < total; i++) {
      if (ctl.stop) break;
      while (ctl.paused && !ctl.stop) { await _sleep(500); }
      if (ctl.stop) break;
      setDeviceRun(udid, { msg: 'compte ' + (i + 1) + '/' + total + ' en cours (' + (fl ? fl.name : flowId) + ')…' });
      attempts++;
      const ok = await runOneAccount(udid, flowId, ctl.winRef);
      if (ok) done++;
      await patchRun(runId, { done: done, attempts: attempts });
    }
    const finalStatus = ctl.stop ? 'stopped' : 'termine';
    await patchRun(runId, { status: finalStatus });

    delete runCtlRef.current[udid];
    setDeviceRun(udid, null);
    setLaunchMsg((ctl.stop ? '⛔ ' : '✓ ') + devTag + ' — Run ' + runId + (ctl.stop ? ' arrêté ' : ' terminé ') + '(' + done + '/' + total + ' compte(s)).');
    setTimeout(function () { setLaunchMsg(''); }, 8000);
  }, [flowId, devices, flows, nbComptes, pool, model, niche, loadRuns, patchRun]);

  const doLaunchAll = useCallback(function () {
    if (!deviceIds.length) { setLaunchMsg('Sélectionne au moins un iPhone.'); setTimeout(function () { setLaunchMsg(''); }, 4000); return; }
    deviceIds.forEach(function (udid) { launchOneDevice(udid); });
  }, [deviceIds, launchOneDevice]);

  const doPauseDevice = useCallback(function (udid) {
    const ctl = runCtlRef.current[udid];
    if (!ctl) return;
    ctl.paused = !ctl.paused;
    setDeviceRun(udid, { paused: ctl.paused });
    const rid = activeRunsRef.current[udid] && activeRunsRef.current[udid].runId;
    patchRun(rid, { status: ctl.paused ? 'pause' : 'en_cours' });
  }, [patchRun]);

  const doStopDevice = useCallback(function (udid) {
    const ctl = runCtlRef.current[udid];
    if (!ctl) return;
    ctl.stop = true; ctl.paused = false;
    setDeviceRun(udid, { paused: false });
    if (ctl.winRef.current && !ctl.winRef.current.closed) {
      try { ctl.winRef.current.close(); } catch (e) {}
    }
    const rid = activeRunsRef.current[udid] && activeRunsRef.current[udid].runId;
    patchRun(rid, { status: 'stopped' });
  }, [patchRun]);

  const labelStyle = { fontSize: 10, color: '#8b949e', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.05em', marginBottom: 4 };
  const inputStyle = { width: '100%', boxSizing: 'border-box', background: '#161b22', border: '1px solid #30363d', borderRadius: 6, color: '#e5e5e5', fontSize: 12.5, padding: '7px 10px', fontFamily: "'JetBrains Mono', monospace" };
  const field = function (label, node, minWidth, grow) {
    return React.createElement('div', { style: { minWidth: minWidth || 140, flex: grow === undefined ? 1 : grow } },
      React.createElement('div', { style: labelStyle }, label),
      node
    );
  };
  const select = function (value, onChange, opts) {
    return React.createElement('select', { value: value, onChange: function (e) { onChange(e.target.value); }, style: inputStyle },
      opts.map(function (o) { return React.createElement('option', { key: o.v || o, value: o.v || o }, o.l || o); })
    );
  };

  return React.createElement('div', { style: { padding: '16px 24px', maxWidth: 1200, margin: '0 auto' } },
    React.createElement('div', { style: { display: 'flex', gap: 8, marginBottom: 16 } },
      React.createElement('button', { onClick: function () { setTab('comptes'); }, style: { padding: '6px 14px', borderRadius: 8, fontSize: 12, fontWeight: 700, cursor: 'pointer', border: '1px solid ' + (tab === 'comptes' ? '#8957e5' : '#30363d'), background: tab === 'comptes' ? 'rgba(137,87,229,.15)' : '#161b22', color: tab === 'comptes' ? '#bc8cff' : '#8b949e' } }, 'Comptes & runs'),
      React.createElement('button', { onClick: function () { setTab('frise'); }, style: { padding: '6px 14px', borderRadius: 8, fontSize: 12, fontWeight: 700, cursor: 'pointer', border: '1px solid ' + (tab === 'frise' ? '#8957e5' : '#30363d'), background: tab === 'frise' ? 'rgba(137,87,229,.15)' : '#161b22', color: tab === 'frise' ? '#bc8cff' : '#8b949e' } }, 'Frise par âge')
    ),
    tab === 'frise'
      ? React.createElement('div', { style: { textAlign: 'center', padding: 40, color: '#8b949e', fontSize: 13 } }, 'Frise par âge — pas encore implémentée.')
      : React.createElement(React.Fragment, null,
        React.createElement('div', { style: { background: '#0d1117', border: '1px solid #21262d', borderRadius: 12, padding: 18, marginBottom: 24 } },
          React.createElement('div', { style: { fontSize: 13, fontWeight: 700, color: '#e5e5e5', marginBottom: 12 } }, 'Lancer un pool'),
          React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 12 } },
            field('Device (plusieurs possibles)', React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
              devices.length === 0
                ? React.createElement('div', { style: Object.assign({}, inputStyle, { color: '#8b949e', width: 'auto' }) }, 'Aucun iPhone détecté')
                : devices.map(function (d) {
                  const on = deviceIds.includes(d.udid);
                  return React.createElement('button', {
                    key: d.udid, onClick: function () { toggleDevice(d.udid); },
                    title: on ? 'Retirer de la sélection' : 'Ajouter à la sélection',
                    style: {
                      padding: '6px 12px', borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: 'pointer',
                      border: '1px solid ' + (on ? '#3fb950' : '#30363d'),
                      background: on ? 'rgba(63,185,80,.15)' : '#161b22', color: on ? '#3fb950' : '#8b949e'
                    }
                  }, (on ? '✓ ' : '') + (d.tag || d.label || d.udid));
                }),
              React.createElement('button', {
                onClick: function () { loadDevices(true); }, title: 'Réactualiser la liste des iPhones',
                style: { padding: '0 12px', height: 30, borderRadius: 6, border: '1px solid #30363d', background: '#161b22', color: '#8b949e', cursor: 'pointer', fontSize: 14 }
              }, '↻')
            ), 260),
            field('Pool', select(pool, setPool, CAMPAGNE_POOLS)),
            field('Model', React.createElement('input', { value: model, placeholder: 'ex: maya', onChange: function (e) { setModel(e.target.value); }, style: inputStyle, list: 'campagne-models' })),
            field('Niche', React.createElement('div', { style: { display: 'flex', gap: 6 } },
              select(niche, setNiche, [{ v: '', l: 'Aucun' }].concat(niches)),
              React.createElement('button', {
                onClick: function () { setAddingNiche(!addingNiche); }, title: 'Ajouter une niche',
                style: { padding: '0 12px', borderRadius: 6, border: '1px solid #30363d', background: '#161b22', color: '#8b949e', cursor: 'pointer', fontSize: 14, fontWeight: 700 }
              }, '+')
            ))
          ),
          React.createElement('datalist', { id: 'campagne-models' }, models.map(function (m) { return React.createElement('option', { key: m, value: m }); })),
          addingNiche && React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 12, maxWidth: 280 } },
            React.createElement('input', {
              value: newNiche, placeholder: 'Nouvelle niche…', autoFocus: true,
              onChange: function (e) { setNewNiche(e.target.value); },
              onKeyDown: function (e) { if (e.key === 'Enter') submitNewNiche(); if (e.key === 'Escape') { setAddingNiche(false); setNewNiche(''); } },
              style: Object.assign({}, inputStyle, { flex: 1 })
            }),
            React.createElement('button', { onClick: submitNewNiche, style: { padding: '0 14px', borderRadius: 6, border: 'none', background: '#238636', color: '#fff', cursor: 'pointer', fontWeight: 700, fontSize: 12 } }, 'Ajouter')
          ),
          React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-end', marginBottom: 12 } },
            field('Automatisation', flows.length === 0
              ? React.createElement('div', { style: Object.assign({}, inputStyle, { color: '#8b949e' }) }, 'Aucune automatisation enregistrée')
              : select(flowId, setFlowId, flows.map(function (f) { return { v: f.id, l: (f.name || 'Sans nom') + ' (' + f.id + ')' }; })), 460),
            field('Story lien dès', select(storyLienDes, setStoryLienDes, CAMPAGNE_STORY_OPTS)),
            field('Reels / compte', React.createElement('input', { type: 'number', min: 0, value: reelsParCompte, onChange: function (e) { setReelsParCompte(parseInt(e.target.value) || 0); }, style: inputStyle })),
            React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#8b949e', paddingBottom: 7 } },
              React.createElement('input', { type: 'checkbox', checked: publierVraiment, onChange: function (e) { setPublierVraiment(e.target.checked); } }),
              'publier vraiment'
            )
          ),
          React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap', marginBottom: 12 } },
            field('Comptes', React.createElement('input', { type: 'number', min: 1, value: nbComptes, onChange: function (e) { setNbComptes(parseInt(e.target.value) || 1); }, style: inputStyle }), 80, 0),
            field('Destination (si J0)', React.createElement('input', { value: destination, placeholder: 'https://...', onChange: function (e) { setDestination(e.target.value); }, style: inputStyle }), 400, 1),
            React.createElement('button', {
              onClick: doLaunchAll,
              title: deviceIds.length > 1 ? 'Lance le pool en parallèle sur les ' + deviceIds.length + ' iPhones sélectionnés' : 'Lance le pool',
              style: { padding: '8px 20px', borderRadius: 8, fontSize: 12.5, fontWeight: 700, cursor: 'pointer', border: 'none', background: '#238636', color: '#fff' }
            }, 'Lancer' + (deviceIds.length > 1 ? ' (' + deviceIds.length + ' iPhones)' : ''))
          ),
          // Une ligne de contrôle par iPhone actuellement en cours (pause/stop indépendants).
          Object.keys(activeRuns).length > 0 && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 } },
            Object.keys(activeRuns).map(function (udid) {
              const ar = activeRuns[udid];
              return React.createElement('div', { key: udid, style: { display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', borderRadius: 8, background: '#161b22', border: '1px solid #30363d', fontSize: 11.5, fontFamily: "'JetBrains Mono', monospace" } },
                React.createElement('span', { style: { fontWeight: 700, color: '#e5e5e5', minWidth: 90 } }, ar.devTag || udid),
                React.createElement('span', { style: { color: '#8b949e', flex: 1 } }, ar.msg || '…'),
                React.createElement('button', {
                  onClick: function () { doPauseDevice(udid); },
                  style: { padding: '4px 12px', borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: 'pointer', border: 'none', background: ar.paused ? '#388bfd' : '#d29922', color: '#fff' }
                }, ar.paused ? '▶ Reprendre' : '⏸ Pause'),
                React.createElement('button', {
                  onClick: function () { doStopDevice(udid); },
                  style: { padding: '4px 12px', borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: 'pointer', border: '1px solid #30363d', background: '#21262d', color: '#f85149' }
                }, '■ Stop')
              );
            })
          ),
          React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 11.5, color: '#8b949e', fontFamily: "'JetBrains Mono', monospace" } },
            checkingAvail
              ? React.createElement('span', null, '⏳ Vérification de la disponibilité sur le téléphone…')
              : React.createElement('span', null,
                (availCounts.reels != null ? availCounts.reels : '?') + ' Vidéos/Réels disponibles · ' +
                (availCounts.story != null ? availCounts.story : '?') + ' Photo Story Highlights disponibles · ' +
                (availCounts.pdp != null ? availCounts.pdp : '?') + ' Photos de profil IG disponibles'
              ),
            deviceIds[0] && !checkingAvail && React.createElement('button', {
              onClick: function () { checkAvailability(deviceIds[0]); }, title: 'Ouvre Photos sur le 1er iPhone sélectionné (' + deviceIds[0] + ') et compte les éléments disponibles',
              style: { background: 'rgba(88,166,255,.12)', border: '1px solid #388bfd', borderRadius: 6, color: '#58a6ff', cursor: 'pointer', fontSize: 11, fontWeight: 700, padding: '2px 10px' }
            }, '🔄 Vérifier disponibilité'),
            availAt && !checkingAvail && React.createElement('span', { style: { color: '#3fb950' } }, '· actualisé ' + availAt)
          ),
          availErr && React.createElement('div', { style: { fontSize: 11, color: '#f85149', fontFamily: "'JetBrains Mono', monospace", marginTop: 4 } }, '⚠️ ' + availErr),
          launchMsg && React.createElement('div', { style: { fontSize: 11.5, color: '#d29922', fontFamily: "'JetBrains Mono', monospace", marginTop: 6 } }, launchMsg)
        ),
        React.createElement('div', { style: { background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', marginBottom: 14, display: 'flex', alignItems: 'center', gap: 24 } },
          React.createElement('div', { style: { fontSize: 12, fontWeight: 700, color: '#e5e5e5', textTransform: 'uppercase', letterSpacing: '.05em' } }, 'Progression'),
          (function () {
            const totalAttempts = runs.reduce(function (s, r) { return s + (r.attempts || 0); }, 0);
            const totalDone = runs.reduce(function (s, r) { return s + (r.done || 0); }, 0);
            const totalEchec = Math.max(0, totalAttempts - totalDone);
            const stat = function (label, value, color) {
              return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: 50 } },
                React.createElement('div', { style: { fontSize: 9, color: '#6e7681', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.04em' } }, label),
                React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: color || '#e5e5e5' } }, value)
              );
            };
            return React.createElement('div', { style: { display: 'flex', gap: 22 } },
              stat('Tenté', totalAttempts, '#8b949e'),
              stat('Fini', totalDone, '#3fb950'),
              stat('Échec', totalEchec, totalEchec > 0 ? '#f85149' : '#8b949e')
            );
          })()
        ),
        React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } },
          React.createElement('div', { style: { fontSize: 13, fontWeight: 700, color: '#e5e5e5' } }, 'Runs'),
          runsAt && React.createElement('div', { style: { fontSize: 11, color: '#3fb950' } }, '· actualisé ' + runsAt)
        ),
        React.createElement('div', { style: { overflowX: 'auto', border: '1px solid #21262d', borderRadius: 10, marginBottom: 24 } },
          React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12.5, fontFamily: "'JetBrains Mono', monospace" } },
            React.createElement('thead', null,
              React.createElement('tr', { style: { textAlign: 'left', color: '#8b949e', borderBottom: '1px solid #21262d' } },
                ['Run', 'Automatisation', 'Date', 'Heure', 'Device', 'Model', 'Progression', 'État', ''].map(function (h) {
                  return React.createElement('th', { key: h, style: { padding: '8px 12px', fontWeight: 700 } }, h);
                })
              )
            ),
            React.createElement('tbody', null,
              runs.length === 0
                ? React.createElement('tr', null, React.createElement('td', { colSpan: 9, style: { padding: 20, textAlign: 'center', color: '#8b949e' } }, 'Aucun run.'))
                : runs.map(function (r) {
                  // Run id = "YYYYMMDD_HHMMSS" (généré par farm_backend.py) -> date/heure lisibles
                  const _m = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/.exec(r.id || '');
                  const dateStr = _m ? (_m[3] + '/' + _m[2] + '/' + _m[1]) : '—';
                  const heureStr = _m ? (_m[4] + ':' + _m[5] + ':' + _m[6]) : '—';
                  const badge = function (bg, fg, txt) { return React.createElement('span', { style: { padding: '2px 8px', borderRadius: 6, fontSize: 11, background: bg, color: fg, border: '1px solid ' + fg + '4d' } }, txt); };
                  const stat = function (label, value, color) {
                    return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: 42 } },
                      React.createElement('div', { style: { fontSize: 9, color: '#6e7681', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.04em' } }, label),
                      React.createElement('div', { style: { fontSize: 13, fontWeight: 700, color: color || '#e5e5e5' } }, value)
                    );
                  };
                  const echecCount = Math.max(0, (r.attempts || 0) - (r.done || 0));
                  const etatNode = r.verif_count > 0
                    ? badge('rgba(210,153,34,.15)', '#d29922', r.verif_count + ' vérif. humaine' + (r.verif_count > 1 ? 's' : ''))
                    : r.status === 'stopped' ? badge('rgba(248,81,73,.15)', '#f85149', 'stoppé')
                    : r.status === 'pause' ? badge('rgba(56,139,253,.15)', '#58a6ff', 'en pause')
                    : r.status === 'termine' ? badge('rgba(63,185,80,.15)', '#3fb950', 'terminé')
                    : badge('rgba(139,148,158,.15)', '#8b949e', 'en cours');
                  const isRunning = Object.keys(activeRuns).some(function (u) { return activeRuns[u].runId === r.id; });
                  return React.createElement('tr', { key: r.id, style: { borderBottom: '1px solid #161b22' } },
                    React.createElement('td', { style: { padding: '8px 12px', color: '#e5e5e5' } }, r.id),
                    React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, r.flow_name || '—'),
                    React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, dateStr),
                    React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, heureStr),
                    React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, r.device_tag || r.device_udid || '-'),
                    React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, r.model || '—'),
                    React.createElement('td', { style: { padding: '8px 12px' } },
                      React.createElement('div', { style: { display: 'flex', gap: 14 } },
                        stat('Tenté', r.attempts, '#8b949e'),
                        stat('Fini', r.done, '#3fb950'),
                        stat('Échec', echecCount, echecCount > 0 ? '#f85149' : '#8b949e')
                      )
                    ),
                    React.createElement('td', { style: { padding: '8px 12px' } }, etatNode),
                    React.createElement('td', { style: { padding: '8px 12px' } },
                      React.createElement('button', {
                        onClick: function () { deleteRun(r.id); },
                        disabled: isRunning,
                        title: isRunning ? 'Impossible de supprimer un run en cours' : 'Supprimer ce run',
                        style: {
                          background: 'none', border: '1px solid #30363d', borderRadius: 6, padding: '3px 8px',
                          color: isRunning ? '#30363d' : '#f85149', cursor: isRunning ? 'default' : 'pointer', fontSize: 11
                        }
                      }, '🗑')
                    )
                  );
                })
            )
          )
        ),
        React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
          React.createElement('div', { style: { fontSize: 13, fontWeight: 700, color: '#e5e5e5' } }, 'Comptes produits · ' + filtered.length + ' affiché(s) sur ' + accounts.length),
          React.createElement('div', { style: { display: 'flex', gap: 10 } },
            field('Filtrer par modèle', select(filterModel, setFilterModel, [{ v: 'toutes', l: 'toutes' }].concat(models))),
            field('État', select(filterEtat, setFilterEtat, [{ v: 'tous', l: 'tous' }].concat(etats)))
          )
        ),
        React.createElement('div', { style: { overflowX: 'auto', border: '1px solid #21262d', borderRadius: 10 } },
          React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12.5, fontFamily: "'JetBrains Mono', monospace" } },
            React.createElement('thead', null,
              React.createElement('tr', { style: { textAlign: 'left', color: '#8b949e', borderBottom: '1px solid #21262d' } },
                ['Container', 'Compte', 'Model', 'Reels postés', '2FA', 'État', 'Créé le'].map(function (h) {
                  return React.createElement('th', { key: h, style: { padding: '8px 12px', fontWeight: 700 } }, h);
                })
              )
            ),
            React.createElement('tbody', null,
              loading
                ? React.createElement('tr', null, React.createElement('td', { colSpan: 7, style: { padding: 20, textAlign: 'center', color: '#8b949e' } }, 'Chargement…'))
                : filtered.length === 0
                  ? React.createElement('tr', null, React.createElement('td', { colSpan: 7, style: { padding: 20, textAlign: 'center', color: '#8b949e' } }, 'Aucun compte.'))
                  : filtered.map(function (a) {
                    return React.createElement('tr', { key: a.id, style: { borderBottom: '1px solid #161b22' } },
                      React.createElement('td', { style: { padding: '8px 12px', color: '#bc8cff' } }, a.container_name || '-'),
                      React.createElement('td', { style: { padding: '8px 12px', color: '#e5e5e5' } }, '@' + a.username),
                      React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, a.model || '-'),
                      React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, (a.last_post_date === _localToday()) ? (a.reels_today || 0) : 0),
                      React.createElement('td', { style: { padding: '8px 12px' } }, a.twofa_enabled ? React.createElement('span', { style: { color: '#3fb950' } }, 'oui') : React.createElement('span', { style: { color: '#8b949e' } }, 'non')),
                      React.createElement('td', { style: { padding: '8px 12px', color: '#d29922' } }, a.tag_status || a.status || '-'),
                      React.createElement('td', { style: { padding: '8px 12px', color: '#8b949e' } }, (a.created_at || '').slice(0, 16))
                    );
                  })
            )
          )
        )
      )
  );
}

window.CampaignBuilder = CampaignBuilder;
