(() => {
  const { useState, apiFetch } = window.DC;
  const HTTP_ACTION_COLORS = {
    pending: "#64748b",
    running: "#00e5ff",
    ok: "#22d3a5",
    partial: "#fbbf24",
    skipped: "#94a3b8",
    timeout: "#f43f5e",
    error: "#f43f5e",
    http_error: "#f43f5e",
    stopped: "#94a3b8",
  };
  const HTTP_ACTION_ICONS = {
    pending: "⏸",
    running: "⟳",
    ok: "✅",
    partial: "⚠",
    skipped: "⏭",
    timeout: "⏱",
    error: "❌",
    http_error: "❌",
    stopped: "⏹",
  };

  function Toggle({ value, onChange, color = "#00e5ff", disabled }) {
    return (
      <div
        className="toggle"
        onClick={() => !disabled && onChange(!value)}
        style={{
          background: value ? `${color}44` : "#1a2030",
          border: `1px solid ${value ? color : "#1e2a3a"}`,
          opacity: disabled ? 0.4 : 1,
          cursor: disabled ? "not-allowed" : "pointer",
        }}
      >
        <div
          className="toggle-dot"
          style={{ left: value ? 18 : 2, background: value ? color : "#64748b" }}
        />
      </div>
    );
  }

  function highlightTextMatch(text, term) {
    const value = String(text == null ? "" : text);
    const normalizedTerm = String(term || "").trim();
    if (!normalizedTerm) return value;
    const lowerValue = value.toLowerCase();
    const lowerTerm = normalizedTerm.toLowerCase();
    const parts = [];
    let lastIndex = 0;
    let matchIndex = lowerValue.indexOf(lowerTerm);
    let key = 0;
    while (matchIndex !== -1) {
      if (matchIndex > lastIndex) parts.push(value.slice(lastIndex, matchIndex));
      parts.push(
        <mark key={`hl-${key++}`} className="search-highlight">
          {value.slice(matchIndex, matchIndex + normalizedTerm.length)}
        </mark>
      );
      lastIndex = matchIndex + normalizedTerm.length;
      matchIndex = lowerValue.indexOf(lowerTerm, lastIndex);
    }
    if (lastIndex < value.length) parts.push(value.slice(lastIndex));
    return parts.length ? parts : value;
  }

  function DomainCell({ serverId, user, onUpdate, isViewer, highlightTerm = "" }) {
    const [editing, setEditing] = useState(false);
    const [val, setVal] = useState(user.domain || "");
    const [saving, setSaving] = useState(false);

    const save = async () => {
      if (saving) return;
      setSaving(true);
      const res = await apiFetch(`/servers/${serverId}/users/${user.username}`, {
        method: "PATCH",
        body: { domain: val },
      });
      setSaving(false);
      if (res && res.error) return; // mantener la edición abierta ante un error
      setEditing(false);
      // onUpdate puede recibir el nuevo dominio para una actualización dirigida.
      onUpdate(val);
    };

    if (isViewer) {
      return <span style={{ fontSize: 11, color: "#64748b" }}>{user.domain ? highlightTextMatch(user.domain, highlightTerm) : "—"}</span>;
    }

    if (editing) {
      return (
        <div
          style={{ display: "flex", gap: 4, alignItems: "center", minWidth: 160 }}
          onClick={(event) => event.stopPropagation()}
        >
          <input
            value={val}
            onChange={(event) => setVal(event.target.value)}
            onKeyDown={(event) => event.key === "Enter" && save()}
            autoFocus
            className="input input-sm"
            style={{ flex: 1, minWidth: 0 }}
            placeholder="dominio.edu.pe"
          />
          <button
            onClick={save}
            disabled={saving}
            className="btn btn-ghost btn-xs"
            style={{ color: "#22d3a5", borderColor: "#22d3a544", opacity: saving ? 0.6 : 1 }}
          >
            {saving ? "…" : "✓"}
          </button>
          <button onClick={() => setEditing(false)} disabled={saving} className="btn btn-ghost btn-xs">
            ✕
          </button>
        </div>
      );
    }

    return (
      <div style={{ display: "flex", alignItems: "center", gap: 4, minWidth: 0 }}>
        {user.domain ? (
          <a
            href={`https://${user.domain}/intranet/`}
            target="_blank"
            rel="noreferrer"
            className="domain-link"
            title={`Abrir ${user.domain}`}
            onClick={(event) => event.stopPropagation()}
          >
            {highlightTextMatch(user.domain, highlightTerm)}
          </a>
        ) : (
          <span style={{ fontSize: 11, color: "#475569" }}>+ dominio</span>
        )}
        <button
          onClick={(event) => {
            event.stopPropagation();
            setEditing(true);
            setVal(user.domain || "");
          }}
          style={{
            background: "none",
            border: "none",
            color: "#475569",
            cursor: "pointer",
            fontSize: 11,
            padding: "0 2px",
            flexShrink: 0,
          }}
          title="Editar dominio"
        >
          ✏
        </button>
      </div>
    );
  }

  function HttpActionModal({ users, onClose, initialForm = {}, presets = [], deployId = null }) {
    const [form, setForm] = useState({
      action_label: initialForm.action_label || "",
      route_path: initialForm.route_path || "/intranet/",
      method: initialForm.method || "GET",
      request_body: initialForm.request_body || "",
      timeout: initialForm.timeout ?? 15,
    });
    const [mode, setMode] = useState(initialForm.mode === "multiple" ? "multiple" : "single");
    const [presetId, setPresetId] = useState(initialForm.preset_id ? String(initialForm.preset_id) : "");
    const [queuedPresetIds, setQueuedPresetIds] = useState(
      Array.isArray(initialForm.queued_preset_ids) && initialForm.queued_preset_ids.length > 0
        ? initialForm.queued_preset_ids.map((item) => String(item))
        : initialForm.preset_id
          ? [String(initialForm.preset_id)]
          : []
    );
    const [skipPrecheck, setSkipPrecheck] = useState(false);
    const [batchStates, setBatchStates] = useState({});
    const [started, setStarted] = useState(false);
    const [running, setRunning] = useState(false);
    const [sequenceProgress, setSequenceProgress] = useState(null);
    const [precheckState, setPrecheckState] = useState({ loading: false, summary: null });
    const [globalFilter, setGlobalFilter] = useState(null); // null | "ok" | "error" | "skipped"

    const selectedQueuedPresets = presets.filter((preset) => queuedPresetIds.includes(String(preset.id)));

    const applyPreset = (nextPresetId) => {
      setPresetId(nextPresetId);
      const preset = presets.find((item) => String(item.id) === String(nextPresetId));
      if (!preset) return;
      setForm({
        action_label: preset.action_label || "",
        route_path: preset.route_path || "/intranet/",
        method: preset.method || "GET",
        request_body: preset.request_body || "",
        timeout: Math.max(1, Math.round((preset.timeout_ms || 15000) / 1000)),
      });
    };

    const toggleQueuedPreset = (nextPresetId) => {
      const normalizedId = String(nextPresetId);
      setQueuedPresetIds((prev) => (
        prev.includes(normalizedId)
          ? prev.filter((item) => item !== normalizedId)
          : [...prev, normalizedId]
      ));
    };

    const selectAllQueuedPresets = () => {
      setQueuedPresetIds(presets.map((preset) => String(preset.id)));
    };

    const clearQueuedPresets = () => {
      setQueuedPresetIds([]);
    };

    const stopBatch = async (batchId) => {
      await apiFetch(`/http-actions/${batchId}/stop`, { method: "POST" });
      const status = await apiFetch(`/http-actions/${batchId}/status`);
      setBatchStates((prev) => ({ ...prev, [batchId]: status }));
    };

    const POLL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutos máximo de polling

    const pollBatches = (batchIds) =>
      new Promise((resolve) => {
        const deadline = Date.now() + POLL_TIMEOUT_MS;

        const tick = async () => {
          // Timeout de seguridad: si lleva más de 30 min, finalizar
          if (Date.now() > deadline) {
            resolve();
            return;
          }

          let done = true;
          for (const batchId of batchIds) {
            const status = await apiFetch(`/http-actions/${batchId}/status`);
            if (!status.error) {
              setBatchStates((prev) => ({ ...prev, [batchId]: status }));
              // Un lote sigue activo solo si: status pending/running Y (is_running=true O es reciente)
              // Si status='running' pero is_running=false → lote huérfano, el backend lo finalizó
              const isActive = (
                ["pending", "running"].includes(status.batch?.status) &&
                (status.batch?.is_running !== false)
              );
              if (isActive) done = false;
            }
          }

          if (done) {
            resolve();
            return;
          }

          setTimeout(tick, 1500);
        };

        tick();
      });

    const startBatches = async (action, sessionId, sessionLabel) => {
      const byServer = {};
      users.forEach((user) => {
        if (!byServer[user.serverId]) byServer[user.serverId] = [];
        byServer[user.serverId].push(user.username);
      });

      const batchIds = [];
      for (const [serverId, usernames] of Object.entries(byServer)) {
        const response = await apiFetch("/http-actions/run", {
          method: "POST",
          body: {
            server_id: Number.parseInt(serverId, 10),
            deploy_id: deployId || undefined,
            skip_precheck: skipPrecheck || undefined,
            session_id: sessionId || undefined,
            session_label: sessionLabel || undefined,
            usernames,
            action_label: action.action_label,
            route_path: action.route_path,
            method: action.method,
            request_body: action.method === "GET" ? "" : action.request_body,
            timeout_ms: Number(action.timeout || 15) * 1000,
          },
        });

        if (response.error) {
          throw new Error(response.error);
        }

        batchIds.push(response.batch_id);
      }

      return batchIds;
    };

    const runPrecheck = async () => {
      const byServer = {};
      users.forEach((user) => {
        if (!byServer[user.serverId]) {
          byServer[user.serverId] = {
            serverId: user.serverId,
            serverLabel: user.serverLabel,
            usernames: [],
          };
        }
        byServer[user.serverId].usernames.push(user.username);
      });

      setPrecheckState({ loading: true, summary: null });
      const serverResponses = [];

      for (const item of Object.values(byServer)) {
        const response = await apiFetch("/http-actions/precheck", {
          method: "POST",
          body: {
            server_id: item.serverId,
            usernames: item.usernames,
          },
        });

        if (response.error) {
          alert(`${item.serverLabel}: ${response.error}`);
          continue;
        }

        serverResponses.push(response);
      }

      const inactiveRows = serverResponses.flatMap((item) =>
        (item.rows || [])
          .filter((row) => row.action_status === "skipped")
          .map((row) => ({ ...row, server_label: item.server_label }))
      );

      setPrecheckState({
        loading: false,
        summary: {
          checked_at: new Date().toISOString(),
          total: serverResponses.reduce((acc, item) => acc + (item.total || 0), 0),
          ready_count: serverResponses.reduce((acc, item) => acc + (item.ready_count || 0), 0),
          skipped_count: serverResponses.reduce((acc, item) => acc + (item.skipped_count || 0), 0),
          servers: serverResponses,
          inactive_rows: inactiveRows,
        },
      });
    };

    const startAction = async () => {
      // Generar session_id único para agrupar todos los lotes de esta ejecución en el historial
      const sessionId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);

      if (mode === "multiple") {
        if (!selectedQueuedPresets.length) {
          alert("Selecciona al menos un preset para ejecutar la cola");
          return;
        }

        const presetNames = selectedQueuedPresets.map((p) => p.name);
        const sessionLabel = presetNames.length > 3
          ? `Cola: ${presetNames.length} presets (${presetNames.slice(0, 3).join(", ")}...)`
          : `Cola: ${presetNames.join(", ")}`;

        setStarted(true);
        setRunning(true);
        setSequenceProgress({ current: 0, total: selectedQueuedPresets.length, name: "" });

        try {
          for (let index = 0; index < selectedQueuedPresets.length; index += 1) {
            const preset = selectedQueuedPresets[index];
            setSequenceProgress({
              current: index + 1,
              total: selectedQueuedPresets.length,
              name: preset.name,
            });

            const batchIds = await startBatches({
              action_label: preset.action_label || "",
              route_path: preset.route_path || "/intranet/",
              method: preset.method || "GET",
              request_body: preset.request_body || "",
              timeout: Math.max(1, Math.round((preset.timeout_ms || 15000) / 1000)),
            }, sessionId, sessionLabel);
            if (batchIds.length) {
              await pollBatches(batchIds);
            }
          }
        } catch (error) {
          alert(error.message || "No se pudo ejecutar la cola de acciones");
        } finally {
          setRunning(false);
        }

        return;
      }

      if (!String(form.route_path || "").trim()) {
        alert("Ingresa la ruta relativa a ejecutar");
        return;
      }
      if (!String(form.route_path || "").trim().startsWith("/")) {
        alert("La ruta debe empezar con /");
        return;
      }
      if (form.method !== "GET" && String(form.request_body || "").trim()) {
        try {
          JSON.parse(form.request_body);
        } catch (_error) {
          alert("El body debe ser JSON válido");
          return;
        }
      }

      setStarted(true);
      setRunning(true);
      setSequenceProgress(null);

      const sessionLabel = form.action_label || `${form.method || "GET"} ${form.route_path || ""}`;

      try {
        const batchIds = await startBatches(form, sessionId, sessionLabel);
        if (!batchIds.length) {
          setRunning(false);
          return;
        }
        await pollBatches(batchIds);
      } catch (error) {
        alert(error.message || "No se pudo ejecutar la acción");
      } finally {
        setRunning(false);
      }
    };

    const allDone = started && !running;
    const groupedUsers = users.reduce((acc, user) => {
      if (!acc[user.serverId]) acc[user.serverId] = [];
      acc[user.serverId].push(user);
      return acc;
    }, {});

    return (
      <div className="modal-overlay">
        <div className="modal modal-lg">
          <div className="modal-head">
            <span>⚡ Acción HTTP — {users.length} usuario{users.length !== 1 ? "s" : ""}</span>
            {!running && (
              <button onClick={onClose} className="btn btn-ghost btn-sm">
                ✕
              </button>
            )}
          </div>
          <div className="modal-body">
            {!started && (
              <>
                <div className="alert alert-info" style={{ marginBottom: 14 }}>
                  {mode === "multiple" ? (
                    <>
                      Se ejecutarán <strong style={{ color: "#e2e8f0" }}>{selectedQueuedPresets.length || 0} acciones</strong> en orden,
                      reutilizando los presets guardados sobre estos mismos usuarios con dominio configurado.
                    </>
                  ) : (
                    <>
                      Se ejecutará como <strong style={{ color: "#e2e8f0" }}>https://dominio{form.route_path || "/ruta"}</strong> solo sobre usuarios con deploy exitoso y dominio configurado.
                    </>
                  )}
                  {!skipPrecheck && (
                    <div style={{ marginTop: 8 }}>
                      Antes de lanzar la ruta final, el sistema hace un <strong style={{ color: "#e2e8f0" }}>precheck web en https://dominio/intranet/</strong> para omitir dominios caídos o sin DNS.
                    </div>
                  )}
                  {skipPrecheck && (
                    <div style={{ marginTop: 8, color: "#f59e0b" }}>
                      ⚠ Precheck desactivado — la acción se ejecutará en <strong style={{ color: "#f59e0b" }}>todos los dominios</strong> sin verificación previa.
                    </div>
                  )}
                </div>

                {precheckState.summary && (
                  <div style={{ marginBottom: 14, background: "#0b1016", border: "1px solid #1e2a3a", borderRadius: 12, padding: 14 }}>
                    <div style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap", marginBottom: 10 }}>
                      <div>
                        <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>Precheck previo</div>
                        <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                          {precheckState.summary.ready_count} listos · {precheckState.summary.skipped_count} omitidos · {precheckState.summary.total} revisados
                        </div>
                      </div>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                        <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                          listos {precheckState.summary.ready_count}
                        </span>
                        <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                          omitidos {precheckState.summary.skipped_count}
                        </span>
                      </div>
                    </div>

                    {precheckState.summary.inactive_rows.length > 0 ? (
                      <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 180, overflowY: "auto", paddingRight: 4 }}>
                        {precheckState.summary.inactive_rows.map((row, index) => (
                          <div key={`${row.server_label}-${row.virt_user}-${index}`} style={{ background: "#131820", border: "1px solid #1e2a3a", borderRadius: 10, padding: 10 }}>
                            <div style={{ fontSize: 12, fontWeight: 700 }}>{row.virt_user} · {row.domain}</div>
                            <div style={{ fontSize: 10, color: "#64748b", marginTop: 4 }}>
                              {row.server_label} · {row.status} · dns {row.dns_ok ? "ok" : "fail"} · https {row.https_ok ? "ok" : "fail"}
                            </div>
                          </div>
                        ))}
                      </div>
                    ) : (
                      <div className="alert alert-success" style={{ marginTop: 6 }}>
                        ✔ Todos los dominios revisados están listos.
                      </div>
                    )}
                  </div>
                )}

                <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 14 }}>
                  {Object.entries(groupedUsers).map(([serverId, serverUsers]) => (
                    <div key={serverId} style={{ background: "#080b10", border: "1px solid #1e2a3a", borderRadius: 10, padding: 12 }}>
                      <div style={{ fontWeight: 700, fontSize: 12, marginBottom: 6 }}>
                        {serverUsers[0]?.serverLabel || `Servidor #${serverId}`} · {serverUsers.length} usuario{serverUsers.length !== 1 ? "s" : ""}
                      </div>
                      <div style={{ fontSize: 11, color: "#94a3b8", lineHeight: 1.6 }}>
                        {serverUsers.map((user) => `${user.username}${user.domain ? ` (${user.domain})` : ""}`).join(", ")}
                      </div>
                    </div>
                  ))}
                </div>

                {presets.length > 1 && (
                  <div className="field">
                    <label>Modo de ejecución</label>
                    <div className="btn-row" style={{ marginBottom: 4 }}>
                      <button
                        type="button"
                        onClick={() => setMode("single")}
                        className={mode === "single" ? "btn btn-primary" : "btn btn-secondary"}
                      >
                        Una acción
                      </button>
                      <button
                        type="button"
                        onClick={() => setMode("multiple")}
                        className={mode === "multiple" ? "btn btn-primary" : "btn btn-secondary"}
                      >
                        Varias acciones
                      </button>
                    </div>
                  </div>
                )}

                {mode === "multiple" ? (
                  <div className="field">
                    <div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 8 }}>
                      <label style={{ marginBottom: 0 }}>Cola de presets</label>
                      <div className="btn-row" style={{ flex: "0 0 auto" }}>
                        <button type="button" onClick={selectAllQueuedPresets} className="btn btn-ghost btn-sm">
                          Todos
                        </button>
                        <button type="button" onClick={clearQueuedPresets} className="btn btn-ghost btn-sm">
                          Ninguno
                        </button>
                      </div>
                    </div>
                    <div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 280, overflowY: "auto", paddingRight: 4 }}>
                      {presets.map((preset) => {
                        const isSelected = queuedPresetIds.includes(String(preset.id));
                        return (
                          <label
                            key={preset.id}
                            style={{
                              display: "flex",
                              alignItems: "flex-start",
                              gap: 10,
                              padding: "10px 12px",
                              borderRadius: 10,
                              border: `1px solid ${isSelected ? "#22d3a544" : "#1e2a3a"}`,
                              background: isSelected ? "#22d3a512" : "#131820",
                              cursor: "pointer",
                            }}
                          >
                            <input
                              type="checkbox"
                              checked={isSelected}
                              onChange={() => toggleQueuedPreset(preset.id)}
                              style={{ marginTop: 3 }}
                            />
                            <div style={{ minWidth: 0 }}>
                              <div style={{ fontWeight: 700, fontSize: 12 }}>{preset.name}</div>
                              <div style={{ fontSize: 10, color: "#64748b", marginTop: 3 }}>
                                {(preset.method || "GET")} {preset.route_path} · timeout {Math.max(1, Math.round((preset.timeout_ms || 15000) / 1000))}s
                              </div>
                            </div>
                          </label>
                        );
                      })}
                    </div>
                  </div>
                ) : (
                  <>
                    {presets.length > 0 && (
                      <div className="field">
                        <label>Preset guardado</label>
                        <select
                          value={presetId}
                          onChange={(event) => applyPreset(event.target.value)}
                          className="input select"
                        >
                          <option value="">Sin preset</option>
                          {presets.map((preset) => (
                            <option key={preset.id} value={preset.id}>
                              {preset.name}
                            </option>
                          ))}
                        </select>
                      </div>
                    )}

                    <div className="grid-3">
                      <div className="field">
                        <label>Etiqueta</label>
                        <input
                          value={form.action_label}
                          onChange={(event) => setForm((prev) => ({ ...prev, action_label: event.target.value }))}
                          className="input"
                          placeholder="Reset cache"
                        />
                      </div>
                      <div className="field">
                        <label>Método</label>
                        <select
                          value={form.method}
                          onChange={(event) => setForm((prev) => ({ ...prev, method: event.target.value }))}
                          className="input select"
                        >
                          {["GET", "POST", "PUT", "PATCH", "DELETE"].map((method) => (
                            <option key={method} value={method}>{method}</option>
                          ))}
                        </select>
                      </div>
                      <div className="field">
                        <label>Timeout (seg)</label>
                        <input
                          type="number"
                          min="1"
                          max="120"
                          value={form.timeout}
                          onChange={(event) => setForm((prev) => ({ ...prev, timeout: event.target.value }))}
                          className="input"
                        />
                      </div>
                    </div>

                    <div className="field">
                      <label>Ruta relativa</label>
                      <input
                        value={form.route_path}
                        onChange={(event) => setForm((prev) => ({ ...prev, route_path: event.target.value }))}
                        className="input"
                        placeholder="/intranet/Api_extention/control_total/reset_cache"
                      />
                    </div>

                    <div className="field">
                      <label>Body JSON opcional</label>
                      <textarea
                        value={form.request_body}
                        onChange={(event) => setForm((prev) => ({ ...prev, request_body: event.target.value }))}
                        className="input"
                        rows={5}
                        disabled={form.method === "GET"}
                        style={{ resize: "vertical" }}
                        placeholder='{"reset":true}'
                      />
                    </div>
                  </>
                )}
              </>
            )}

            {started && sequenceProgress && running && (
              <div className="alert alert-info" style={{ marginBottom: 14 }}>
                Ejecutando acción {sequenceProgress.current} de {sequenceProgress.total}
                {sequenceProgress.name ? ` · ${sequenceProgress.name}` : ""}
              </div>
            )}

            {/* ── Resumen global cuando hay lotes activos ── */}
            {started && Object.keys(batchStates).length > 0 && (() => {
              const allBatches = Object.values(batchStates).map((s) => s?.batch).filter(Boolean);
              const gTotal    = allBatches.reduce((a, b) => a + (b.total_targets || 0), 0);
              const gOk       = allBatches.reduce((a, b) => a + (b.ok_count || 0), 0);
              const gErr      = allBatches.reduce((a, b) => a + (b.error_count || 0) + (b.timeout_count || 0), 0);
              const gSkip     = allBatches.reduce((a, b) => a + (b.skipped_count || 0), 0);
              const gDone     = gOk + gErr + gSkip;
              const gPct      = gTotal > 0 ? Math.round((gDone / gTotal) * 100) : 0;

              const filterBtn = (key, label, count, color) => {
                const active = globalFilter === key;
                return (
                  <button
                    type="button"
                    onClick={() => setGlobalFilter(active ? null : key)}
                    className="tag"
                    style={{
                      borderColor: active ? color : `${color}44`,
                      color,
                      background: active ? `${color}30` : `${color}15`,
                      cursor: "pointer",
                      fontWeight: active ? 700 : 400,
                      outline: active ? `1px solid ${color}` : "none",
                    }}
                  >
                    {label} {count}
                  </button>
                );
              };

              return (
                <div style={{ marginBottom: 16, background: "#0b1016", border: "1px solid #1e2a3a", borderRadius: 12, padding: 14 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8, marginBottom: 10 }}>
                    <div style={{ fontSize: 12, fontWeight: 700, color: "#e2e8f0" }}>
                      Resumen global · {allBatches.length} servidor{allBatches.length !== 1 ? "es" : ""}
                    </div>
                    <div style={{ fontSize: 11, color: "#64748b" }}>{gDone} / {gTotal} usuarios procesados ({gPct}%)</div>
                  </div>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 10, alignItems: "center" }}>
                    {filterBtn("ok",      "✔",  gOk,   "#22d3a5")}
                    {filterBtn("error",   "✗",  gErr,  "#f43f5e")}
                    {filterBtn("skipped", "⏭", gSkip, "#94a3b8")}
                    <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                      ⏳ {gTotal - gDone} pendientes
                    </span>
                    {globalFilter && (
                      <button
                        type="button"
                        onClick={() => setGlobalFilter(null)}
                        style={{ marginLeft: "auto", fontSize: 10, color: "#64748b", background: "none", border: "none", cursor: "pointer", textDecoration: "underline" }}
                      >
                        ✕ ver todo
                      </button>
                    )}
                  </div>
                  <div style={{ background: "#131820", borderRadius: 6, height: 6, overflow: "hidden" }}>
                    <div style={{ display: "flex", height: "100%" }}>
                      <div style={{ width: `${gTotal > 0 ? (gOk / gTotal) * 100 : 0}%`, background: "#22d3a5", transition: "width .3s" }} />
                      <div style={{ width: `${gTotal > 0 ? (gErr / gTotal) * 100 : 0}%`, background: "#f43f5e", transition: "width .3s" }} />
                      <div style={{ width: `${gTotal > 0 ? (gSkip / gTotal) * 100 : 0}%`, background: "#64748b", transition: "width .3s" }} />
                    </div>
                  </div>
                </div>
              );
            })()}

            {/* ── Vista filtrada global ── */}
            {globalFilter && started && (() => {
              const filterLabels = { ok: "Exitosos", error: "Fallidos", skipped: "Ignorados" };
              const filterColors = { ok: "#22d3a5", error: "#f43f5e", skipped: "#94a3b8" };
              const color = filterColors[globalFilter];

              const filteredRows = [];
              for (const [, payload] of Object.entries(batchStates)) {
                if (!payload?.batch || !payload?.results) continue;
                const serverLabel = payload.batch.server_label || `Lote #${payload.batch.id}`;
                for (const result of payload.results) {
                  const match =
                    (globalFilter === "ok"      && result.status === "ok") ||
                    (globalFilter === "error"   && ["error", "http_error", "timeout"].includes(result.status)) ||
                    (globalFilter === "skipped" && result.status === "skipped");
                  if (match) filteredRows.push({ ...result, _serverLabel: serverLabel });
                }
              }

              return (
                <div style={{ marginBottom: 16, border: `1px solid ${color}33`, borderRadius: 12, overflow: "hidden" }}>
                  <div style={{ padding: "10px 14px", background: `${color}12`, borderBottom: `1px solid ${color}22`, display: "flex", alignItems: "center", gap: 8 }}>
                    <span style={{ fontSize: 12, fontWeight: 700, color }}>{filterLabels[globalFilter]} · {filteredRows.length} dominio{filteredRows.length !== 1 ? "s" : ""}</span>
                    <button
                      type="button"
                      onClick={() => setGlobalFilter(null)}
                      style={{ marginLeft: "auto", fontSize: 11, color: "#64748b", background: "none", border: "none", cursor: "pointer" }}
                    >
                      ✕ cerrar filtro
                    </button>
                  </div>
                  {filteredRows.length === 0 ? (
                    <div style={{ padding: "14px", fontSize: 11, color: "#475569", textAlign: "center" }}>
                      {running ? "⟳ Cargando resultados..." : "Sin resultados en esta categoría"}
                    </div>
                  ) : (
                    <div style={{ background: "#080b10", maxHeight: 420, overflowY: "auto" }}>
                      {filteredRows.map((result) => {
                        const resultTone = HTTP_ACTION_COLORS[result.status] || "#64748b";
                        return (
                          <div
                            key={`${result.id}-${result._serverLabel}`}
                            style={{ padding: "8px 14px", display: "flex", alignItems: "flex-start", gap: 10, borderBottom: "1px solid #1e2a3a15", fontSize: 12 }}
                          >
                            <span style={{ color: resultTone, fontSize: 14, minWidth: 20, flexShrink: 0 }}>
                              {HTTP_ACTION_ICONS[result.status] || "•"}
                            </span>
                            <div style={{ flex: 1, minWidth: 0 }}>
                              <div style={{ fontWeight: 700 }}>{result.virt_user}</div>
                              <div style={{ color: "#64748b", fontSize: 10 }}>{result.domain}</div>
                              <div style={{ fontSize: 10, color: "#475569", marginTop: 2 }}>{result._serverLabel}</div>
                              {result.response_excerpt && (
                                <div style={{ fontSize: 10, color: "#94a3b8", marginTop: 4, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                                  {result.response_excerpt.slice(0, 220)}
                                </div>
                              )}
                            </div>
                            <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
                              <span className="tag" style={{ borderColor: `${resultTone}44`, color: resultTone, background: `${resultTone}15` }}>
                                {result.status}
                              </span>
                              {result.http_status > 0 && (
                                <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                                  HTTP {result.http_status}
                                </span>
                              )}
                              {result.duration_ms != null && (
                                <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                                  {result.duration_ms}ms
                                </span>
                              )}
                            </div>
                          </div>
                        );
                      })}
                    </div>
                  )}
                </div>
              );
            })()}

            {/* ── Detalle por servidor ── */}
            {Object.entries(batchStates).map(([batchId, payload]) => {
              if (!payload?.batch) return null;
              const batch = payload.batch;
              const tone = HTTP_ACTION_COLORS[batch.status] || "#64748b";
              const bOk   = batch.ok_count || 0;
              const bErr  = (batch.error_count || 0) + (batch.timeout_count || 0);
              const bSkip = batch.skipped_count || 0;
              const bTotal = batch.total_targets || 0;
              const bDone  = bOk + bErr + bSkip;
              const bPct   = bTotal > 0 ? Math.round((bDone / bTotal) * 100) : 0;
              const isActive = ["pending", "running"].includes(batch.status) && batch.is_running !== false;
              return (
                <div key={batchId} style={{ marginBottom: 16, border: "1px solid #1e2a3a22", borderRadius: 10, overflow: "hidden" }}>
                  {/* Cabecera del servidor */}
                  <div style={{ background: "#0d1420", padding: "10px 14px" }}>
                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap", marginBottom: 8 }}>
                      <div>
                        <div style={{ fontFamily: "Syne,sans-serif", fontWeight: 700, fontSize: 13 }}>
                          {batch.server_label}
                        </div>
                        <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                          {batch.action_label || `${batch.method} ${batch.route_path}`} · timeout {Math.round((batch.timeout_ms || 0) / 1000)}s
                        </div>
                      </div>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
                        <span className="tag" style={{ borderColor: `${tone}44`, color: tone, background: `${tone}15` }}>
                          {HTTP_ACTION_ICONS[batch.status] || "•"} {batch.status}
                        </span>
                        {isActive && (
                          <button onClick={() => stopBatch(batch.id)} className="btn btn-ghost btn-sm" style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}>
                            ⏹ Detener
                          </button>
                        )}
                      </div>
                    </div>

                    {/* Contadores por servidor */}
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                      <span className="tag" style={{ borderColor: "#22d3a544", color: "#22d3a5", background: "#22d3a515" }}>
                        ✔ {bOk}
                      </span>
                      <span className="tag" style={{ borderColor: "#f43f5e44", color: "#f43f5e", background: "#f43f5e15" }}>
                        ✗ {bErr}
                      </span>
                      <span className="tag" style={{ borderColor: "#94a3b844", color: "#94a3b8", background: "#94a3b815" }}>
                        ⏭ {bSkip}
                      </span>
                      <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                        ⏳ {Math.max(0, bTotal - bDone)}
                      </span>
                      <span className="tag" style={{ borderColor: "#64748b44", color: "#64748b", background: "transparent" }}>
                        {bDone}/{bTotal} · {bPct}%
                      </span>
                    </div>

                    {/* Barra de progreso por servidor */}
                    <div style={{ background: "#131820", borderRadius: 4, height: 4, overflow: "hidden" }}>
                      <div style={{ display: "flex", height: "100%" }}>
                        <div style={{ width: `${bTotal > 0 ? (bOk / bTotal) * 100 : 0}%`, background: "#22d3a5", transition: "width .3s" }} />
                        <div style={{ width: `${bTotal > 0 ? (bErr / bTotal) * 100 : 0}%`, background: "#f43f5e", transition: "width .3s" }} />
                        <div style={{ width: `${bTotal > 0 ? (bSkip / bTotal) * 100 : 0}%`, background: "#475569", transition: "width .3s" }} />
                      </div>
                    </div>
                  </div>

                  {/* Resultados por dominio */}
                  {(() => {
                    const allResults = payload.results || [];
                    const executedResults = allResults.filter((r) => r.status !== "skipped");
                    const skippedResults  = allResults.filter((r) => r.status === "skipped");

                    const renderRow = (result) => {
                      const resultTone = HTTP_ACTION_COLORS[result.status] || "#64748b";
                      return (
                        <div
                          key={result.id}
                          style={{
                            padding: "8px 14px",
                            display: "flex",
                            alignItems: "flex-start",
                            gap: 10,
                            borderBottom: "1px solid #1e2a3a15",
                            fontSize: 12,
                          }}
                        >
                          <span style={{ color: resultTone, fontSize: 14, minWidth: 20, flexShrink: 0 }}>
                            {HTTP_ACTION_ICONS[result.status] || "•"}
                          </span>
                          <div style={{ flex: 1, minWidth: 120 }}>
                            <div style={{ fontWeight: 700 }}>{result.virt_user}</div>
                            <div style={{ color: "#64748b", fontSize: 10, marginTop: 2 }}>{result.domain}</div>
                            {result.response_excerpt && (
                              <div style={{ fontSize: 10, color: "#94a3b8", marginTop: 5, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                                {result.response_excerpt.slice(0, 220)}
                              </div>
                            )}
                          </div>
                          <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end" }}>
                            <span className="tag" style={{ borderColor: `${resultTone}44`, color: resultTone, background: `${resultTone}15` }}>
                              {result.status}
                            </span>
                            {result.http_status > 0 && (
                              <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                                HTTP {result.http_status}
                              </span>
                            )}
                            {result.duration_ms != null && (
                              <span className="tag" style={{ borderColor: "#64748b44", color: "#94a3b8", background: "#64748b15" }}>
                                {result.duration_ms}ms
                              </span>
                            )}
                          </div>
                        </div>
                      );
                    };

                    return (
                      <div style={{ background: "#080b10" }}>
                        {/* Resultados ejecutados */}
                        {executedResults.map(renderRow)}
                        {executedResults.length === 0 && isActive && (
                          <div style={{ padding: "12px 14px", fontSize: 11, color: "#475569" }}>
                            ⟳ Esperando resultados...
                          </div>
                        )}

                        {/* Dominios ignorados: sección colapsable */}
                        {(skippedResults.length > 0 || (!isActive && bSkip > 0)) && (
                          <details style={{ borderTop: "1px solid #1e2a3a" }}>
                            <summary
                              style={{
                                padding: "8px 14px",
                                fontSize: 11,
                                fontWeight: 700,
                                color: "#64748b",
                                cursor: "pointer",
                                userSelect: "none",
                                listStyle: "none",
                                display: "flex",
                                alignItems: "center",
                                gap: 6,
                              }}
                            >
                              <span style={{ color: "#94a3b8" }}>⏭</span>
                              <span style={{ color: "#94a3b8" }}>
                                {bSkip} dominio{bSkip !== 1 ? "s" : ""} ignorado{bSkip !== 1 ? "s" : ""} por DNS inactivo o servidor caído
                              </span>
                              <span style={{ marginLeft: "auto", fontSize: 10, color: "#475569" }}>▼ ver detalle</span>
                            </summary>
                            <div style={{ background: "#060810" }}>
                              {skippedResults.map(renderRow)}
                              {skippedResults.length < bSkip && (
                                <div style={{ padding: "6px 14px", fontSize: 10, color: "#475569", fontStyle: "italic" }}>
                                  … y {bSkip - skippedResults.length} más no cargados aún
                                </div>
                              )}
                            </div>
                          </details>
                        )}
                      </div>
                    );
                  })()}
                </div>
              );
            })}

            {allDone && (
              <div className="alert alert-success">
                ✔ {mode === "multiple" ? "Cola de acciones completada" : "Acción HTTP completada"}
              </div>
            )}
          </div>
          <div className="modal-foot">
            {!started ? (
              <>
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10, padding: "8px 12px", background: skipPrecheck ? "#f59e0b15" : "#0b1016", border: `1px solid ${skipPrecheck ? "#f59e0b44" : "#1e2a3a"}`, borderRadius: 10 }}>
                  <Toggle value={skipPrecheck} onChange={setSkipPrecheck} color="#f59e0b" />
                  <div>
                    <div style={{ fontSize: 12, fontWeight: 700, color: skipPrecheck ? "#f59e0b" : "#94a3b8" }}>
                      Forzar ejecución (ignorar precheck)
                    </div>
                    <div style={{ fontSize: 10, color: "#64748b", marginTop: 2 }}>
                      {skipPrecheck
                        ? "⚠ Se ejecutará en todos los dominios sin verificar estado web previo"
                        : "El precheck omite dominios caídos o sin DNS antes de ejecutar"}
                    </div>
                  </div>
                </div>
                <div className="btn-row">
                  <button onClick={onClose} className="btn btn-secondary">
                    Cancelar
                  </button>
                  {!skipPrecheck && (
                    <button onClick={runPrecheck} disabled={precheckState.loading} className="btn btn-secondary">
                      {precheckState.loading ? "⟳ Revisando..." : "🔎 Precheck"}
                    </button>
                  )}
                  <button onClick={startAction} className="btn btn-primary">
                    ⚡ {mode === "multiple" ? `Ejecutar ${selectedQueuedPresets.length || 0} acciones` : "Ejecutar acción"}
                  </button>
                </div>
              </>
            ) : (() => {
              const batchList = Object.values(batchStates);
              const activeCount = batchList.filter((s) =>
                ["pending", "running"].includes(s?.batch?.status) && s?.batch?.is_running !== false
              ).length;
              const doneCount = batchList.filter((s) =>
                s?.batch && !["pending", "running"].includes(s.batch.status)
              ).length;
              const totalBatches = batchList.length;

              return (
                <div style={{ width: "100%" }}>
                  {running && totalBatches > 1 && (
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8, padding: "6px 0" }}>
                      <span style={{ fontSize: 11, color: "#64748b" }}>
                        ✔ {doneCount} de {totalBatches} servidores completados
                        {activeCount > 0 && ` · ${activeCount} ejecutando`}
                      </span>
                      <button
                        onClick={async () => {
                          for (const s of batchList) {
                            if (s?.batch && ["pending", "running"].includes(s.batch.status)) {
                              await apiFetch(`/http-actions/${s.batch.id}/stop`, { method: "POST" });
                            }
                          }
                        }}
                        className="btn btn-ghost btn-sm"
                        style={{ color: "#f43f5e", borderColor: "#f43f5e44" }}
                      >
                        ⏹ Detener todos
                      </button>
                    </div>
                  )}
                  {!running && (() => {
                    const allBatches = batchList.map((s) => s?.batch).filter(Boolean);
                    const totalFailed = allBatches.reduce((a, b) => a + (b.error_count || 0) + (b.timeout_count || 0), 0);
                    const totalSkipped = allBatches.reduce((a, b) => a + (b.skipped_count || 0), 0);

                    const doRetryFailed = async () => {
                      const retrySessionId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
                      const retrySessionLabel = `Reintento fallidos · ${new Date().toLocaleTimeString("es-PE", { hour: "2-digit", minute: "2-digit" })}`;
                      const newIds = {};
                      for (const s of batchList) {
                        const batch = s?.batch;
                        if (!batch) continue;
                        const failCount = (batch.error_count || 0) + (batch.timeout_count || 0);
                        if (failCount > 0) {
                          const result = await apiFetch(`/http-actions/${batch.id}/retry`, { method: "POST", body: { session_id: retrySessionId, session_label: retrySessionLabel } });
                          if (result?.batch_id) newIds[result.batch_id] = null;
                        }
                      }
                      if (Object.keys(newIds).length > 0) {
                        setBatchStates(newIds);
                        setRunning(true);
                        pollBatches(Object.keys(newIds).map(Number)).finally(() => setRunning(false));
                      }
                    };

                    const doRetrySkipped = async () => {
                      const retrySessionId = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
                      const retrySessionLabel = `Reintento omitidos · ${new Date().toLocaleTimeString("es-PE", { hour: "2-digit", minute: "2-digit" })}`;
                      const newIds = {};
                      for (const s of batchList) {
                        const batch = s?.batch;
                        if (!batch) continue;
                        if ((batch.skipped_count || 0) > 0) {
                          const result = await apiFetch(`/http-actions/${batch.id}/retry-skipped`, { method: "POST", body: { session_id: retrySessionId, session_label: retrySessionLabel } });
                          if (result?.batch_id) newIds[result.batch_id] = null;
                        }
                      }
                      if (Object.keys(newIds).length > 0) {
                        setBatchStates(newIds);
                        setRunning(true);
                        pollBatches(Object.keys(newIds).map(Number)).finally(() => setRunning(false));
                      }
                    };

                    return (
                      <div style={{ display: "flex", gap: 8, justifyContent: "center", flexWrap: "wrap", marginBottom: 6 }}>
                        {totalFailed > 0 && (
                          <button onClick={doRetryFailed} className="btn btn-sm" style={{ color: "#f43f5e", borderColor: "#f43f5e55", background: "#f43f5e11" }}>
                            ↻ Reintentar fallidos ({totalFailed})
                          </button>
                        )}
                        {totalSkipped > 0 && (
                          <button onClick={doRetrySkipped} className="btn btn-sm" style={{ color: "#f59e0b", borderColor: "#f59e0b55", background: "#f59e0b11" }}>
                            ↻ Reintentar omitidos ({totalSkipped})
                          </button>
                        )}
                      </div>
                    );
                  })()}
                  <div
                    style={{
                      textAlign: "center",
                      fontSize: 12,
                      color: running ? "#00e5ff" : "#22d3a5",
                      padding: "6px 0",
                    }}
                  >
                    {running ? `⟳ Ejecutando${totalBatches > 1 ? ` (${activeCount}/${totalBatches} servidores)` : "..."}` : "✔ Completado"}
                  </div>
                </div>
              );
            })()}
          </div>
        </div>
      </div>
    );
  }

  function ModalLoadingOverlay({
    title = "Cargando modal",
    message = "Preparando la informacion, espera un momento...",
    front = false,
  }) {
    return (
      <div className={`modal-overlay${front ? " modal-overlay-front" : ""}`}>
        <div className="modal modal-loading-card">
          <div className="modal-loading-spinner" aria-hidden="true" />
          <div className="modal-loading-title">{title}</div>
          <div className="modal-loading-copy">{message}</div>
        </div>
      </div>
    );
  }

  function FilterableFileList({
    files = [],
    getSearchableText = (file) => file?.path || String(file || ""),
    renderItem,
    placeholder = "Buscar archivo…",
    maxHeight = 260,
    emptyMessage = "No hay archivos.",
    autoFocus = false,
    showCount = true,
    style,
  }) {
    const CHUNK = 100;
    const [search, setSearch] = useState("");
    // Render incremental: listas grandes (previews/diagnóstico) reventaban el DOM al
    // pintar miles de filas de una. Mostramos de a CHUNK con un botón "Ver más".
    const [visibleCount, setVisibleCount] = useState(CHUNK);
    const items = Array.isArray(files) ? files : [];
    const term = search.trim().toLowerCase();
    const filtered = term
      ? items.filter((file) => String(getSearchableText(file) || "").toLowerCase().includes(term))
      : items;
    const visible = filtered.slice(0, visibleCount);
    const remaining = filtered.length - visible.length;

    const resetSearch = (value) => {
      setSearch(value);
      setVisibleCount(CHUNK); // al cambiar el filtro, reiniciamos la ventana visible
    };

    return (
      <div style={style}>
        <div style={{ display: "flex", gap: 8, marginBottom: 8, alignItems: "center" }}>
          <input
            value={search}
            onChange={(event) => resetSearch(event.target.value)}
            placeholder={placeholder}
            className="input"
            autoFocus={autoFocus}
            style={{ flex: 1, fontSize: 11 }}
          />
          {search && (
            <button onClick={() => resetSearch("")} className="btn btn-ghost btn-sm" type="button">
              Limpiar
            </button>
          )}
          {showCount && (
            <span style={{ fontSize: 11, color: "#64748b", whiteSpace: "nowrap" }}>
              {term ? `${filtered.length}/${items.length}` : `${items.length}`}
            </span>
          )}
        </div>
        <div style={{ background: "#080b10", borderRadius: 8, maxHeight, overflowY: "auto" }}>
          {!filtered.length ? (
            <div style={{ padding: "12px", fontSize: 11, color: "#64748b" }}>
              {term ? `Sin resultados para "${search}".` : emptyMessage}
            </div>
          ) : (
            <>
              {visible.map((file, index) => renderItem(file, index))}
              {remaining > 0 && (
                <button
                  onClick={() => setVisibleCount((count) => count + CHUNK)}
                  className="btn btn-ghost btn-sm"
                  type="button"
                  style={{ width: "100%", borderRadius: 0, fontSize: 11 }}
                >
                  Ver más ({remaining} restante{remaining === 1 ? "" : "s"})
                </button>
              )}
            </>
          )}
        </div>
      </div>
    );
  }

  Object.assign(window.DCComponents, { Toggle, DomainCell, HttpActionModal, ModalLoadingOverlay, FilterableFileList });
})();
