const tokenInput = document.getElementById("token");
const saveButton = document.getElementById("saveToken");
const statusEl = document.getElementById("status");
const serverModeEl = document.getElementById("serverMode");
const uptimeEl = document.getElementById("uptime");
const runningEl = document.getElementById("running");
const readyEl = document.getElementById("ready");
const configConvergedEl = document.getElementById("configConverged");
const configStatusSummaryEl = document.getElementById("configStatusSummary");
const configRuntimeBadgeEl = document.getElementById("configRuntimeBadge");
const configRuntimeDetailEl = document.getElementById("configRuntimeDetail");
const configRestartBadgeEl = document.getElementById("configRestartBadge");
const configRestartDetailEl = document.getElementById("configRestartDetail");
const configControlPlaneBadgeEl = document.getElementById("configControlPlaneBadge");
const configControlPlaneDetailEl = document.getElementById("configControlPlaneDetail");
const configLastActionEl = document.getElementById("configLastAction");
const configLastSavedEl = document.getElementById("configLastSaved");
const configLastAppliedEl = document.getElementById("configLastApplied");
const configPathEl = document.getElementById("configPath");
const configDirEl = document.getElementById("configDir");
const serverConfigFileEl = document.getElementById("serverConfigFile");
const statsDbEl = document.getElementById("statsDb");
const rnsdBinEl = document.getElementById("rnsdBin");
const sentineldBinEl = document.getElementById("sentineldBin");
const statsdBinEl = document.getElementById("statsdBin");
const httpBindEl = document.getElementById("httpBind");
const httpAuthEl = document.getElementById("httpAuth");
const launchPlanRowsEl = document.getElementById("launchPlanRows");
const configCandidateEl = document.getElementById("configCandidate");
const builderStatsDbPathEl = document.getElementById("builderStatsDbPath");
const builderRnsdBinEl = document.getElementById("builderRnsdBin");
const builderSentineldBinEl = document.getElementById("builderSentineldBin");
const builderStatsdBinEl = document.getElementById("builderStatsdBin");
const builderHttpEnabledEl = document.getElementById("builderHttpEnabled");
const builderHttpHostEl = document.getElementById("builderHttpHost");
const builderHttpPortEl = document.getElementById("builderHttpPort");
const builderHttpDisableAuthEl = document.getElementById("builderHttpDisableAuth");
const builderHttpAuthTokenEl = document.getElementById("builderHttpAuthToken");
const loadCurrentConfigButton = document.getElementById("loadCurrentConfig");
const loadExampleConfigButton = document.getElementById("loadExampleConfig");
const syncBuilderFromJsonButton = document.getElementById("syncBuilderFromJson");
const syncJsonFromBuilderButton = document.getElementById("syncJsonFromBuilder");
const formatConfigButton = document.getElementById("formatConfig");
const validateConfigButton = document.getElementById("validateConfig");
const saveConfigButton = document.getElementById("saveConfig");
const applyConfigButton = document.getElementById("applyConfig");
const configValidationStatusEl = document.getElementById("configValidationStatus");
const builderDirtyStateEl = document.getElementById("builderDirtyState");
const configActionSummaryEl = document.getElementById("configActionSummary");
const configWarningListEl = document.getElementById("configWarningList");
const configPlanSummaryEl = document.getElementById("configPlanSummary");
const configPlanActionEl = document.getElementById("configPlanAction");
const configPlanImpactEl = document.getElementById("configPlanImpact");
const configPlanTargetsEl = document.getElementById("configPlanTargets");
const configPlanChangeCountEl = document.getElementById("configPlanChangeCount");
const configChangeRowsEl = document.getElementById("configChangeRows");
const configSchemaNotesEl = document.getElementById("configSchemaNotes");
const configSchemaRowsEl = document.getElementById("configSchemaRows");
const configValidationResultEl = document.getElementById("configValidationResult");
const processRowsEl = document.getElementById("processRows");
const processEventRowsEl = document.getElementById("processEventRows");
const selectedProcessNameEl = document.getElementById("selectedProcessName");
const selectedProcessSummaryEl = document.getElementById("selectedProcessSummary");
const selectedProcessStatusBadgeEl = document.getElementById("selectedProcessStatusBadge");
const selectedProcessReadyEl = document.getElementById("selectedProcessReady");
const selectedProcessReadyDetailEl = document.getElementById("selectedProcessReadyDetail");
const selectedProcessRestartsEl = document.getElementById("selectedProcessRestarts");
const selectedProcessTransitionEl = document.getElementById("selectedProcessTransition");
const selectedProcessLastExitEl = document.getElementById("selectedProcessLastExit");
const selectedProcessLastErrorEl = document.getElementById("selectedProcessLastError");
const selectedProcessLogCountEl = document.getElementById("selectedProcessLogCount");
const selectedProcessLogMetaEl = document.getElementById("selectedProcessLogMeta");
const selectedProcessEventRowsEl = document.getElementById("selectedProcessEventRows");
const logProcessNameEl = document.getElementById("logProcessName");
const logStatusEl = document.getElementById("logStatus");
const processLogOutputEl = document.getElementById("processLogOutput");
const toggleAdvancedConfigButton = document.getElementById("toggleAdvancedConfig");
const advancedConfigSectionEl = document.getElementById("advancedConfigSection");
let configEditorDirty = false;
let configBuilderDirty = false;
let advancedConfigVisible = false;
let selectedProcess = null;
let selectedLogProcess = null;
let currentConfigJson = "";
let schemaExampleJson = "";
let latestProcesses = [];
let latestProcessEvents = [];

