// ═══ ComfyUI Serverless Admin UI ═══

const MODEL_FOLDERS = [
  "checkpoints",
  "diffusion_models",
  "loras",
  "vae",
  "clip",
  "text_encoders",
  "upscale_models",
  "latent_upscale_models",
  "controlnet",
  "style_models",
  "embeddings",
  "unet",
  "clip_vision",
  "ipadapter",
  "animatediff_models",
  "animatediff_motion_lora",
  "audio_encoders",
  "vae_approx",
  "ultralytics",
  "facerestore_models",
  "inpaint",
  "gguf",
  "sams",
  "pulid",
  "insightface",
  "grounding_dino",
];

let state = {
  models: {},
  custom_nodes: [],
  custom_node_models: {},
  requirements: [],
  env: {},
  comfy_commit_hash: "",
};

const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s);

// ═══ Theme toggle (light / dark) ═══
(function initTheme() {
  const saved = localStorage.getItem("theme") || "dark";
  if (saved === "light") document.documentElement.setAttribute("data-theme", "light");
  document.addEventListener("DOMContentLoaded", () => {
    syncThemeIcons();
    document.getElementById("themeToggle")?.addEventListener("click", () => {
      const isLight = document.documentElement.getAttribute("data-theme") === "light";
      if (isLight) {
        document.documentElement.removeAttribute("data-theme");
        localStorage.setItem("theme", "dark");
      } else {
        document.documentElement.setAttribute("data-theme", "light");
        localStorage.setItem("theme", "light");
      }
      syncThemeIcons();
    });
  });
  function syncThemeIcons() {
    const isLight = document.documentElement.getAttribute("data-theme") === "light";
    const darkIcon = document.querySelector(".theme-icon-dark");
    const lightIcon = document.querySelector(".theme-icon-light");
    if (darkIcon) darkIcon.style.display = isLight ? "none" : "";
    if (lightIcon) lightIcon.style.display = isLight ? "" : "none";
  }
})();

// ═══ Tab switching (horizontal top tabs + runners chip) ═══
function activateTab(tab) {
  $$(".tab-trigger").forEach((b) => b.classList.toggle("active", b.dataset.tab === tab));
  const runnersChip = $("#runnersChip");
  const outputsChip = $("#outputsChip");
  const settingsChip = $("#settingsChip");
  if (runnersChip) runnersChip.classList.toggle("active", tab === "runners");
  if (outputsChip) outputsChip.classList.toggle("active", tab === "outputs");
  if (settingsChip) settingsChip.classList.toggle("active", tab === "settings");
  $$(".tab").forEach((t) => t.classList.add("hidden"));
  const target = $(`#tab-${tab}`);
  if (target) target.classList.remove("hidden");
  if (tab === "runners") loadRunners();
  if (tab === "settings") populateSettingsTab();
  if (tab === "outputs") loadOutputs();
}

$$(".tab-trigger").forEach((btn) => {
  btn.addEventListener("click", () => activateTab(btn.dataset.tab));
});
$("#runnersChip")?.addEventListener("click", () => activateTab("runners"));
$("#outputsChip")?.addEventListener("click", () => activateTab("outputs"));
$("#settingsChip")?.addEventListener("click", () => activateTab("settings"));

// ═══ Bottom drawer log tabs (Deploy / Proxy / Runner) ═══
function activateLogTab(which) {
  $$(".logs-tab").forEach((t) => t.classList.toggle("active", t.dataset.logstab === which));
  $$(".logs-pane").forEach((p) => p.classList.toggle("hidden", p.dataset.pane !== which));
  document.querySelector(".actionbar")?.classList.add("expanded");
}
$$(".logs-tab").forEach((t) => {
  t.addEventListener("click", async () => {
    const which = t.dataset.logstab;
    activateLogTab(which);
    // If switching to Runner tab and nothing is streaming yet, auto-pick first active runner
    if (which === "runner" && !_logReader) {
      try {
        const runners = await (await fetch("/api/runners")).json();
        if (Array.isArray(runners) && runners.length > 0) {
          const first = runners[0];
          streamRunnerLogs(first.runner_id);
        } else {
          $("#runnerLogs").textContent = "No active runners. Start the ComfyUI proxy (Run button) or deploy to spin one up.";
        }
      } catch (_) {}
    }
  });
});
$("#logsClear")?.addEventListener("click", () => {
  const active = document.querySelector(".logs-tab.active")?.dataset.logstab;
  const pane = document.querySelector(`.logs-pane[data-pane="${active}"]`);
  if (pane) pane.textContent = "";
});

function setLogTabLive(which, live) {
  const t = document.querySelector(`.logs-tab[data-logstab="${which}"]`);
  if (t) t.classList.toggle("live", !!live);
}
function setLogsHint(text) {
  const el = $("#logsHint");
  if (el) el.textContent = text || "";
}

// ═══ Toast ═══
function toast(msg, type = "success") {
  const el = document.createElement("div");
  el.className = `toast ${type}`;
  el.textContent = msg;
  $("#toastStack").appendChild(el);
  setTimeout(() => el.remove(), 3000);
}

