const uiHeader = { "X-Local-UI": "AIWorkerService" };
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const fallbackTimeZones = [
  "Africa/Johannesburg",
  "Africa/Cairo",
  "Africa/Lagos",
  "Africa/Nairobi",
  "America/Chicago",
  "America/Denver",
  "America/Los_Angeles",
  "America/New_York",
  "America/Sao_Paulo",
  "Asia/Dubai",
  "Asia/Hong_Kong",
  "Asia/Kolkata",
  "Asia/Singapore",
  "Asia/Tokyo",
  "Australia/Sydney",
  "Europe/Berlin",
  "Europe/London",
  "Europe/Paris",
  "Pacific/Auckland",
  "UTC"
];
const timeZones = typeof Intl.supportedValuesOf === "function"
  ? Intl.supportedValuesOf("timeZone")
  : fallbackTimeZones;
const currentTermsVersion = "2026-08-05";
let dashboard = null;
let scheduleEditing = false;
let modelDownloadInProgress = false;
let modelDownloadLabel = "";
let modelDownloadStatus = "";
let modelDownloadFailed = false;
let dashboardLoadPromise = null;

const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => [...document.querySelectorAll(selector)];

async function request(url, options = {}) {
  options.headers = { ...uiHeader, ...(options.headers || {}) };
  if (options.body && typeof options.body !== "string") {
    options.headers["Content-Type"] = "application/json";
    options.body = JSON.stringify(options.body);
  }
  const response = await fetch(url, options);
  if (!response.ok) {
    const payload = await response.json().catch(() => ({}));
    throw new Error(payload.error || payload.detail || `${response.status} ${response.statusText}`);
  }
  return response.status === 204 ? null : response.json();
}

function showNotice(message, error = false) {
  const notice = $("#notice");
  notice.textContent = message;
  notice.classList.toggle("error", error);
  notice.classList.remove("hidden");
  clearTimeout(showNotice.timer);
  showNotice.timer = setTimeout(() => notice.classList.add("hidden"), 4500);
}

function formatDate(value) {
  return value ? new Date(value).toLocaleString() : "Not sent yet";
}

function formatTime(value) {
  return value ? value.slice(0, 5) : "";
}

function formatGb(value) {
  return value == null ? "—" : `${Number(value).toFixed(1)} GB`;
}

function formatDownloadSize(value) {
  if (value == null) return "size unknown";
  const gigabytes = Number(value);
  return gigabytes < 1
    ? `~${Math.max(1, Math.round(gigabytes * 1024))} MB`
    : `~${gigabytes.toFixed(1)} GB`;
}

function formatBytes(value) {
  const bytes = Number(value || 0);
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
}

async function loadDashboard() {
  if (dashboardLoadPromise) return dashboardLoadPromise;
  dashboardLoadPromise = (async () => {
    try {
      dashboard = await request("/local-api/dashboard");
      renderDashboard();
    } catch (error) {
      showNotice(error.message, true);
    } finally {
      dashboardLoadPromise = null;
    }
  })();
  return dashboardLoadPromise;
}

async function loadVersion() {
  try {
    const build = await request("/version");
    $("#build-version").textContent = `v${build.version} · build ${build.buildNumber}`;
  } catch {
    $("#build-version").textContent = "Version unavailable";
  }
}

function renderDashboard() {
  const runtime = dashboard.runtime || {};
  const schedule = dashboard.schedule;
  const settings = dashboard.settings;
  const phase = runtime.phase || "Starting";

  $("#worker-phase").textContent = phase;
  $("#worker-summary").textContent = dashboard.enrolled
    ? `${dashboard.workerName} is connected to worker ${dashboard.workerId}.`
    : "Waiting for first-time enrollment.";
  $("#active-assignment").textContent = runtime.assignmentId ? `#${runtime.assignmentId}` : "None";
  $("#active-request").textContent = runtime.requestId ? `Request #${runtime.requestId}` : "The worker is idle";
  $("#last-heartbeat").textContent = formatDate(runtime.lastHeartbeatAt);
  $("#worker-id").textContent = dashboard.enrolled ? `Worker #${dashboard.workerId}` : "Not enrolled";
  $("#availability-badge").textContent = schedule?.status || "Unavailable";

  const error = runtime.lastError || dashboard.apiError;
  $("#error-row").classList.toggle("hidden", !error);
  $("#last-error").textContent = error || "";

  const needsAccountRelink =
    !dashboard.credits.available &&
    String(dashboard.credits.message || "").includes("not linked to an account");
  $("#account-label").textContent = settings.emailAddress || "No account connected";
  $("#account-status").textContent = needsAccountRelink
    ? "Verified locally · relink required for token balance"
    : settings.emailAddress
      ? "Verified spin-up.ai account"
      : "A verified spin-up.ai account is required.";
  $("#account-link-controls").classList.toggle("hidden", Boolean(settings.emailAddress));
  $("#unlink-account").classList.toggle("hidden", !settings.emailAddress);
  $("#credit-balance").textContent = dashboard.credits.available
    ? Number(dashboard.credits.balance || 0).toLocaleString()
    : needsAccountRelink ? "Relink" : "—";
  $("#credit-note").textContent = needsAccountRelink
    ? "Open Account and link your API key once."
    : dashboard.credits.message;
  renderWorkerUpdate(dashboard.workerUpdate || {});

  $("#model-directory").value = settings.modelDirectory;
  $("#maximum-storage").value = settings.maximumStorageGb;
  $("#preserve-storage").value = settings.preserveFreeSpaceGb;
  $("#auto-remove").checked = settings.automaticallyRemoveUnusedModels;
  $("#start-with-windows").checked = Boolean(settings.startWithWindows);

  const used = Number(dashboard.storage.usedGb || 0);
  const limit = Number(settings.maximumStorageGb || 1);
  const percent = Math.min(100, used / limit * 100);
  $("#storage-used").textContent = formatGb(used);
  $("#storage-limit").textContent = formatGb(limit);
  $("#storage-percent").textContent = `${percent.toFixed(0)}%`;
  $("#storage-meter").style.width = `${percent}%`;

  renderActivityChart(runtime.activity || []);
  renderToday(schedule);
  renderModels();
  renderTestModels();
  if (!scheduleEditing) renderSchedule(schedule);
  renderExceptions(schedule);
  renderTermsAcceptance(settings);
}