const params = new URLSearchParams(window.location.search);
const initialToken = params.get("token") || localStorage.getItem("rnsctl_token") || "";
tokenInput.value = initialToken;

function fmtSeconds(value) {
  if (value == null) return "-";
  const total = Math.floor(value);
  const hours = Math.floor(total / 3600);
  const minutes = Math.floor((total % 3600) / 60);
  const seconds = total % 60;
  return `${hours}h ${minutes}m ${seconds}s`;
}

function fmtAge(value) {
  if (value == null) return "-";
  if (value < 1) return "<1s ago";
  return `${fmtSeconds(value)} ago`;
}

function setBadge(el, label, className) {
  el.textContent = label;
  el.className = `pill ${className}`;
}

function authHeaders() {
  const token = tokenInput.value.trim();
  if (!token) return {};
  return { Authorization: `Bearer ${token}` };
}

async function fetchJson(path) {
  const response = await fetch(path, { headers: authHeaders() });
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${response.statusText}: ${body}`);
  }
  return response.json();
}

async function postJson(path) {
  const response = await fetch(path, { method: "POST", headers: authHeaders() });
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`${response.status} ${response.statusText}: ${body}`);
  }
  return response.json();
}

function renderProcesses(processes) {
  latestProcesses = processes;
  processRowsEl.innerHTML = "";
  for (const process of processes) {
    const tr = document.createElement("tr");
    const statusClass = process.status === "running" ? "running" : (process.status || "stopped");
    const healthSummary = [
      process.status_detail ?? process.last_error ?? "",
      process.last_log_age_seconds != null ? `last log ${fmtAge(process.last_log_age_seconds)}` : "",
      process.durable_log_path ? `file ${process.durable_log_path}` : "",
    ].filter(Boolean).join(" | ");
    tr.innerHTML = `
      <td>${process.name}</td>
      <td><span class="pill ${statusClass}">${process.status}</span></td>
      <td>${process.ready ? "yes" : (process.ready_state ?? "no")}</td>
      <td>${process.pid ?? "-"}</td>
      <td>${fmtSeconds(process.uptime_seconds)}</td>
      <td>${fmtSeconds(process.last_transition_seconds)}</td>
      <td>${process.last_exit_code ?? "-"}</td>
      <td>${healthSummary}</td>
      <td>
        <button class="secondary" data-select="${process.name}">Inspect</button>
        <button class="secondary" data-start="${process.name}">Start</button>
        <button class="secondary" data-stop="${process.name}">Stop</button>
        <button class="secondary" data-restart="${process.name}">Restart</button>
      </td>
      <td><button class="secondary" data-logs="${process.name}">View Logs</button></td>
    `;
    if (process.name === selectedProcess) {
      tr.classList.add("process-row-selected");
    }
    processRowsEl.appendChild(tr);
  }

  if (!selectedProcess && processes.length > 0) {
    selectedProcess = processes[0].name;
  }
  if (!selectedLogProcess && processes.length > 0) {
    selectedLogProcess = selectedProcess || processes[0].name;
  }
  if (selectedProcess && !processes.some((process) => process.name === selectedProcess)) {
    selectedProcess = processes[0]?.name || null;
  }
  if (selectedLogProcess && !processes.some((process) => process.name === selectedLogProcess)) {
    selectedLogProcess = selectedProcess;
  }

  for (const button of processRowsEl.querySelectorAll("[data-select]")) {
    button.addEventListener("click", () => {
      selectedProcess = button.getAttribute("data-select");
      renderProcesses(latestProcesses);
      renderSelectedProcessDetail();
    });
  }
  for (const button of processRowsEl.querySelectorAll("[data-restart]")) {
    button.addEventListener("click", async () => {
      const name = button.getAttribute("data-restart");
      statusEl.textContent = `Restarting ${name}...`;
      try {
        await postJson(`/api/processes/${name}/restart`);
        statusEl.textContent = `Restart queued for ${name}`;
        refresh();
      } catch (error) {
        statusEl.textContent = error.message;
      }
    });
  }
  for (const button of processRowsEl.querySelectorAll("[data-start]")) {
    button.addEventListener("click", async () => {
      const name = button.getAttribute("data-start");
      statusEl.textContent = `Starting ${name}...`;
      try {
        await postJson(`/api/processes/${name}/start`);
        statusEl.textContent = `Start queued for ${name}`;
        refresh();
      } catch (error) {
        statusEl.textContent = error.message;
      }
    });
  }
  for (const button of processRowsEl.querySelectorAll("[data-stop]")) {
    button.addEventListener("click", async () => {
      const name = button.getAttribute("data-stop");
      statusEl.textContent = `Stopping ${name}...`;
      try {
        await postJson(`/api/processes/${name}/stop`);
        statusEl.textContent = `Stop queued for ${name}`;
        refresh();
      } catch (error) {
        statusEl.textContent = error.message;
      }
    });
  }
  for (const button of processRowsEl.querySelectorAll("[data-logs]")) {
    button.addEventListener("click", async () => {
      selectedLogProcess = button.getAttribute("data-logs");
      selectedProcess = selectedLogProcess;
      renderProcesses(latestProcesses);
      renderSelectedProcessDetail();
      await refreshLogs();
    });
  }
}

function renderProcessEvents(events) {
  latestProcessEvents = events;
  processEventRowsEl.innerHTML = "";
  for (const event of events) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${event.process}</td>
      <td>${event.event}</td>
      <td>${fmtSeconds(event.age_seconds)}</td>
      <td>${event.detail ?? ""}</td>
    `;
    processEventRowsEl.appendChild(tr);
  }
  renderSelectedProcessDetail();
}