// ═══ Load initial config ═══
async function loadConfig() {
  const resp = await fetch("/api/config");
  state = await resp.json();
  $("#envApp").textContent = state.env.APP_NAME || "comfy";
  $("#envCommit").textContent = (state.comfy_commit_hash || "").slice(0, 8);
  const depHint = $("#deployTarget");
  if (depHint) depHint.textContent = "— " + (state.env.APP_NAME || "comfy");
  renderModels();
  renderNodes();
  renderDeps();
  populateFolderFilter();
}

function populateFolderFilter() {
  const select = $("#modelFolderFilter");
  select.innerHTML = '<option value="">All folders</option>';
  Object.keys(state.models).forEach((f) => {
    const opt = document.createElement("option");
    opt.value = f;
    opt.textContent = f;
    select.appendChild(opt);
  });
}

// ═══ MODELS ═══
function renderModels() {
  const tbody = $("#modelsTable tbody");
  tbody.innerHTML = "";
  const filter = $("#modelFilter").value.toLowerCase();
  const folderFilter = $("#modelFolderFilter").value;
  const rows = [];
  for (const [folder, list] of Object.entries(state.models)) {
    if (folderFilter && folder !== folderFilter) continue;
    for (let i = 0; i < list.length; i++) {
      const [name, url] = list[i];
      if (filter && !name.toLowerCase().includes(filter) && !folder.toLowerCase().includes(filter)) continue;
      rows.push({ folder, name, url, idx: i });
    }
  }
  if (rows.length === 0) {
    tbody.innerHTML = '<tr><td colspan="4" class="muted" style="padding:32px;text-align:center">No models yet. Click "Add Model" to get started.</td></tr>';
    return;
  }
  rows.forEach(({ folder, name, url, idx }) => {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><span class="folder-badge">${folder}</span></td>
      <td class="mono">${name}</td>
      <td class="url"><span class="mono" title="${url}">${url}</span></td>
      <td>
        <button class="btn-icon" data-edit>✎</button>
        <button class="btn-icon" data-del>🗑️</button>
      </td>
    `;
    tr.querySelector("[data-edit]").onclick = () => openModelModal(folder, idx);
    tr.querySelector("[data-del]").onclick = () => deleteModel(folder, idx);
    tbody.appendChild(tr);
  });
}

$("#modelFilter").addEventListener("input", renderModels);
$("#modelFolderFilter").addEventListener("change", renderModels);

$("#btnAddModel").addEventListener("click", () => openModelModal());

function openModelModal(existingFolder, existingIdx) {
  const isEdit = existingFolder !== undefined;
  const cur = isEdit ? state.models[existingFolder][existingIdx] : ["", ""];
  $("#modalTitle").textContent = isEdit ? "Edit Model" : "Add Model";
  $("#modalBody").innerHTML = `
    <div>
      <label>Folder</label>
      <select id="mFolder">
        ${MODEL_FOLDERS.map(f => `<option value="${f}" ${existingFolder === f ? 'selected' : ''}>${f}</option>`).join('')}
      </select>
      <div class="field-help">Where this model lives on the server. See docs for which folder to use.</div>
    </div>
    <div>
      <label>Filename (subfolder allowed)</label>
      <input type="text" id="mName" value="${cur[0]}" placeholder="my-model.safetensors  or  klein/my-model.safetensors" />
      <div class="field-help">Use a slash to save into a sub-folder, e.g. <code class="mono">klein/flux2-vae.safetensors</code> saves into <code class="mono">loras/klein/</code> (or the matching folder). Plain <code class="mono">file.safetensors</code> goes straight into the selected folder.</div>
    </div>
    <div>
      <label>Download URL</label>
      <input type="url" id="mUrl" value="${cur[1]}" placeholder="https://huggingface.co/.../file.safetensors" />
      <div class="validating" id="mUrlStatus"></div>
    </div>
    <div class="modal-footer">
      <button class="btn" id="mCancel">Cancel</button>
      <button class="btn btn-primary" id="mSave">Save</button>
    </div>
  `;
  $("#modal").classList.remove("hidden");

  // Live URL validation
  let urlTimer;
  $("#mUrl").addEventListener("input", () => {
    clearTimeout(urlTimer);
    const url = $("#mUrl").value.trim();
    if (!url) { $("#mUrlStatus").textContent = ""; return; }
    $("#mUrlStatus").textContent = "Checking...";
    $("#mUrlStatus").className = "validating";
    urlTimer = setTimeout(async () => {
      const resp = await fetch(`/api/validate_url?url=${encodeURIComponent(url)}`);
      const { ok, status } = await resp.json();
      $("#mUrlStatus").textContent = ok ? "✓ URL reachable" : `✗ URL returned ${status || 'error'}`;
      $("#mUrlStatus").className = "validating " + (ok ? "ok" : "err");
    }, 400);
  });

  $("#mCancel").onclick = closeModal;
  $("#mSave").onclick = async () => {
    const folder = $("#mFolder").value;
    const name = $("#mName").value.trim();
    const url = $("#mUrl").value.trim();
    if (!name || !url) { toast("Name and URL required", "error"); return; }

    if (isEdit) {
      // Remove from old folder if folder changed
      if (folder !== existingFolder) {
        state.models[existingFolder].splice(existingIdx, 1);
        if (!state.models[folder]) state.models[folder] = [];
        state.models[folder].push([name, url]);
      } else {
        state.models[folder][existingIdx] = [name, url];
      }
    } else {
      if (!state.models[folder]) state.models[folder] = [];
      state.models[folder].push([name, url]);
    }

    const r = await fetch("/api/models", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ models: state.models }),
    });
    if (r.ok) {
      toast("Model saved");
      closeModal();
      await loadConfig();
    } else {
      toast("Failed to save", "error");
    }
  };
}

async function deleteModel(folder, idx) {
  const [name] = state.models[folder][idx];
  if (!confirm(`Delete ${name}?`)) return;
  state.models[folder].splice(idx, 1);
  const r = await fetch("/api/models", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ models: state.models }),
  });
  if (r.ok) {
    toast("Model deleted");
    await loadConfig();
  }
}

// ═══ CUSTOM NODES ═══
function renderNodes() {
  const tbody = $("#nodesTable tbody");
  tbody.innerHTML = "";
  if (state.custom_nodes.length === 0) {
    tbody.innerHTML = '<tr><td colspan="3" class="muted" style="padding:32px;text-align:center">No custom nodes.</td></tr>';
    return;
  }
  state.custom_nodes.forEach((node, idx) => {
    const [url, commit] = node;
    const repoName = url.replace(/\.git$/, "").split("/").slice(-2).join("/");
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><div class="mono">${repoName}</div><div class="muted" style="font-size:11px">${url}</div></td>
      <td class="mono">${commit.slice(0, 10)}...</td>
      <td>
        <button class="btn-icon" data-edit>✎</button>
        <button class="btn-icon" data-del>🗑️</button>
      </td>
    `;
    tr.querySelector("[data-edit]").onclick = () => openNodeModal(idx);
    tr.querySelector("[data-del]").onclick = () => deleteNode(idx);
    tbody.appendChild(tr);
  });
}