function renderWorkerUpdate(update) {
  const title = $("#worker-update-title");
  const status = $("#worker-update-status");
  const notes = $("#worker-update-notes");
  const downloadButton = $("#download-worker-update");
  const cancelButton = $("#cancel-worker-update");
  const active = ["Downloading", "Verifying", "Cancelling"].includes(update.phase);

  if (update.phase === "Available") {
    title.textContent = `Version ${update.latestVersion} is available`;
    status.textContent =
      `Installed: ${update.currentVersion} · Download: ${formatBytes(update.sizeBytes)}`;
  } else if (update.phase === "Downloading") {
    title.textContent = `Downloading version ${update.latestVersion}`;
    const rate = update.bytesPerSecond
      ? ` at ${formatBytes(update.bytesPerSecond)}/s`
      : "";
    status.textContent =
      `${formatBytes(update.bytesDownloaded)} of ${formatBytes(update.sizeBytes)} ` +
      `(${update.percent || 0}%)${rate}`;
  } else if (update.phase === "Downloaded") {
    title.textContent = `Version ${update.latestVersion} is ready`;
    status.textContent = `Verified package saved to ${update.downloadPath}`;
  } else if (update.phase === "Failed" || update.phase === "Check failed") {
    title.textContent = "Update check failed";
    status.textContent = update.error || "The update service could not be reached.";
  } else {
    title.textContent = update.phase === "Up to date"
      ? `Version ${update.currentVersion} is up to date`
      : "Worker software updates";
    status.textContent = update.lastCheckedAt
      ? `Last checked ${formatDate(update.lastCheckedAt)}`
      : "The worker will check automatically.";
  }

  notes.textContent = update.releaseNotes || "";
  downloadButton.classList.toggle("hidden", !update.updateAvailable || active);
  cancelButton.classList.toggle("hidden", !update.canCancel);
  $("#check-worker-update").disabled = active;
}

function renderActivityChart(activity) {
  const chart = $("#activity-chart");
  if (!chart) return;

  const left = 32;
  const right = 784;
  const baseline = 190;
  const windowMs = 10 * 60 * 1000;
  const now = Date.now();
  const windowStart = now - windowMs;
  const events = activity
    .map(item => ({ ...item, timestamp: new Date(item.occurredAt).getTime() }))
    .filter(item => Number.isFinite(item.timestamp) && item.timestamp >= windowStart && item.timestamp <= now)
    .sort((a, b) => a.timestamp - b.timestamp);

  const xFor = timestamp =>
    left + ((timestamp - windowStart) / windowMs) * (right - left);
  const points = [[left, baseline]];

  for (const event of events) {
    const x = xFor(event.timestamp);
    const isCompleted = event.type === "completed";
    const peak = isCompleted ? 42 : 150;
    const halfWidth = isCompleted ? 7 : 4;
    points.push(
      [Math.max(left, x - halfWidth), baseline],
      [x, peak],
      [Math.min(right, x + halfWidth), baseline]
    );
  }
  points.push([right, baseline]);

  const line = points.map(([x, y], index) =>
    `${index === 0 ? "M" : "L"} ${x.toFixed(1)} ${y}`).join(" ");
  $("#activity-line").setAttribute("d", line);
  $("#activity-area").setAttribute("d", `${line} L ${right} ${baseline} L ${left} ${baseline} Z`);

  const markers = $("#activity-markers");
  markers.replaceChildren(...events.map(event => {
    const isCompleted = event.type === "completed";
    const marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    marker.setAttribute("cx", xFor(event.timestamp).toFixed(1));
    marker.setAttribute("cy", String(isCompleted ? 42 : 150));
    marker.setAttribute("r", String(isCompleted ? 6 : 4));
    marker.setAttribute("class", isCompleted
      ? "activity-marker-completed"
      : "activity-marker-heartbeat");
    const title = document.createElementNS("http://www.w3.org/2000/svg", "title");
    title.textContent = isCompleted
      ? `Request #${event.requestId} processed at ${new Date(event.timestamp).toLocaleTimeString()}`
      : `Heartbeat at ${new Date(event.timestamp).toLocaleTimeString()}`;
    marker.appendChild(title);
    return marker;
  }));

  const heartbeatCount = events.filter(event => event.type === "heartbeat").length;
  const completedCount = events.filter(event => event.type === "completed").length;
  const summary = events.length === 0
    ? "Waiting for worker activity."
    : `${heartbeatCount} heartbeat${heartbeatCount === 1 ? "" : "s"} and ` +
      `${completedCount} processed request${completedCount === 1 ? "" : "s"} in the last 10 minutes.`;
  $("#activity-chart-summary").textContent = summary;
  $("#activity-chart-description").textContent =
    `${summary} Small pulses show heartbeats and tall spikes show processed requests.`;
}