function selectedProcessRecord() {
  return latestProcesses.find((process) => process.name === selectedProcess) || null;
}

function renderSelectedProcessDetail() {
  const process = selectedProcessRecord();
  selectedProcessEventRowsEl.innerHTML = "";
  if (!process) {
    selectedProcessNameEl.textContent = "No process selected";
    selectedProcessSummaryEl.textContent = "Select a process to inspect its runtime detail, recent events, and logs.";
    setBadge(selectedProcessStatusBadgeEl, "idle", "info");
    selectedProcessReadyEl.textContent = "-";
    selectedProcessReadyDetailEl.textContent = "No process selected.";
    selectedProcessRestartsEl.textContent = "0";
    selectedProcessTransitionEl.textContent = "No transition data yet.";
    selectedProcessLastExitEl.textContent = "-";
    selectedProcessLastErrorEl.textContent = "No recorded process error.";
    selectedProcessLogCountEl.textContent = "0";
    selectedProcessLogMetaEl.textContent = "No log metadata yet.";
    const tr = document.createElement("tr");
    tr.innerHTML = `<td colspan="3" class="muted">No process selected.</td>`;
    selectedProcessEventRowsEl.appendChild(tr);
    return;
  }

  selectedProcessNameEl.textContent = process.name;
  selectedProcessSummaryEl.textContent = [
    `PID ${process.pid ?? "-"}`,
    `uptime ${fmtSeconds(process.uptime_seconds)}`,
    process.status_detail || process.last_error || "No extra runtime detail",
  ].join(" | ");
  setBadge(
    selectedProcessStatusBadgeEl,
    process.status || "unknown",
    process.status === "running" ? "running" : (process.status || "stopped"),
  );
  selectedProcessReadyEl.textContent = process.ready ? "ready" : (process.ready_state || "not-ready");
  selectedProcessReadyDetailEl.textContent = process.status_detail || process.last_error || "No readiness detail recorded.";
  selectedProcessRestartsEl.textContent = String(process.restart_count ?? 0);
  selectedProcessTransitionEl.textContent = `Last transition ${fmtAge(process.last_transition_seconds)}.`;
  selectedProcessLastExitEl.textContent = process.last_exit_code != null ? String(process.last_exit_code) : "-";
  selectedProcessLastErrorEl.textContent = process.last_error || "No recorded process error.";
  selectedProcessLogCountEl.textContent = String(process.recent_log_lines ?? 0);
  selectedProcessLogMetaEl.textContent = [
    process.last_log_age_seconds != null ? `Last log ${fmtAge(process.last_log_age_seconds)}` : "",
    process.durable_log_path ? `File ${process.durable_log_path}` : "",
  ].filter(Boolean).join(" | ") || "No log metadata yet.";

  const events = latestProcessEvents.filter((event) => event.process === process.name).slice(0, 6);
  if (!events.length) {
    const tr = document.createElement("tr");
    tr.innerHTML = `<td colspan="3" class="muted">No recent lifecycle events for ${process.name}.</td>`;
    selectedProcessEventRowsEl.appendChild(tr);
    return;
  }

  for (const event of events) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${event.event}</td>
      <td>${fmtAge(event.age_seconds)}</td>
      <td>${event.detail ?? ""}</td>
    `;
    selectedProcessEventRowsEl.appendChild(tr);
  }
}

function renderConfig(config) {
  configPathEl.textContent = config?.config_path ?? "(default)";
  configDirEl.textContent = config?.resolved_config_dir ?? "-";
  serverConfigFileEl.textContent = config?.server_config_file_path
    ? `${config.server_config_file_path}${config.server_config_file_present ? "" : " (not present)"}`
    : "-";
  statsDbEl.textContent = config?.stats_db_path ?? "-";
  rnsdBinEl.textContent = config?.rnsd_bin ?? "-";
  sentineldBinEl.textContent = config?.sentineld_bin ?? "-";
  statsdBinEl.textContent = config?.statsd_bin ?? "-";

  if (config?.http?.enabled) {
    httpBindEl.textContent = `${config.http.host}:${config.http.port}`;
    const tokenMode = config.http.token_configured ? "token set" : "token generated at startup";
    httpAuthEl.textContent = `${config.http.auth_mode}, ${tokenMode}, daemon=${config.http.daemon_mode ? "yes" : "no"}`;
  } else {
    httpBindEl.textContent = "disabled";
    httpAuthEl.textContent = "disabled";
  }

  launchPlanRowsEl.innerHTML = "";
  for (const process of config?.launch_plan || []) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${process.name}</td>
      <td>${process.bin}</td>
      <td>${process.args && process.args.length ? process.args.join(" ") : "-"}</td>
    `;
    launchPlanRowsEl.appendChild(tr);
  }

  if (!configEditorDirty) {
    configCandidateEl.value = config?.server_config_file_json ?? "";
  }
  if (!configBuilderDirty) {
    populateBuilder(configFromSnapshot(config));
  }
  currentConfigJson = config?.server_config_file_json ?? "";
  updateBuilderDirtyState();
}