$("#btnAddNode").addEventListener("click", () => openNodeModal());

function openNodeModal(existingIdx) {
  const isEdit = existingIdx !== undefined;
  const cur = isEdit ? state.custom_nodes[existingIdx] : ["", ""];
  $("#modalTitle").textContent = isEdit ? "Edit Custom Node" : "Add Custom Node";
  $("#modalBody").innerHTML = `
    <div>
      <label>Git URL</label>
      <input type="url" id="nUrl" value="${cur[0]}" placeholder="https://github.com/user/repo.git" />
    </div>
    <div>
      <label>Commit Hash (40 chars)</label>
      <input type="text" id="nCommit" value="${cur[1]}" placeholder="a1b2c3d4..." />
      <div class="validating" id="nCommitStatus"></div>
    </div>
    <div id="nDepsSection" style="display:none">
      <label>Python dependencies from this node's requirements.txt</label>
      <div class="deps-list" id="nDepsList"></div>
      <div class="field-help">Checked deps will be added to your requirements.txt.</div>
    </div>
    <div class="modal-footer">
      <button class="btn" id="nCancel">Cancel</button>
      <button class="btn btn-primary" id="nSave">Save</button>
    </div>
  `;
  $("#modal").classList.remove("hidden");

  // Live commit validation + auto-fetch deps
  let commitTimer;
  const checkCommit = () => {
    clearTimeout(commitTimer);
    const url = $("#nUrl").value.trim();
    const commit = $("#nCommit").value.trim();
    if (!url || !commit) { $("#nCommitStatus").textContent = ""; return; }
    $("#nCommitStatus").textContent = "Checking...";
    $("#nCommitStatus").className = "validating";
    commitTimer = setTimeout(async () => {
      const resp = await fetch(`/api/validate_commit?git_url=${encodeURIComponent(url)}&commit=${encodeURIComponent(commit)}`);
      const { ok } = await resp.json();
      $("#nCommitStatus").textContent = ok ? "✓ Commit exists" : "✗ Commit not found";
      $("#nCommitStatus").className = "validating " + (ok ? "ok" : "err");
      if (ok) await fetchDeps(url, commit);
    }, 400);
  };
  $("#nUrl").addEventListener("input", checkCommit);
  $("#nCommit").addEventListener("input", checkCommit);

  async function fetchDeps(url, commit) {
    const r = await fetch("/api/fetch_node_deps", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ git_url: url, commit }),
    });
    const { deps } = await r.json();
    if (deps.length === 0) {
      $("#nDepsSection").style.display = "none";
      return;
    }
    const existing = new Set(state.requirements.map(l => l.split(/[=<>]/)[0].trim().toLowerCase()));
    $("#nDepsSection").style.display = "block";
    $("#nDepsList").innerHTML = deps.map(d => {
      const pkg = d.split(/[=<>]/)[0].trim().toLowerCase();
      const already = existing.has(pkg);
      return `<label class="dep-item">
        <input type="checkbox" data-dep="${d}" ${already ? '' : 'checked'} ${already ? 'disabled' : ''}/>
        ${d} ${already ? '<span class="muted">(already in requirements)</span>' : ''}
      </label>`;
    }).join("");
  }

  if (isEdit) checkCommit();

  $("#nCancel").onclick = closeModal;
  $("#nSave").onclick = async () => {
    const url = $("#nUrl").value.trim();
    const commit = $("#nCommit").value.trim();
    if (!url || !commit) { toast("URL and commit required", "error"); return; }

    if (isEdit) {
      state.custom_nodes[existingIdx] = [url, commit];
    } else {
      state.custom_nodes.push([url, commit]);
    }

    // Gather selected deps
    const newDeps = [];
    $$("#nDepsList input[type=checkbox]:checked:not(:disabled)").forEach(cb => {
      newDeps.push(cb.dataset.dep);
    });

    const r = await fetch("/api/custom_nodes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ custom_nodes: state.custom_nodes }),
    });
    if (r.ok) {
      if (newDeps.length > 0) {
        state.requirements.push(...newDeps);
        await fetch("/api/requirements", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ requirements: state.requirements }),
        });
        toast(`Saved node + added ${newDeps.length} deps`);
      } else {
        toast("Node saved");
      }
      closeModal();
      await loadConfig();
    }
  };
}