function renderTermsAcceptance(settings) {
  const accepted = settings.termsAcceptedVersion === currentTermsVersion &&
    Boolean(settings.termsAcceptedAt);
  $("#accept-terms").checked = accepted;
  $("#save-terms-acceptance").disabled = true;
  $("#terms-acceptance-status").textContent = accepted
    ? `Accepted on ${new Date(settings.termsAcceptedAt).toLocaleString()}. The worker may connect.`
    : "Acceptance is required before the worker will connect to spin-up.ai.";
}

function renderTestModels() {
  const models = dashboard.models.filter(model =>
    model.isInstalled && model.isConfigured && model.modelId > 0);
  const select = $("#test-model");
  const previous = select.value;
  select.innerHTML = models.length
    ? models.map(model =>
      `<option value="${model.modelId}">${escapeHtml(model.modelKey)}</option>`).join("")
    : `<option value="">No installed catalogue models</option>`;
  if (models.some(model => String(model.modelId) === previous))
    select.value = previous;
  select.disabled = models.length === 0;
  $("#run-model-test").disabled = models.length === 0;

  const remoteModels = dashboard.models.filter(model =>
    model.isConfigured && model.modelId > 0);
  const remoteSelect = $("#remote-test-model");
  const remotePrevious = remoteSelect.value;
  remoteSelect.innerHTML = remoteModels.length
    ? remoteModels.map(model =>
      `<option value="${model.modelId}">${escapeHtml(model.modelKey)}</option>`).join("")
    : `<option value="">No catalogue models available</option>`;
  if (remoteModels.some(model => String(model.modelId) === remotePrevious))
    remoteSelect.value = remotePrevious;
  remoteSelect.disabled = remoteModels.length === 0;
  $("#run-remote-test").disabled = remoteModels.length === 0;
}

function renderToday(schedule) {
  const container = $("#today-schedule");
  const today = new Date().getDay();
  const windows = (schedule?.windows || []).filter(window => window.dayOfWeek === today && window.isEnabled);
  container.innerHTML = windows.length
    ? windows.map(window => `<div class="summary-row"><span>${escapeHtml(window.scheduleName)}</span><strong>${formatTime(window.startLocalTime)}–${formatTime(window.endLocalTime)}</strong></div>`).join("")
    : `<p class="muted">No recurring availability window today.</p>`;
}