function renderConfigStatus(status) {
  if (!status) {
    configConvergedEl.textContent = "-";
    configStatusSummaryEl.textContent = "No config status yet.";
    setBadge(configRuntimeBadgeEl, "unknown", "info");
    setBadge(configRestartBadgeEl, "unknown", "info");
    setBadge(configControlPlaneBadgeEl, "unknown", "info");
    configRuntimeDetailEl.textContent = "No config status yet.";
    configRestartDetailEl.textContent = "No process restart information yet.";
    configControlPlaneDetailEl.textContent = "No control-plane restart information yet.";
    configLastActionEl.textContent = "-";
    configLastSavedEl.textContent = "-";
    configLastAppliedEl.textContent = "-";
    return;
  }

  configConvergedEl.textContent = status.converged ? "yes" : "no";
  const pending = status.pending_process_restarts?.length
    ? ` Pending restarts: ${status.pending_process_restarts.join(", ")}.`
    : "";
  const action = status.last_action
    ? ` Last action: ${status.last_action}.`
    : "";
  const pendingAction = status.pending_action
    ? ` Pending action: ${status.pending_action}.`
    : "";
  const pendingTargets = status.pending_targets?.length
    ? ` Targets: ${status.pending_targets.join(", ")}.`
    : "";
  configStatusSummaryEl.textContent = `${status.summary}${action}${pendingAction}${pendingTargets}${pending}`;

  if (status.runtime_differs_from_saved) {
    setBadge(configRuntimeBadgeEl, "drifted", "warn");
    configRuntimeDetailEl.textContent = status.blocking_reason || "Saved config is not fully active in the current runtime state.";
  } else {
    setBadge(configRuntimeBadgeEl, "aligned", "ok");
    configRuntimeDetailEl.textContent = "Runtime state matches the saved config.";
  }

  if (status.pending_process_restarts?.length) {
    setBadge(configRestartBadgeEl, "pending", "warn");
    configRestartDetailEl.textContent = status.blocking_reason || `Waiting on: ${status.pending_process_restarts.join(", ")}.`;
  } else {
    setBadge(configRestartBadgeEl, "clear", "ok");
    configRestartDetailEl.textContent = "No supervised child process restart is pending.";
  }

  if (status.control_plane_restart_required) {
    setBadge(configControlPlaneBadgeEl, "restart required", "warn");
    configControlPlaneDetailEl.textContent = "Restart rns-server to apply embedded HTTP control-plane changes.";
  } else if (status.control_plane_reload_required) {
    setBadge(configControlPlaneBadgeEl, "reload pending", "warn");
    configControlPlaneDetailEl.textContent = "Embedded HTTP auth settings were saved but are not active in runtime yet.";
  } else {
    setBadge(configControlPlaneBadgeEl, "active", "ok");
    configControlPlaneDetailEl.textContent = "Embedded HTTP control-plane settings are active.";
  }

  configLastActionEl.textContent = status.last_action
    ? `${status.last_action} (${fmtAge(status.last_action_age_seconds)})`
    : "-";
  configLastSavedEl.textContent = fmtAge(status.last_saved_age_seconds);
  configLastAppliedEl.textContent = fmtAge(status.last_apply_age_seconds);
}