async function deleteNode(idx) {
  const [url] = state.custom_nodes[idx];
  if (!confirm(`Delete ${url}?`)) return;
  state.custom_nodes.splice(idx, 1);
  const r = await fetch("/api/custom_nodes", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ custom_nodes: state.custom_nodes }),
  });
  if (r.ok) {
    toast("Node deleted");
    await loadConfig();
  }
}

// ═══ REQUIREMENTS ═══
function renderDeps() {
  $("#depsEditor").value = state.requirements.join("\n");
}

$("#btnSaveDeps").addEventListener("click", async () => {
  const lines = $("#depsEditor").value.split("\n");
  const r = await fetch("/api/requirements", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ requirements: lines }),
  });
  if (r.ok) { toast("Requirements saved"); state.requirements = lines; }
  else toast("Failed to save", "error");
});

// ═══ RUNNERS ═══
async function loadRunners() {
  $("#runnersStatus").textContent = "Loading...";
  const resp = await fetch("/api/runners");
  const runners = await resp.json();
  const tbody = $("#runnersTable tbody");
  tbody.innerHTML = "";
  if (runners.length === 0) {
    tbody.innerHTML = '<tr><td colspan="6" class="muted" style="padding:32px;text-align:center">No active runners.</td></tr>';
    $("#runnersStatus").textContent = "0 runners";
    return;
  }
  runners.forEach(r => {
    const tr = document.createElement("tr");
    const state = r.state || "UNKNOWN";
    const started = (r.started_at || "").slice(11, 19);
    if (r.is_mine) tr.classList.add("mine");
    const mineTag = r.is_mine
      ? ' <span class="runner-mine-tag">YOURS</span>'
      : "";
    tr.innerHTML = `
      <td class="mono">${r.runner_id.slice(0, 12)}...${mineTag}</td>
      <td><span class="state-badge state-${state}">${state}</span></td>
      <td class="mono">${started}</td>
      <td class="mono">${r.machine_type || '—'}</td>
      <td class="mono">${r.region || '—'}</td>
      <td>
        <button class="btn-icon-only" data-logs title="Stream logs">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h10M4 18h7"/></svg>
        </button>
        <button class="btn-icon-only danger-hover" data-kill title="Terminate runner">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14zM10 11v6M14 11v6"/></svg>
        </button>
      </td>
    `;
    tr.querySelector("[data-logs]").onclick = () => streamRunnerLogs(r.runner_id);
    tr.querySelector("[data-kill]").onclick = async () => {
      if (!confirm(`Terminate runner ${r.runner_id.slice(0, 8)}?`)) return;
      const resp = await fetch(`/api/kill_runner/${r.runner_id}`, { method: "POST" });
      const result = await resp.json();
      if (result.ok) { toast("Runner terminated"); setTimeout(loadRunners, 1000); }
      else toast("Failed: " + (result.error || ""), "error");
    };
    tbody.appendChild(tr);
  });
  $("#runnersStatus").textContent = `${runners.length} runner${runners.length > 1 ? 's' : ''}`;
}

$("#btnRefreshRunners").addEventListener("click", loadRunners);