function renderModels() {
  const artifactDownload = dashboard.artifactDownload || {};
  const activeDownloadPhases = new Set([
    "Authorizing", "Checking storage", "Downloading", "Verifying", "Cancelling"
  ]);
  if (artifactDownload.modelId) {
    modelDownloadInProgress = activeDownloadPhases.has(artifactDownload.phase);
    modelDownloadLabel =
      artifactDownload.modelKey || artifactDownload.fileName || "the selected model";
    modelDownloadFailed = artifactDownload.phase === "Failed";
    if (artifactDownload.phase === "Downloading") {
      const rate = artifactDownload.bytesPerSecond
        ? ` at ${formatBytes(artifactDownload.bytesPerSecond)}/s`
        : "";
      modelDownloadStatus =
        `${artifactDownload.phase}: ${formatBytes(artifactDownload.bytesDownloaded)} of ` +
        `${formatBytes(artifactDownload.sizeBytes)} (${artifactDownload.percent || 0}%)${rate}`;
    } else if (artifactDownload.phase === "Failed") {
      modelDownloadStatus = `Download failed: ${artifactDownload.error || "Unknown error"}`;
    } else if (artifactDownload.phase !== "Idle") {
      modelDownloadStatus = `${artifactDownload.phase}: ${modelDownloadLabel}`;
    }
  }
  const selected = new Set(dashboard.settings.selectedModelIds || []);
  const allSelected = selected.size === 0;
  const hardware = dashboard.hardware || {};
  const gpuText = hardware.gpus?.length
    ? hardware.gpus.map(gpu => `${gpu.name} (${formatGb(gpu.vramGb)} VRAM)`).join(", ")
    : "No NVIDIA GPU detected; CPU compatibility was checked";
  $("#hardware-summary").innerHTML = `
    <div><p class="eyebrow">This PC</p><h2>${formatGb(hardware.totalSystemRamGb)} RAM · ${hardware.logicalProcessorCount || "?"} logical processors</h2></div>
    <p>${escapeHtml(gpuText)}</p>
    <small>Checked ${formatDate(hardware.checkedAt)}. Estimates assume a typical quantized local model; configured requirements take precedence.</small>`;
  const managedLlamaCpp = dashboard.managedRuntimes?.llamaCpp;
  const managedComfyUi = dashboard.managedRuntimes?.comfyUi;
  renderComfyUiSetupAssistant(hardware, managedComfyUi);
  $("#runtime-list").innerHTML = (dashboard.inferenceRuntimes || [])
    .map(runtime => {
      const detail = runtime === "llama.cpp" && managedLlamaCpp?.managed
        ? ` · managed ${managedLlamaCpp.phase.toLowerCase()}`
        : "";
      return `<span class="runtime-chip">${escapeHtml(runtime + detail)}</span>`;
    })
    .concat([`<span class="runtime-chip">ComfyUI · ${escapeHtml(managedComfyUi?.installed ? (managedComfyUi.phase || "stopped").toLowerCase() : "not installed")}</span>`])
    .join("") || `<span class="muted">No runtimes configured</span>`;
  $("#stop-llama-runtime").classList.toggle(
    "hidden",
    !managedLlamaCpp?.processId);
  $("#start-comfyui-runtime").classList.toggle(
    "hidden",
    !managedComfyUi?.installed || managedComfyUi?.phase === "Ready");
  $("#stop-comfyui-runtime").classList.toggle(
    "hidden",
    !managedComfyUi?.processId);
  const downloadable = dashboard.models.filter(model =>
    !model.isInstalled && model.isConfigured && model.modelId > 0 &&
    (model.runtimeName === "Ollama" || model.runtimeName === "llama.cpp"));
  const availableModels = $("#available-models");
  const previousModelId = availableModels.value;
  availableModels.innerHTML = downloadable.length
    ? downloadable.map(model => `
      <option value="${model.modelId}">
        ${escapeHtml(model.modelKey)} · ${formatDownloadSize(model.compatibility.estimatedSizeGb)} · ${escapeHtml(model.compatibility.status)}
      </option>`).join("")
    : `<option value="">No catalogue models available to download</option>`;
  if (downloadable.some(model => String(model.modelId) === previousModelId))
    availableModels.value = previousModelId;
  const selectedDownload = downloadable.find(model =>
    String(model.modelId) === availableModels.value);
  const selectedIsCompatible = selectedDownload &&
    (selectedDownload.compatibility.status === "recommended" ||
      selectedDownload.compatibility.status === "limited");
  availableModels.disabled = modelDownloadInProgress || downloadable.length === 0;
  const downloadButton = $("#download-model");
  downloadButton.disabled = modelDownloadInProgress || !selectedIsCompatible;
  $("#cancel-model-download").classList.toggle(
    "hidden",
    !artifactDownload.canCancel);
  downloadButton.textContent = modelDownloadInProgress ? "Downloading…" : "Download";
  $("#model-download-note").textContent = modelDownloadInProgress
    ? `Downloading ${modelDownloadLabel}. Partial downloads resume automatically.`
    : downloadable.length
      ? selectedDownload.compatibility.reason
      : "No configured Ollama or managed llama.cpp catalogue models are waiting to download.";
  const downloadStatus = $("#model-download-status");
  downloadStatus.textContent = modelDownloadStatus;
  downloadStatus.classList.toggle("hidden", !modelDownloadStatus);
  downloadStatus.classList.toggle("error", modelDownloadFailed);
  downloadStatus.classList.toggle(
    "working",
    modelDownloadInProgress && !modelDownloadFailed);
  $("#model-list").innerHTML = dashboard.models.filter(model => model.isInstalled).map(model => `
    <article class="model-card compatibility-${escapeHtml(model.compatibility.status)}">
      <header>
        <div><p class="eyebrow">${escapeHtml(model.runtimeName)}</p><h3>${escapeHtml(model.modelKey)}</h3></div>
        ${model.isConfigured
          ? `<input class="model-check" type="checkbox" data-model-id="${model.modelId}" ${allSelected || selected.has(model.modelId) ? "checked" : ""} ${model.compatibility.status === "ineligible" ? "disabled" : ""} aria-label="Enable ${escapeHtml(model.modelKey)}">`
          : `<span class="unmanaged-badge">Not in catalogue</span>`}
      </header>
      <span class="compatibility-badge">${escapeHtml(model.compatibility.status)}</span>
      <p>${escapeHtml(model.compatibility.reason)}</p>
      <p>Local runtime name: <strong>${escapeHtml(model.localModelName)}</strong></p>
      ${model.isConfigured ? `<p>Catalogue model ID: ${model.modelId}</p>` : ""}
    </article>`).join("") || `<p class="muted">No configured models are currently available in any runtime.</p>`;
}