async function validateConfigCandidate() {
  await runConfigAction("/api/config/validate", "Validating...", "Validation");
}

async function saveConfigCandidate() {
  await runConfigAction("/api/config", "Saving...", "Save");
}

async function applyConfigCandidate() {
  await runConfigAction("/api/config/apply", "Saving and applying...", "Apply");
}

async function runConfigAction(path, pendingMessage, actionLabel) {
  try {
    syncJsonFromBuilder({ silent: true });
    configValidationStatusEl.textContent = pendingMessage;
    configActionSummaryEl.textContent = pendingMessage;
    configValidationResultEl.textContent = "";
    renderWarnings([]);
    const response = await fetch(path, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...authHeaders(),
      },
      body: configCandidateEl.value.trim(),
    });
    const payload = await response.json();
    if (!response.ok) {
      throw new Error(payload.error || `${response.status} ${response.statusText}`);
    }
    configValidationStatusEl.textContent = `${actionLabel} succeeded`;
    configValidationResultEl.textContent = JSON.stringify(payload.result, null, 2);
    renderConfigPlan(payload.result?.apply_plan);
    renderActionSummary(payload.result, actionLabel);
    renderWarnings(payload.result?.warnings || []);
    if (path !== "/api/config/validate") {
      configEditorDirty = false;
      configBuilderDirty = false;
    }
    await refresh();
  } catch (error) {
    configValidationStatusEl.textContent = `${actionLabel} failed`;
    configActionSummaryEl.textContent = error.message;
    configValidationResultEl.textContent = error.message;
  }
}