// ═══ OUTPUTS ═══
async function loadOutputs() {
  const grid = $("#outputsGrid");
  const status = $("#outputsStatus");
  grid.innerHTML = "";
  status.textContent = "Loading…";
  let data;
  try {
    data = await (await fetch("/api/outputs")).json();
  } catch (e) {
    status.textContent = "Network error: " + e.message;
    return;
  }
  if (!data.ok && data.error === "proxy_not_running") {
    grid.innerHTML = `<div class="output-empty">The ComfyUI proxy isn't running. Click <strong>Run ComfyUI</strong> below to start it, then come back.</div>`;
    status.textContent = "";
    return;
  }
  if (!data.ok) {
    grid.innerHTML = `<div class="output-empty">Couldn't load outputs: ${data.error}</div>`;
    status.textContent = "";
    return;
  }
  const outputs = data.outputs || [];
  let totalFiles = 0;
  for (const entry of outputs.slice().reverse()) {
    // `files` is the deduplicated list from the server; fall back to `urls` for safety
    const files = entry.files || (entry.urls || []).map((u) => ({ url: u, filename: u.split("/").pop() || "output" }));
    for (const f of files) {
      totalFiles++;
      const url = f.url;
      const fname = f.filename;
      const ext = (fname.includes(".") ? fname.split(".").pop() : "").toLowerCase();
      const isVideo = ["mp4", "webm", "mov", "gif"].includes(ext);
      const media = isVideo
        ? `<video src="${url}" muted loop playsinline preload="metadata" onmouseenter="this.play()" onmouseleave="this.pause()"></video>`
        : `<img src="${url}" loading="lazy" alt="${fname}" />`;
      const time = (entry.timestamp || "").slice(11, 19);
      const card = document.createElement("div");
      card.className = "output-card";
      card.innerHTML = `
        <div class="output-media">${media}</div>
        <div class="output-meta">
          <span class="output-time">${time}</span>
          <span class="output-actions">
            <a class="btn-icon-only" href="/api/download?url=${encodeURIComponent(url)}&filename=${encodeURIComponent(fname)}" download="${fname}" title="Download">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
            </a>
            <a class="btn-icon-only" href="${url}" target="_blank" rel="noopener" title="Open in new tab">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3"/></svg>
            </a>
          </span>
        </div>
      `;
      grid.appendChild(card);
    }
  }
  if (totalFiles === 0) {
    grid.innerHTML = `<div class="output-empty">No outputs yet. Run a workflow in ComfyUI to generate some.</div>`;
  }
  status.textContent = `${totalFiles} file${totalFiles !== 1 ? "s" : ""} from ${outputs.length} run${outputs.length !== 1 ? "s" : ""}`;
}

$("#btnRefreshOutputs")?.addEventListener("click", loadOutputs);

$("#btnDownloadAllOutputs")?.addEventListener("click", async () => {
  const btn = $("#btnDownloadAllOutputs");
  const original = btn.textContent;
  btn.disabled = true;
  btn.textContent = "Packaging…";
  try {
    // Use hidden iframe/anchor so the browser handles the download
    const a = document.createElement("a");
    a.href = "/api/download_all";
    a.download = "";
    document.body.appendChild(a);
    a.click();
    a.remove();
    // Give the browser a sec to kick off the stream then re-enable
    setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 1500);
  } catch (e) {
    toast("Download failed: " + e.message, "error");
    btn.disabled = false;
    btn.textContent = original;
  }
});

// Also proxy the /api/download path through to the local proxy
// (so the browser can hit admin-ui's URL and we forward to :8188)

let _logReader = null;
async function streamRunnerLogs(runnerId) {
  const logs = $("#runnerLogs");
  activateLogTab("runner");
  setLogTabLive("runner", true);
  setLogsHint(`streaming ${runnerId.slice(0, 12)}…`);
  logs.textContent = `▶ Streaming logs for runner ${runnerId.slice(0, 12)}\n\n`;

  // cancel prior stream
  if (_logReader) { try { await _logReader.cancel(); } catch (_) {} _logReader = null; }

  const resp = await fetch(`/api/runner_logs/${runnerId}`);
  if (!resp.ok) {
    logs.textContent += "[error starting stream]\n";
    setLogTabLive("runner", false);
    setLogsHint("");
    return;
  }
  _logReader = resp.body.getReader();
  const decoder = new TextDecoder();
  try {
    while (true) {
      const { done, value } = await _logReader.read();
      if (done) break;
      logs.textContent += decoder.decode(value);
      logs.scrollTop = logs.scrollHeight;
    }
  } catch (_) {}
  _logReader = null;
  setLogTabLive("runner", false);
  setLogsHint("");
}

// ═══ DEPLOY (with last-deploy status persistence) ═══
function fmtAgo(ts) {
  if (!ts) return "Never deployed";
  const diff = (Date.now() - ts) / 1000;
  if (diff < 60) return "Deployed just now";
  if (diff < 3600) return `Deployed ${Math.floor(diff/60)}m ago`;
  if (diff < 86400) return `Deployed ${Math.floor(diff/3600)}h ago`;
  return `Deployed ${Math.floor(diff/86400)}d ago`;
}

function applyDeployStatus() {
  const el = $("#deployStatus");
  if (!el) return;
  const raw = localStorage.getItem("lastDeploy");
  if (!raw) {
    el.textContent = "Never deployed";
    el.className = "btn-sub";
    return;
  }
  const { ts, ok } = JSON.parse(raw);
  el.textContent = ok ? fmtAgo(ts) : "Last deploy failed";
  el.className = "btn-sub " + (ok ? "deployed" : "failed");
}