function renderComfyUiSetupAssistant(hardware, managedComfyUi) {
  const assistant = $("#comfyui-setup-assistant");
  const installLink = $("#install-comfyui-link");
  if (managedComfyUi?.installed) {
    assistant.classList.add("hidden");
    return;
  }

  const bestGpu = [...(hardware.gpus || [])]
    .sort((left, right) => Number(right.vramGb || 0) - Number(left.vramGb || 0))[0];
  const vram = Number(bestGpu?.vramGb || 0);
  const ram = Number(hardware.totalSystemRamGb || 0);
  let status;
  let title;
  let message;

  if (vram >= 8 && ram >= 16) {
    status = "recommended";
    title = "This PC is ready for local image generation";
    message = `${bestGpu.name} has ${vram.toFixed(1)} GB VRAM and the PC has ${ram.toFixed(1)} GB RAM. ComfyUI with SDXL-class workflows should be practical.`;
  } else if (vram >= 4 && ram >= 12) {
    status = "limited";
    title = "This PC can run lighter image workflows";
    message = `${bestGpu.name} has ${vram.toFixed(1)} GB VRAM. Start with 512×512 images, smaller checkpoints, and ComfyUI low-VRAM options.`;
  } else {
    status = "ineligible";
    title = "Image generation is not recommended on this PC";
    message = bestGpu
      ? `${bestGpu.name} has ${vram.toFixed(1)} GB VRAM and the PC has ${ram.toFixed(1)} GB RAM. At least 4 GB VRAM and 12 GB system RAM is recommended for the starter workflow.`
      : "No supported NVIDIA GPU was detected. CPU-only image generation is possible but is too slow for rewarded distributed workloads.";
  }

  assistant.className = `panel comfyui-assistant ${status}`;
  $("#comfyui-capacity-title").textContent = title;
  $("#comfyui-capacity-message").textContent = message;
  $("#comfyui-capacity-detail").textContent =
    "The installation page opens on comfy.org. After installation, configure ComfyUi:WorkingDirectory if it is outside the worker folder.";
  installLink.classList.toggle("hidden", status === "ineligible");
}

function renderSchedule(schedule) {
  const list = $("#schedule-list");
  list.innerHTML = "";
  [...(schedule?.windows || [])]
    .sort((left, right) =>
      left.dayOfWeek - right.dayOfWeek ||
      formatTime(left.startLocalTime).localeCompare(formatTime(right.startLocalTime)))
    .forEach(addScheduleRow);
  if (!list.children.length) addScheduleRow();
}

function addScheduleRow(window = {}) {
  const fragment = $("#schedule-row-template").content.cloneNode(true);
  const row = fragment.querySelector(".schedule-row");
  const day = row.querySelector(".day");
  day.innerHTML = days.map((name, index) => `<option value="${index}">${name}</option>`).join("");
  day.value = window.dayOfWeek ?? new Date().getDay();
  row.querySelector(".start").value = formatTime(window.startLocalTime) || "18:00";
  row.querySelector(".end").value = formatTime(window.endLocalTime) || "23:00";
  const timezone = row.querySelector(".timezone");
  const selectedTimeZone = window.timeZoneId ||
    Intl.DateTimeFormat().resolvedOptions().timeZone ||
    "Africa/Johannesburg";
  const availableTimeZones = timeZones.includes(selectedTimeZone)
    ? timeZones
    : [selectedTimeZone, ...timeZones];
  timezone.innerHTML = availableTimeZones
    .map(zone => `<option value="${escapeHtml(zone)}">${escapeHtml(zone.replaceAll("_", " "))}</option>`)
    .join("");
  timezone.value = selectedTimeZone;
  row.querySelector(".remove").addEventListener("click", () => {
    scheduleEditing = true;
    row.remove();
  });
  $("#schedule-list").appendChild(fragment);
}

function renderExceptions(schedule) {
  const list = $("#exception-list");
  const exceptions = schedule?.exceptions || [];
  list.innerHTML = exceptions.length
    ? exceptions.map(item => `
      <div class="exception-item">
        <div><strong>${new Date(item.exceptionDate).toLocaleDateString()}</strong>
          <small>${item.isAvailable ? `${formatTime(item.startLocalTime)}–${formatTime(item.endLocalTime)}` : "Unavailable all day"}${item.reason ? ` · ${escapeHtml(item.reason)}` : ""}</small>
        </div>
        <button class="icon-button delete-exception" data-id="${item.workerScheduleExceptionId}" aria-label="Delete exception">×</button>
      </div>`).join("")
    : `<p class="muted">No upcoming exceptions.</p>`;
  $$(".delete-exception").forEach(button => button.addEventListener("click", async () => {
    try {
      await request(`/local-api/schedule/exceptions/${button.dataset.id}`, { method: "DELETE" });
      showNotice("Exception removed.");
      await loadDashboard();
    } catch (error) { showNotice(error.message, true); }
  }));
}