function renderConfigPlan(plan) {
  if (!plan) {
    configPlanSummaryEl.textContent = "No plan yet";
    setBadge(configPlanActionEl, "unknown", "info");
    configPlanImpactEl.textContent = "Validate a config change to preview its operational impact.";
    configPlanTargetsEl.textContent = "No targets yet.";
    configPlanChangeCountEl.textContent = "0";
    configChangeRowsEl.innerHTML = "";
    return;
  }

  const restartList = plan.processes_to_restart?.length
    ? plan.processes_to_restart.join(", ")
    : "none";
  const controlPlane = plan.control_plane_restart_required ? "yes" : "no";
  const action = plan.overall_action || "unknown";
  const targets = [
    ...(plan.processes_to_restart || []),
    ...(plan.control_plane_reload_required ? ["embedded-http-auth"] : []),
    ...(plan.control_plane_restart_required ? ["rns-server"] : []),
  ];
  const impact = describePlanImpact(plan);
  const notes = plan.notes?.length ? ` ${plan.notes.join(" ")}` : "";
  configPlanSummaryEl.textContent = `Action: ${action}. Processes to restart: ${restartList}. rns-server restart required: ${controlPlane}.${notes}`;
  setBadge(configPlanActionEl, action.replaceAll("_", " "), planBadgeClass(plan));
  configPlanImpactEl.textContent = impact;
  configPlanTargetsEl.innerHTML = targets.length
    ? `<div class="target-list">${targets.map((target) => `<span class="pill info">${target}</span>`).join("")}</div>`
    : "No runtime targets affected.";
  configPlanChangeCountEl.textContent = String((plan.changes || []).length);

  configChangeRowsEl.innerHTML = "";
  for (const change of plan.changes || []) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${change.field}</td>
      <td>${change.before}</td>
      <td>${change.after}</td>
      <td>${change.effect}</td>
    `;
    configChangeRowsEl.appendChild(tr);
  }
}

function describePlanImpact(plan) {
  switch (plan.overall_action) {
    case "none":
      return "No runtime impact. The candidate matches the current effective configuration.";
    case "restart_children":
      return "Applying this config will restart one or more supervised child processes.";
    case "reload_control_plane":
      return "Applying this config will reload embedded HTTP auth settings without restarting rns-server.";
    case "restart_children_and_reload_control_plane":
      return "Applying this config will restart child processes and reload embedded HTTP auth settings.";
    case "restart_server":
      return "Applying this config will still require a full rns-server restart before all control-plane changes take effect.";
    case "restart_children_and_server":
      return "Applying this config will restart child processes and still require a full rns-server restart.";
    default:
      return "Validate the config to inspect its operational impact.";
  }
}

function planBadgeClass(plan) {
  switch (plan.overall_action) {
    case "none":
      return "ok";
    case "reload_control_plane":
      return "info";
    case "restart_children":
    case "restart_children_and_reload_control_plane":
      return "warn";
    case "restart_server":
    case "restart_children_and_server":
      return "warn";
    default:
      return "info";
  }
}

function renderConfigSchema(schema) {
  if (!schema) {
    schemaExampleJson = "";
    configSchemaNotesEl.textContent = "No schema loaded yet";
    configSchemaRowsEl.innerHTML = "";
    return;
  }

  schemaExampleJson = schema.example_config_json || "";
  configSchemaNotesEl.textContent = (schema.notes || []).join(" ");
  configSchemaRowsEl.innerHTML = "";
  for (const field of schema.fields || []) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>${field.field}</td>
      <td>${field.field_type}</td>
      <td>${field.default_value}</td>
      <td>${field.effect}<div class="muted">${field.description ?? ""}</div></td>
    `;
    configSchemaRowsEl.appendChild(tr);
  }
}

function parseConfigText(text) {
  return JSON.parse((text || "").trim() || "{}");
}

function normalizeOptionalText(value) {
  const trimmed = (value || "").trim();
  return trimmed ? trimmed : undefined;
}

function configFromSnapshot(config) {
  return parseConfigText(config?.server_config_file_json ?? "{}");
}

function populateBuilder(config) {
  const http = config?.http || {};
  builderStatsDbPathEl.value = config?.stats_db_path || "";
  builderRnsdBinEl.value = config?.rnsd_bin || "";
  builderSentineldBinEl.value = config?.sentineld_bin || "";
  builderStatsdBinEl.value = config?.statsd_bin || "";
  builderHttpEnabledEl.checked = http.enabled !== false;
  builderHttpHostEl.value = http.host || "";
  builderHttpPortEl.value = http.port != null ? String(http.port) : "";
  builderHttpDisableAuthEl.checked = http.disable_auth === true;
  builderHttpAuthTokenEl.value = http.auth_token || "";
  configBuilderDirty = false;
}