$("#btnDeploy").addEventListener("click", async () => {
  // Guard: must have FAL_KEY before deploying
  const envSt = await refreshEnvStatus();
  if (!envSt.has_key) {
    toast("Add your FAL API key first", "error");
    activateTab("settings");
    populateSettingsTab({ warnMissingKey: true });
    $("#setFalKey")?.focus();
    return;
  }

  activateLogTab("deploy");
  setLogTabLive("deploy", true);
  setLogsHint("fal deploy running…");

  const btn = $("#btnDeploy");
  const logs = $("#deployLogs");
  const bar = document.querySelector(".actionbar");
  const status = $("#deployStatus");
  btn.disabled = true;
  status.textContent = "Deploying…";
  status.className = "btn-sub deploying";
  logs.textContent = "";
  bar?.classList.add("expanded");

  let output = "";
  try {
    const resp = await fetch("/api/deploy", { method: "POST" });
    const reader = resp.body.getReader();
    const decoder = new TextDecoder();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value);
      output += chunk;
      logs.textContent += chunk;
      logs.scrollTop = logs.scrollHeight;
    }
    const ok = /\[exit code: 0\]/.test(output);
    localStorage.setItem("lastDeploy", JSON.stringify({ ts: Date.now(), ok }));
    toast(ok ? "Deploy complete" : "Deploy failed", ok ? "success" : "error");
  } catch (e) {
    localStorage.setItem("lastDeploy", JSON.stringify({ ts: Date.now(), ok: false }));
    toast("Deploy error: " + e.message, "error");
  } finally {
    btn.disabled = false;
    setLogTabLive("deploy", false);
    setLogsHint("");
    applyDeployStatus();
  }
});

// ═══ Run ComfyUI — starts the local proxy + streams logs + opens :8188 ═══
let _proxyLogTimer = null;
let _proxyRunning = false;

async function refreshProxyStatus() {
  try {
    const s = await (await fetch("/api/proxy/status")).json();
    _proxyRunning = !!s.running;
    const label = $("#runLabel");
    const sub = $("#runSub");
    if (label && sub) {
      if (_proxyRunning) {
        label.textContent = "Open ComfyUI ↗";
        if (s.runner_state) {
          const state = s.runner_state.toUpperCase();
          const pretty = {
            "IDLE": "runner idle · ready",
            "RUNNING": "runner processing",
            "STARTING": "runner cold-starting… (5–10 min first time)",
            "SETUP": "runner installing deps… (5–10 min first time)",
            "PENDING": "runner queued…",
          }[state] || `runner ${state.toLowerCase()}`;
          sub.textContent = pretty;
        } else {
          sub.textContent = "proxy on :8188";
        }
      } else {
        label.textContent = "Run ComfyUI";
        sub.textContent = "starts local proxy";
      }
    }
  } catch (_) {}
}

function startProxyLogPolling() {
  if (_proxyLogTimer) return;
  activateLogTab("proxy");
  setLogTabLive("proxy", true);
  const pre = $("#proxyLogs");
  _proxyLogTimer = setInterval(async () => {
    try {
      const { lines } = await (await fetch("/api/proxy/logs")).json();
      pre.textContent = lines.join("\n");
      pre.scrollTop = pre.scrollHeight;
    } catch (_) {}
    // Stop pulsing dot when proxy stops
    if (!_proxyRunning) setLogTabLive("proxy", false);
  }, 1200);
}

function stopProxyLogPolling() {
  if (_proxyLogTimer) { clearInterval(_proxyLogTimer); _proxyLogTimer = null; }
  setLogTabLive("proxy", false);
}

$("#btnRun")?.addEventListener("click", async () => {
  const btn = $("#btnRun");

  // Open the tab SYNCHRONOUSLY on the click — browsers only allow popups that
  // happen during a real user gesture, not after an async await.
  // We navigate it later once the proxy is ready.
  const comfyWin = window.open("about:blank", "_blank");
  if (!comfyWin) {
    toast("Your browser blocked the popup. Allow popups for localhost and retry.", "error");
    return;
  }

  // If the proxy is already up, redirect the new tab immediately
  await refreshProxyStatus();
  if (_proxyRunning) {
    comfyWin.location.href = "http://localhost:8188";
    return;
  }

  btn.disabled = true;
  $("#runLabel").textContent = "Starting…";
  $("#runSub").textContent = "booting proxy";
  setLogsHint("proxy booting…");
  startProxyLogPolling();

  // Show a "starting" page in the opened tab so the user sees something
  try {
    comfyWin.document.write(
      '<!DOCTYPE html><html><head><title>Starting ComfyUI…</title>' +
      '<style>body{background:#0a0a0e;color:#eee;font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0;text-align:center}</style>' +
      '</head><body><div><h1 style="font-weight:500">Starting ComfyUI…</h1>' +
      '<p style="opacity:.6">This tab will refresh when the proxy is ready (usually a few seconds).</p></div></body></html>'
    );
  } catch (_) {}

  try {
    const resp = await fetch("/api/proxy/start", { method: "POST" });
    const data = await resp.json();
    if (!resp.ok) {
      toast(data.error || "Failed to start", "error");
      if ((data.error || "").includes("FAL_KEY")) activateTab("settings");
      try { comfyWin.close(); } catch (_) {}
      return;
    }
    // Wait for :8188 to bind (max ~20s)
    let ready = false;
    for (let i = 0; i < 40; i++) {
      await new Promise((r) => setTimeout(r, 500));
      try { await fetch("http://localhost:8188/", { mode: "no-cors" }); ready = true; break; } catch (_) {}
    }
    if (ready) {
      try { comfyWin.location.href = "http://localhost:8188"; } catch (_) {}
      toast("ComfyUI running");
    } else {
      toast("Proxy didn't respond in 20s — check the Proxy logs", "error");
      try { comfyWin.close(); } catch (_) {}
    }
  } catch (e) {
    toast("Error: " + e.message, "error");
    try { comfyWin.close(); } catch (_) {}
  } finally {
    btn.disabled = false;
    await refreshProxyStatus();
  }
});