async function saveSettings() {
  const selectedIds = $$(".model-check:checked").map(item => Number(item.dataset.modelId));
  const payload = {
    emailAddress: dashboard.settings.emailAddress || null,
    modelDirectory: $("#model-directory").value.trim(),
    maximumStorageGb: Number($("#maximum-storage").value),
    preserveFreeSpaceGb: Number($("#preserve-storage").value),
    automaticallyRemoveUnusedModels: $("#auto-remove").checked,
    startWithWindows: $("#start-with-windows").checked,
    selectedModelIds: selectedIds
  };
  dashboard.settings = await request("/local-api/settings", { method: "PUT", body: payload });
  showNotice("Local settings saved.");
  renderDashboard();
}

async function linkAccount() {
  const apiKeyInput = $("#account-api-key");
  const apiKey = apiKeyInput.value.trim();
  if (!apiKey) {
    showNotice("Enter an active spin-up.ai API key.", true);
    return;
  }

  const button = $("#save-account");
  button.disabled = true;
  button.textContent = "Verifying...";
  try {
    dashboard.settings = await request("/local-api/account/link", {
      method: "POST",
      body: { apiKey }
    });
    apiKeyInput.value = "";
    await loadDashboard();
    showNotice(`Linked to ${dashboard.settings.emailAddress}.`);
  } catch (error) {
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Link account";
  }
}

async function unlinkAccount() {
  if (!window.confirm("Unlink this account from the worker on this device?")) return;

  const button = $("#unlink-account");
  button.disabled = true;
  button.textContent = "Unlinking...";
  try {
    dashboard.settings = await request("/local-api/account/link", { method: "DELETE" });
    $("#account-api-key").value = "";
    await loadDashboard();
    showNotice("The locally linked account data was cleared.");
  } catch (error) {
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Unlink account";
  }
}

async function saveSchedule() {
  const windows = $$(".schedule-row").map((row, index) => ({
    scheduleName: `${days[Number(row.querySelector(".day").value)]} availability`,
    timeZoneId: row.querySelector(".timezone").value.trim(),
    dayOfWeek: Number(row.querySelector(".day").value),
    startLocalTime: `${row.querySelector(".start").value}:00`,
    endLocalTime: `${row.querySelector(".end").value}:00`,
    isEnabled: true,
    priority: index,
    validFrom: null,
    validUntil: null
  }));
  await request("/local-api/schedule", { method: "PUT", body: { windows } });
  scheduleEditing = false;
  showNotice("Availability schedule saved.");
  await loadDashboard();
}

function escapeHtml(value) {
  return String(value ?? "").replace(/[&<>"']/g, character => ({
    "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
  })[character]);
}

$$(".nav-item").forEach(button => button.addEventListener("click", () => {
  $$(".nav-item").forEach(item => item.classList.remove("active"));
  $$(".view").forEach(view => view.classList.remove("active"));
  button.classList.add("active");
  $(`#view-${button.dataset.view}`).classList.add("active");
  $("#page-title").textContent = button.textContent;
}));

$$("[data-worker-action]").forEach(button => button.addEventListener("click", async () => {
  try {
    await request(`/local-api/worker/${button.dataset.workerAction}`, { method: "POST" });
    showNotice(`Worker command sent: ${button.dataset.workerAction}.`);
    await loadDashboard();
  } catch (error) { showNotice(error.message, true); }
}));

$("#refresh").addEventListener("click", loadDashboard);
$("#add-window").addEventListener("click", () => {
  scheduleEditing = true;
  addScheduleRow();
});
$("#schedule-list").addEventListener("input", () => {
  scheduleEditing = true;
});
$("#schedule-list").addEventListener("change", () => {
  scheduleEditing = true;
});
$("#save-schedule").addEventListener("click", () => saveSchedule().catch(error => showNotice(error.message, true)));
$("#save-account").addEventListener("click", linkAccount);
$("#unlink-account").addEventListener("click", unlinkAccount);
$("#save-storage").addEventListener("click", () => saveSettings().catch(error => showNotice(error.message, true)));
$("#save-models").addEventListener("click", () => saveSettings().catch(error => showNotice(error.message, true)));
$("#check-hardware").addEventListener("click", async () => {
  showNotice("Checking local hardware…");
  await loadDashboard();
  showNotice("Hardware compatibility refreshed.");
});
$("#available-models").addEventListener("change", event => {
  renderModels();
});