function buildConfigFromBuilder() {
  const config = {};
  const http = {
    enabled: builderHttpEnabledEl.checked,
    disable_auth: builderHttpDisableAuthEl.checked,
  };
  const statsDbPath = normalizeOptionalText(builderStatsDbPathEl.value);
  const rnsdBin = normalizeOptionalText(builderRnsdBinEl.value);
  const sentineldBin = normalizeOptionalText(builderSentineldBinEl.value);
  const statsdBin = normalizeOptionalText(builderStatsdBinEl.value);
  const httpHost = normalizeOptionalText(builderHttpHostEl.value);
  const httpAuthToken = normalizeOptionalText(builderHttpAuthTokenEl.value);
  const httpPortValue = builderHttpPortEl.value.trim();

  if (statsDbPath) config.stats_db_path = statsDbPath;
  if (rnsdBin) config.rnsd_bin = rnsdBin;
  if (sentineldBin) config.sentineld_bin = sentineldBin;
  if (statsdBin) config.statsd_bin = statsdBin;
  if (httpHost) http.host = httpHost;
  if (httpAuthToken) http.auth_token = httpAuthToken;
  if (httpPortValue) {
    const parsedPort = Number.parseInt(httpPortValue, 10);
    if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
      throw new Error("HTTP port must be an integer between 1 and 65535");
    }
    http.port = parsedPort;
  }

  config.http = http;
  return config;
}

function loadConfigEditor(text, statusMessage) {
  configCandidateEl.value = text || "";
  configEditorDirty = false;
  configValidationStatusEl.textContent = statusMessage;
  updateBuilderDirtyState();
}

function syncBuilderFromJson(options = {}) {
  const parsed = parseConfigText(configCandidateEl.value);
  populateBuilder(parsed);
  if (!options.silent) {
    configValidationStatusEl.textContent = "Builder updated from JSON";
  }
  updateBuilderDirtyState();
}

function syncJsonFromBuilder(options = {}) {
  configCandidateEl.value = JSON.stringify(buildConfigFromBuilder(), null, 2);
  configEditorDirty = false;
  configBuilderDirty = false;
  if (!options.silent) {
    configValidationStatusEl.textContent = "Generated JSON updated from builder";
  }
  updateBuilderDirtyState();
}

function formatConfigEditor() {
  try {
    const parsed = parseConfigText(configCandidateEl.value);
    configCandidateEl.value = JSON.stringify(parsed, null, 2);
    configEditorDirty = false;
    configValidationStatusEl.textContent = "Candidate JSON formatted";
    updateBuilderDirtyState();
  } catch (error) {
    configValidationStatusEl.textContent = `Format failed: ${error.message}`;
  }
}

function updateBuilderDirtyState() {
  if (!builderDirtyStateEl) return;
  builderDirtyStateEl.textContent = configBuilderDirty
    ? "Builder changes are waiting to be generated into JSON"
    : "Builder is in sync";
}

function setAdvancedConfigVisible(visible) {
  advancedConfigVisible = visible;
  advancedConfigSectionEl.classList.toggle("hidden", !visible);
  toggleAdvancedConfigButton.textContent = visible ? "Hide Advanced JSON" : "Show Advanced JSON";
}

function syncJsonFromBuilderOnInput() {
  try {
    syncJsonFromBuilder({ silent: true });
  } catch (error) {
    configValidationStatusEl.textContent = `Builder export failed: ${error.message}`;
  }
}

function renderWarnings(warnings) {
  configWarningListEl.innerHTML = "";
  for (const warning of warnings || []) {
    const li = document.createElement("li");
    li.textContent = warning;
    configWarningListEl.appendChild(li);
  }
}

function renderActionSummary(result, actionLabel) {
  if (!result) {
    configActionSummaryEl.textContent = "No config action run yet";
    return;
  }
  const childRestarts = result.apply_plan?.processes_to_restart?.length
    ? result.apply_plan.processes_to_restart.join(", ")
    : "none";
  const serverRestart = result.apply_plan?.control_plane_restart_required ? "yes" : "no";
  const action = result.apply_plan?.overall_action || "unknown";
  const warningCount = result.warnings?.length || 0;
  configActionSummaryEl.textContent = `${actionLabel}: action ${action}; child restarts ${childRestarts}; rns-server restart required ${serverRestart}; warnings ${warningCount}.`;
}

function renderProcessLogs(process, lines) {
  logProcessNameEl.textContent = process || "No process selected";
  if (!process) {
    logStatusEl.textContent = "Choose a process log stream";
    processLogOutputEl.textContent = "";
    return;
  }
  logStatusEl.textContent = `${lines.length} recent lines`;
  processLogOutputEl.textContent = lines
    .slice()
    .reverse()
    .map((entry) => `[${entry.stream}] ${entry.line}`)
    .join("\n");
}