// ═══ Modal helpers ═══
function closeModal() {
  $("#modal").classList.add("hidden");
}
$("#modalClose").addEventListener("click", closeModal);
$("#modal").addEventListener("click", (e) => {
  if (e.target.id === "modal") closeModal();
});

// ═══ Settings tab (FAL_KEY + APP_NAME) ═══
async function refreshEnvStatus() {
  try {
    const s = await (await fetch("/api/env_status")).json();
    const tabBtn = document.querySelector('.tab-trigger[data-tab="settings"]');
    if (tabBtn) tabBtn.classList.toggle("needs-attention", !s.has_key);
    refreshWhoami();
    return s;
  } catch (_) {
    return { has_key: false };
  }
}

async function refreshWhoami() {
  const el = $("#whoamiSlug");
  if (!el) return;
  try {
    const w = await (await fetch("/api/whoami")).json();
    el.classList.remove("unknown", "warn");
    if (w.ok) {
      el.textContent = w.slug;
      if (!w.has_apps) el.classList.add("warn");
    } else if (w.error === "no_key") {
      el.textContent = "no key";
      el.classList.add("unknown");
    } else if (w.error === "invalid_key") {
      el.textContent = "invalid key";
      el.classList.add("warn");
    } else {
      el.textContent = "error";
      el.classList.add("warn");
    }
  } catch (_) {
    el.textContent = "—";
    el.classList.add("unknown");
  }
}

function populateSettingsTab(opts = {}) {
  const currentApp = (state.env && state.env.APP_NAME) || "comfy";
  const masked = (state.env && state.env.FAL_KEY_MASKED) || "";
  const hasHf = !!(state.env && state.env.HF_TOKEN_SET);
  const hasCv = !!(state.env && state.env.CIVITAI_TOKEN_SET);

  $("#setFalKey").value = "";
  $("#setFalKey").placeholder = masked || "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:xxxxxxxxxxxx…";
  $("#setAppName").value = currentApp;
  $("#setAppEcho").textContent = currentApp;

  if ($("#setHfToken")) {
    $("#setHfToken").value = "";
    $("#setHfToken").placeholder = hasHf ? "(token saved — leave blank to keep it)" : "hf_xxxxxxxxxxxxxxxx";
  }
  if ($("#setCivitaiToken")) {
    $("#setCivitaiToken").value = "";
    $("#setCivitaiToken").placeholder = hasCv ? "(token saved — leave blank to keep it)" : "cv_xxxxxxxxxxxxxxxx";
  }

  const hint = $("#setFalKeyHint");
  if (hint) {
    hint.innerHTML = masked
      ? `Current: <code class="mono">${masked}</code> — leave blank to keep it.`
      : `Generate one at <a href="https://fal.ai/dashboard/keys" target="_blank" style="color:var(--content-light)">fal.ai/dashboard/keys</a> on the correct team.`;
  }
  const existing = document.querySelector("#tab-settings .settings-warn");
  if (existing) existing.remove();
  if (opts.warnMissingKey) {
    const warn = document.createElement("div");
    warn.className = "settings-warn";
    warn.textContent = "A FAL API key is required before deploying.";
    $("#tab-settings").querySelector(".settings-grid").insertAdjacentElement("beforebegin", warn);
  }
}

$("#setAppName")?.addEventListener("input", (e) => {
  $("#setAppEcho").textContent = e.target.value || "comfy";
});