$("#stop-llama-runtime").addEventListener("click", async () => {
  try {
    await request("/local-api/runtimes/llama.cpp/stop", { method: "POST" });
    showNotice("Managed llama.cpp stopped. It will restart on the next request.");
    await loadDashboard();
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#start-comfyui-runtime").addEventListener("click", async () => {
  try {
    showNotice("Starting ComfyUI…");
    await request("/local-api/runtimes/comfyui/start", { method: "POST" });
    await loadDashboard();
    showNotice("ComfyUI is ready.");
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#stop-comfyui-runtime").addEventListener("click", async () => {
  try {
    await request("/local-api/runtimes/comfyui/stop", { method: "POST" });
    await loadDashboard();
    showNotice("Managed ComfyUI stopped.");
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#download-model").addEventListener("click", async () => {
  const modelId = Number($("#available-models").value);
  if (!modelId) return;
  const model = dashboard.models.find(item => item.modelId === modelId);
  modelDownloadInProgress = true;
  modelDownloadLabel = model?.localModelName || "the selected model";
  modelDownloadStatus = `Starting ${modelDownloadLabel} download…`;
  modelDownloadFailed = false;
  renderModels();
  showNotice(`Downloading ${model?.localModelName || "model"}…`);
  try {
    await request(`/local-api/models/${modelId}/download`, { method: "POST" });
    if (model?.runtimeName === "llama.cpp") {
      modelDownloadStatus = `Download started: ${modelDownloadLabel}`;
      showNotice(`${model?.modelKey || "Model"} download started.`);
      await loadDashboard();
      return;
    }
    modelDownloadInProgress = false;
    modelDownloadLabel = "";
    modelDownloadStatus = `${model?.localModelName || "Model"} downloaded and installed successfully.`;
    modelDownloadFailed = false;
    showNotice(`${model?.modelKey || "Model"} installed.`);
    await loadDashboard();
  } catch (error) {
    modelDownloadInProgress = false;
    modelDownloadLabel = "";
    modelDownloadStatus = `Download failed: ${error.message}`;
    modelDownloadFailed = true;
    renderModels();
    showNotice(error.message, true);
  }
});
$("#cancel-model-download").addEventListener("click", async () => {
  try {
    await request("/local-api/downloads/current/cancel", { method: "POST" });
    showNotice("Cancelling model download…");
    await loadDashboard();
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#check-worker-update").addEventListener("click", async () => {
  const button = $("#check-worker-update");
  button.disabled = true;
  try {
    await request("/local-api/updates/check", { method: "POST" });
    await loadDashboard();
  } catch (error) {
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
  }
});
$("#download-worker-update").addEventListener("click", async () => {
  try {
    await request("/local-api/updates/download", { method: "POST" });
    showNotice("Worker update download started.");
    await loadDashboard();
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#cancel-worker-update").addEventListener("click", async () => {
  try {
    await request("/local-api/updates/download/cancel", { method: "POST" });
    showNotice("Cancelling worker update download…");
    await loadDashboard();
  } catch (error) {
    showNotice(error.message, true);
  }
});
$("#test-model-form").addEventListener("submit", async event => {
  event.preventDefault();
  const modelId = Number($("#test-model").value);
  const prompt = $("#test-prompt").value.trim();
  if (!modelId || !prompt) return;

  const button = $("#run-model-test");
  button.disabled = true;
  button.textContent = "Running…";
  const selectedModel = dashboard.models.find(item => item.modelId === modelId);
  $("#test-status").textContent =
    `Waiting for ${selectedModel?.runtimeName || "the model runtime"}…`;
  $("#test-result-panel").classList.add("hidden");
  try {
    const result = await request(`/local-api/models/${modelId}/test`, {
      method: "POST",
      body: { prompt }
    });
    $("#test-result-model").textContent = result.modelKey;
    $("#test-elapsed").textContent = `${(Number(result.elapsedMilliseconds) / 1000).toFixed(1)} seconds`;
    $("#test-result").textContent = result.response;
    $("#test-result-panel").classList.remove("hidden");
    $("#test-status").textContent = "Completed.";
  } catch (error) {
    $("#test-status").textContent = `Failed: ${error.message}`;
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Run test";
  }
});

$("#test-image-form").addEventListener("submit", async event => {
  event.preventDefault();
  const prompt = $("#test-image-prompt").value.trim();
  if (!prompt) return;
  const seedText = $("#test-image-seed").value.trim();
  const button = $("#run-image-test");
  button.disabled = true;
  button.textContent = "Generating…";
  $("#test-image-status").textContent = "Waiting for ComfyUI…";
  $("#test-image-result-panel").classList.add("hidden");
  try {
    const result = await request("/local-api/images/test", {
      method: "POST",
      body: {
        prompt,
        negativePrompt: $("#test-image-negative").value.trim() || null,
        width: Number($("#test-image-width").value),
        height: Number($("#test-image-height").value),
        steps: Number($("#test-image-steps").value),
        seed: seedText ? Number(seedText) : null
      }
    });
    $("#test-image-results").innerHTML = (result.images || [])
      .map(image => `<img src="${image.dataUrl}" alt="Generated image">`)
      .join("");
    $("#test-image-elapsed").textContent =
      `${(Number(result.durationMs || 0) / 1000).toFixed(1)}s · seed ${result.seed}`;
    $("#test-image-result-panel").classList.remove("hidden");
    $("#test-image-status").textContent = "Completed locally.";
  } catch (error) {
    $("#test-image-status").textContent = error.message;
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Generate image";
  }
});

$("#test-audio-form").addEventListener("submit", async event => {
  event.preventDefault();
  const prompt = $("#test-audio-prompt").value.trim();
  if (!prompt) return;
  const seedText = $("#test-audio-seed").value.trim();
  const button = $("#run-audio-test");
  button.disabled = true;
  button.textContent = "Generating…";
  $("#test-audio-status").textContent = "Waiting for ComfyUI…";
  $("#test-audio-result-panel").classList.add("hidden");
  try {
    const result = await request("/local-api/audio/test", {
      method: "POST",
      body: {
        prompt,
        negativePrompt: $("#test-audio-negative").value.trim() || null,
        durationSeconds: Number($("#test-audio-duration").value),
        seed: seedText ? Number(seedText) : null
      }
    });
    $("#test-audio-results").innerHTML = (result.audio || [])
      .map(file => `
        <article class="generated-audio-item">
          <audio controls preload="metadata" src="${file.dataUrl}"></audio>
          <a class="button ghost" href="${file.dataUrl}" download="${escapeHtml(file.fileName || "generated-audio.wav")}">Download</a>
        </article>`)
      .join("");
    $("#test-audio-elapsed").textContent =
      `${(Number(result.durationMs || 0) / 1000).toFixed(1)}s · seed ${result.seed}`;
    $("#test-audio-result-panel").classList.remove("hidden");
    $("#test-audio-status").textContent = "Completed locally.";
  } catch (error) {
    $("#test-audio-status").textContent = error.message;
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Generate audio";
  }
});

const waitForRemoteTest = milliseconds =>
  new Promise(resolve => setTimeout(resolve, milliseconds));

function readQueuedResponse(response, responseJson) {
  if (response != null) {
    if (typeof response === "string") return response;
    return response.content ?? JSON.stringify(response, null, 2);
  }
  if (!responseJson) return "The request completed without a response body.";
  try {
    const payload = JSON.parse(responseJson);
    return payload.content ?? JSON.stringify(payload, null, 2);
  } catch {
    return responseJson;
  }
}

$("#remote-test-form").addEventListener("submit", async event => {
  event.preventDefault();
  const modelId = Number($("#remote-test-model").value);
  const prompt = $("#remote-test-prompt").value.trim();
  if (!modelId || !prompt) return;

  const model = dashboard.models.find(item => item.modelId === modelId);
  const button = $("#run-remote-test");
  button.disabled = true;
  button.textContent = "Submitting…";
  $("#remote-test-result-panel").classList.add("hidden");
  try {
    let result = await request("/local-api/remote-tests", {
      method: "POST",
      body: { modelId, prompt }
    });
    const requestId = result.requestId;
    button.textContent = "Waiting…";
    $("#remote-test-status").textContent = `Request #${requestId}: queued`;

    const deadline = Date.now() + 10 * 60 * 1000;
    while (!["Completed", "Failed", "Cancelled", "Expired"].includes(result.status)) {
      if (Date.now() >= deadline)
        throw new Error(`Request #${requestId} is still ${result.status}. You can check it again later.`);
      await waitForRemoteTest(2000);
      result = await request(`/local-api/remote-tests/${requestId}`);
      $("#remote-test-status").textContent =
        `Request #${requestId}: ${result.status.toLowerCase()} · attempt ${result.attemptCount}/${result.maximumAttempts}`;
    }

    if (result.status !== "Completed")
      throw new Error(`Request #${requestId} ended with status ${result.status}.`);

    $("#remote-test-result-model").textContent =
      `${model?.modelKey || `Model ${modelId}`} · Request #${requestId}`;
    $("#remote-test-elapsed").textContent = result.inferenceDurationMs == null
      ? "Completed"
      : `${(Number(result.inferenceDurationMs) / 1000).toFixed(1)} seconds inference`;
    $("#remote-test-result").textContent =
      readQueuedResponse(result.response, result.responseJson);
    $("#remote-test-result-panel").classList.remove("hidden");
    $("#remote-test-status").textContent = `Request #${requestId}: completed`;
  } catch (error) {
    $("#remote-test-status").textContent = `Failed: ${error.message}`;
    showNotice(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = "Queue test";
  }
});
$("#accept-terms").addEventListener("change", event => {
  $("#save-terms-acceptance").disabled = !event.target.checked;
});
$("#save-terms-acceptance").addEventListener("click", async () => {
  const button = $("#save-terms-acceptance");
  button.disabled = true;
  try {
    dashboard.settings = await request("/local-api/terms/accept", {
      method: "POST",
      body: { accepted: true, version: currentTermsVersion }
    });
    renderTermsAcceptance(dashboard.settings);
    showNotice("Terms accepted. The worker may now connect.");
    await loadDashboard();
  } catch (error) {
    button.disabled = false;
    showNotice(error.message, true);
  }
});
$("#exception-form").addEventListener("submit", async event => {
  event.preventDefault();
  const available = $("#exception-type").value === "true";
  try {
    await request("/local-api/schedule/exceptions", {
      method: "POST",
      body: {
        exceptionDate: $("#exception-date").value,
        isAvailable: available,
        startLocalTime: available ? `${$("#exception-start").value}:00` : null,
        endLocalTime: available ? `${$("#exception-end").value}:00` : null,
        reason: $("#exception-reason").value.trim() || null
      }
    });
    event.target.reset();
    showNotice("Schedule exception added.");
    await loadDashboard();
  } catch (error) { showNotice(error.message, true); }
});

$("#view-dashboard").prepend($(".activity-chart-panel"));
loadDashboard();
loadVersion();
setInterval(() => void loadDashboard(), 5000);