function renderProcessLogPayload(payload) {
  const lines = payload.lines || [];
  renderProcessLogs(payload.process, lines);
  const details = [
    `${lines.length} recent lines`,
    payload.recent_log_lines != null ? `${payload.recent_log_lines} buffered` : "",
    payload.last_log_age_seconds != null ? `last log ${fmtAge(payload.last_log_age_seconds)}` : "",
    payload.durable_log_path ? `file ${payload.durable_log_path}` : "",
  ].filter(Boolean);
  logStatusEl.textContent = details.join(" | ");
}

async function refreshLogs() {
  if (!selectedLogProcess) {
    renderProcessLogs(null, []);
    return;
  }
  try {
    const payload = await fetchJson(`/api/processes/${selectedLogProcess}/logs?limit=200`);
    renderProcessLogPayload(payload);
  } catch (error) {
    logProcessNameEl.textContent = selectedLogProcess;
    logStatusEl.textContent = error.message;
    processLogOutputEl.textContent = "";
  }
}

async function refresh() {
  try {
    const [node, config, configSchema, configStatus, processes, processEvents] = await Promise.all([
      fetchJson("/api/node"),
      fetchJson("/api/config"),
      fetchJson("/api/config/schema"),
      fetchJson("/api/config/status"),
      fetchJson("/api/processes"),
      fetchJson("/api/process_events"),
    ]);
    serverModeEl.textContent = node.server_mode || "-";
    uptimeEl.textContent = fmtSeconds(node.uptime_seconds);
    runningEl.textContent = `${node.processes_running}/${node.process_count}`;
    readyEl.textContent = `${node.processes_ready}/${node.process_count}`;
    renderConfig(config.config);
    renderConfigSchema(configSchema.schema);
    renderConfigStatus(configStatus.status);
    renderProcesses(processes.processes || []);
    renderProcessEvents(processEvents.events || []);
    await refreshLogs();
    statusEl.textContent = "Connected";
  } catch (error) {
    statusEl.textContent = error.message;
  }
}

saveButton.addEventListener("click", () => {
  localStorage.setItem("rnsctl_token", tokenInput.value.trim());
  refresh();
});
configCandidateEl.addEventListener("input", () => {
  configEditorDirty = true;
  updateBuilderDirtyState();
});
for (const input of [
  builderStatsDbPathEl,
  builderRnsdBinEl,
  builderSentineldBinEl,
  builderStatsdBinEl,
  builderHttpEnabledEl,
  builderHttpHostEl,
  builderHttpPortEl,
  builderHttpDisableAuthEl,
  builderHttpAuthTokenEl,
]) {
  input.addEventListener("input", () => {
    configBuilderDirty = true;
    updateBuilderDirtyState();
    syncJsonFromBuilderOnInput();
  });
  input.addEventListener("change", () => {
    configBuilderDirty = true;
    updateBuilderDirtyState();
    syncJsonFromBuilderOnInput();
  });
}
toggleAdvancedConfigButton.addEventListener("click", () => {
  setAdvancedConfigVisible(!advancedConfigVisible);
});

loadCurrentConfigButton.addEventListener("click", () => {
  loadConfigEditor(currentConfigJson, "Loaded current saved config");
  syncBuilderFromJson({ silent: true });
});
loadExampleConfigButton.addEventListener("click", () => {
  loadConfigEditor(schemaExampleJson, "Loaded example config");
  syncBuilderFromJson({ silent: true });
});
syncBuilderFromJsonButton.addEventListener("click", () => {
  try {
    syncBuilderFromJson();
  } catch (error) {
    configValidationStatusEl.textContent = `Builder sync failed: ${error.message}`;
  }
});
syncJsonFromBuilderButton.addEventListener("click", () => {
  try {
    syncJsonFromBuilder();
  } catch (error) {
    configValidationStatusEl.textContent = `Builder export failed: ${error.message}`;
  }
});
formatConfigButton.addEventListener("click", () => {
  formatConfigEditor();
  try {
    syncBuilderFromJson({ silent: true });
  } catch (_error) {
  }
});
validateConfigButton.addEventListener("click", () => {
  validateConfigCandidate();
});
saveConfigButton.addEventListener("click", () => {
  saveConfigCandidate();
});
applyConfigButton.addEventListener("click", () => {
  applyConfigCandidate();
});

setAdvancedConfigVisible(false);
updateBuilderDirtyState();
refresh();
setInterval(refresh, 2000);