$("#btnDeleteApp")?.addEventListener("click", async () => {
  const appName = (state.env && state.env.APP_NAME) || "comfy";
  const typed = prompt(
    `This will permanently undeploy "${appName}" from fal.ai.\n\nType the app name to confirm:`
  );
  if (typed === null) return;
  if (typed.trim() !== appName) {
    toast("App name doesn't match — nothing deleted", "error");
    return;
  }
  const btn = $("#btnDeleteApp");
  btn.disabled = true;
  btn.textContent = "Undeploying…";
  activateLogTab("deploy");
  setLogTabLive("deploy", true);
  setLogsHint(`fal apps delete ${appName}`);
  try {
    const resp = await fetch("/api/delete_app", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ confirm: typed }),
    });
    const data = await resp.json();
    $("#deployLogs").textContent = data.output || "(no output)";
    if (data.ok) {
      toast(`${appName} undeployed`);
      localStorage.removeItem("lastDeploy");
      applyDeployStatus();
    } else {
      toast("Delete failed — check log", "error");
    }
  } catch (e) {
    toast("Error: " + e.message, "error");
  } finally {
    btn.disabled = false;
    btn.textContent = "Undeploy app…";
    setLogTabLive("deploy", false);
    setLogsHint("");
  }
});

$("#btnSaveSettings")?.addEventListener("click", async () => {
  const payload = {};
  const key = $("#setFalKey").value.trim();
  const appName = $("#setAppName").value.trim();
  const hf = $("#setHfToken")?.value.trim() || "";
  const cv = $("#setCivitaiToken")?.value.trim() || "";
  if (key) payload.FAL_KEY = key;
  if (appName) payload.APP_NAME = appName;
  if (hf) payload.HF_TOKEN = hf;
  if (cv) payload.CIVITAI_TOKEN = cv;
  if (Object.keys(payload).length === 0) { toast("Nothing to save", "error"); return; }

  const btn = $("#btnSaveSettings");
  btn.disabled = true;
  btn.textContent = "Saving…";
  try {
    const resp = await fetch("/api/env", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (!resp.ok) {
      const data = await resp.json().catch(() => ({}));
      toast(data.detail || "Failed to save", "error");
    } else {
      await loadConfig();
      await refreshEnvStatus();
      populateSettingsTab();
      toast("Settings saved");
    }
  } catch (e) {
    toast("Error: " + e.message, "error");
  } finally {
    btn.disabled = false;
    btn.textContent = "Save settings";
  }
});

// ═══ ComfyUI commit editor ═══
$("#commitChip")?.addEventListener("click", openCommitModal);

function openCommitModal() {
  const current = state.comfy_commit_hash || "";
  $("#modalTitle").textContent = "ComfyUI Version";
  $("#modalBody").innerHTML = `
    <div class="form-group">
      <label>Git commit SHA (from <a href="https://github.com/comfyanonymous/ComfyUI/commits/master" target="_blank" style="color:var(--content-light)">comfyanonymous/ComfyUI</a>)</label>
      <input type="text" id="commitInput" value="${current}" placeholder="e.g. acd718598eca0b944a1a7a82072a9dec40d3d4f7" spellcheck="false" autocomplete="off" />
      <p class="muted" id="commitPreview" style="margin:8px 0 0;font-size:12px">Current: <code class="mono">${(current || "—").slice(0, 12)}</code></p>
    </div>
    <div class="form-actions">
      <button class="btn" id="cCancel">Cancel</button>
      <button class="btn btn-primary" id="cSave">Save & validate</button>
    </div>
  `;
  $("#modal").classList.remove("hidden");
  const input = $("#commitInput");
  input.focus();
  input.select();

  $("#cCancel").onclick = closeModal;
  $("#cSave").onclick = async () => {
    const commit = input.value.trim();
    $("#cSave").disabled = true;
    $("#cSave").textContent = "Validating…";
    try {
      const resp = await fetch("/api/commit_hash", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ commit }),
      });
      const data = await resp.json();
      if (!resp.ok) {
        toast(data.detail || "Validation failed", "error");
        $("#cSave").disabled = false;
        $("#cSave").textContent = "Save & validate";
        return;
      }
      state.comfy_commit_hash = data.commit;
      $("#envCommit").textContent = data.commit.slice(0, 8);
      toast(`ComfyUI pinned to ${data.commit.slice(0, 8)}`);
      closeModal();
    } catch (e) {
      toast("Error: " + e.message, "error");
      $("#cSave").disabled = false;
      $("#cSave").textContent = "Save & validate";
    }
  };
}

// ═══ Runners chip — live poll every 15s ═══
async function updateRunnersChip() {
  try {
    const runners = await (await fetch("/api/runners")).json();
    const count = Array.isArray(runners) ? runners.length : 0;
    const states = Array.isArray(runners)
      ? runners.map((r) => (r.state || "").toUpperCase())
      : [];
    const anyRunning = states.includes("RUNNING");
    const anyIdle = states.includes("IDLE");
    const dot = $("#runnersDot");
    const cnt = $("#runnersCount");
    if (cnt) cnt.textContent = String(count);
    if (dot) {
      dot.classList.remove("live", "idle", "warn");
      if (anyRunning) dot.classList.add("live");
      else if (anyIdle) dot.classList.add("idle");
      else if (count > 0) dot.classList.add("warn");
    }
  } catch (_) {}
}

// ═══ Init ═══
loadConfig();
applyDeployStatus();
refreshEnvStatus();
refreshProxyStatus();
updateRunnersChip();
setInterval(updateRunnersChip, 15000);
setInterval(refreshEnvStatus, 20000);
setInterval(refreshProxyStatus, 5000);
