/* settings_panel.js — Saltybot Settings Panel (Issue #614) */
'use strict';

// ── ROS2 parameter type constants ──────────────────────────────────────────
const P_BOOL    = 1;
const P_INT     = 2;
const P_DOUBLE  = 3;

// ── Section / parameter definitions ───────────────────────────────────────
// Each section: { id, node, params: [{name, label, type, min, max, step, unit, def}] }
const SECTIONS = {

  balance_pid: {
    node: 'balance_controller',
    params: [
      { name: 'pid_p', label: 'Proportional (Kp)', type: P_DOUBLE, min: 0, max: 5,    step: 0.01,  unit: '',      def: 0.5  },
      { name: 'pid_i', label: 'Integral (Ki)',      type: P_DOUBLE, min: 0, max: 2,    step: 0.005, unit: '',      def: 0.1  },
      { name: 'pid_d', label: 'Derivative (Kd)',    type: P_DOUBLE, min: 0, max: 1,    step: 0.005, unit: '',      def: 0.05 },
      { name: 'i_clamp',label:'I clamp',            type: P_DOUBLE, min: 0, max: 50,   step: 0.5,   unit: '',      def: 10.0 },
      { name: 'frequency',label:'Control rate',     type: P_INT,    min: 10, max: 200, step: 10,    unit: 'Hz',    def: 50   },
    ],
  },

  adaptive_pid_empty: {
    node: 'adaptive_pid',
    params: [
      { name: 'kp_empty', label: 'Kp (empty load)',  type: P_DOUBLE, min: 0, max: 50,  step: 0.5,  unit: '', def: 15.0 },
      { name: 'ki_empty', label: 'Ki (empty load)',  type: P_DOUBLE, min: 0, max: 5,   step: 0.05, unit: '', def: 0.5  },
      { name: 'kd_empty', label: 'Kd (empty load)',  type: P_DOUBLE, min: 0, max: 10,  step: 0.1,  unit: '', def: 1.5  },
      { name: 'kp_light', label: 'Kp (light load)',  type: P_DOUBLE, min: 0, max: 50,  step: 0.5,  unit: '', def: 18.0 },
      { name: 'ki_light', label: 'Ki (light load)',  type: P_DOUBLE, min: 0, max: 5,   step: 0.05, unit: '', def: 0.6  },
      { name: 'kd_light', label: 'Kd (light load)',  type: P_DOUBLE, min: 0, max: 10,  step: 0.1,  unit: '', def: 2.0  },
      { name: 'kp_heavy', label: 'Kp (heavy load)',  type: P_DOUBLE, min: 0, max: 50,  step: 0.5,  unit: '', def: 22.0 },
      { name: 'ki_heavy', label: 'Ki (heavy load)',  type: P_DOUBLE, min: 0, max: 5,   step: 0.05, unit: '', def: 0.8  },
      { name: 'kd_heavy', label: 'Kd (heavy load)',  type: P_DOUBLE, min: 0, max: 10,  step: 0.1,  unit: '', def: 2.5  },
    ],
  },

  adaptive_pid_bounds: {
    node: 'adaptive_pid',
    params: [
      { name: 'kp_min',      label: 'Kp min',       type: P_DOUBLE, min: 0, max: 20,  step: 0.5,  unit: '', def: 5.0  },
      { name: 'kp_max',      label: 'Kp max',       type: P_DOUBLE, min: 0, max: 80,  step: 1,    unit: '', def: 40.0 },
      { name: 'ki_min',      label: 'Ki min',       type: P_DOUBLE, min: 0, max: 5,   step: 0.05, unit: '', def: 0.0  },
      { name: 'ki_max',      label: 'Ki max',       type: P_DOUBLE, min: 0, max: 10,  step: 0.1,  unit: '', def: 5.0  },
      { name: 'kd_min',      label: 'Kd min',       type: P_DOUBLE, min: 0, max: 5,   step: 0.05, unit: '', def: 0.0  },
      { name: 'kd_max',      label: 'Kd max',       type: P_DOUBLE, min: 0, max: 20,  step: 0.2,  unit: '', def: 10.0 },
      { name: 'output_min',  label: 'Output min',   type: P_DOUBLE, min: -100, max: 0, step: 1,   unit: '', def: -50.0},
      { name: 'output_max',  label: 'Output max',   type: P_DOUBLE, min: 0, max: 100, step: 1,    unit: '', def: 50.0 },
    ],
  },

  tank_limits: {
    node: 'tank_driver',
    params: [
      { name: 'max_linear_vel',  label: 'Max linear vel',  type: P_DOUBLE, min: 0.1, max: 3.0, step: 0.05, unit: 'm/s',   def: 1.0 },
      { name: 'max_angular_vel', label: 'Max angular vel', type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.1,  unit: 'rad/s', def: 2.5 },
      { name: 'max_speed_ms',    label: 'Max drive speed', type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.1,  unit: 'm/s',   def: 1.5 },
      { name: 'slip_factor',     label: 'Track slip factor',type: P_DOUBLE,min: 0,   max: 0.5, step: 0.01, unit: '',      def: 0.3 },
      { name: 'watchdog_timeout_s',label:'Watchdog timeout',type:P_DOUBLE, min: 0.1, max: 2.0, step: 0.05, unit: 's',     def: 0.3 },
    ],
  },

  smooth_vel: {
    node: 'smooth_velocity_controller',
    params: [
      { name: 'max_linear_accel',  label: 'Max linear accel',  type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.05, unit: 'm/s²',   def: 0.5 },
      { name: 'max_linear_decel',  label: 'Max linear decel',  type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.05, unit: 'm/s²',   def: 0.8 },
      { name: 'max_angular_accel', label: 'Max angular accel', type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.1,  unit: 'rad/s²', def: 1.0 },
      { name: 'max_angular_decel', label: 'Max angular decel', type: P_DOUBLE, min: 0.1, max: 5.0, step: 0.1,  unit: 'rad/s²', def: 1.0 },
    ],
  },

  batt_speed: {
    node: 'battery_speed_limiter',
    params: [
      { name: 'speed_factor_full',     label: 'Speed factor (full)',     type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 1.0 },
      { name: 'speed_factor_reduced',  label: 'Speed factor (reduced)',  type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.7 },
      { name: 'speed_factor_critical', label: 'Speed factor (critical)', type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.4 },
    ],
  },

  safety_zone: {
    node: 'safety_zone',
    params: [
      { name: 'danger_range_m',       label: 'Danger range',        type: P_DOUBLE, min: 0.05, max: 1.0,  step: 0.01, unit: 'm',   def: 0.30 },
      { name: 'warn_range_m',         label: 'Warn range',          type: P_DOUBLE, min: 0.2,  max: 5.0,  step: 0.05, unit: 'm',   def: 1.00 },
      { name: 'forward_arc_deg',      label: 'Forward arc (±)',     type: P_DOUBLE, min: 10,   max: 180,  step: 5,    unit: '°',   def: 60.0 },
      { name: 'estop_debounce_frames',label: 'E-stop debounce',     type: P_INT,    min: 1,    max: 20,   step: 1,    unit: 'frames', def: 2  },
      { name: 'min_valid_range_m',    label: 'Min valid range',     type: P_DOUBLE, min: 0.01, max: 0.5,  step: 0.01, unit: 'm',   def: 0.05 },
      { name: 'publish_rate',         label: 'Publish rate',        type: P_DOUBLE, min: 1,    max: 50,   step: 1,    unit: 'Hz',  def: 10.0 },
    ],
  },

  power_sup: {
    node: 'power_supervisor_node',
    params: [
      { name: 'warning_pct',     label: 'Warning threshold',    type: P_DOUBLE, min: 5,  max: 60, step: 1, unit: '%', def: 30.0 },
      { name: 'dock_search_pct', label: 'Dock-search threshold',type: P_DOUBLE, min: 5,  max: 50, step: 1, unit: '%', def: 20.0 },
      { name: 'critical_pct',    label: 'Critical threshold',   type: P_DOUBLE, min: 2,  max: 30, step: 1, unit: '%', def: 10.0 },
      { name: 'emergency_pct',   label: 'Emergency threshold',  type: P_DOUBLE, min: 1,  max: 15, step: 1, unit: '%', def: 5.0  },
      { name: 'warn_speed_factor',   label: 'Speed factor (warn)',type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.6 },
      { name: 'critical_speed_factor',label:'Speed factor (crit)',type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.3 },
    ],
  },

  lidar_avoid: {
    node: 'lidar_avoidance',
    params: [
      { name: 'emergency_stop_distance', label: 'E-stop distance',    type: P_DOUBLE, min: 0.1, max: 3.0, step: 0.05, unit: 'm',   def: 0.5  },
      { name: 'min_safety_zone',         label: 'Min safety zone',    type: P_DOUBLE, min: 0.1, max: 2.0, step: 0.05, unit: 'm',   def: 0.6  },
      { name: 'safety_zone_at_max_speed',label: 'Zone at max speed',  type: P_DOUBLE, min: 0.5, max: 10,  step: 0.1,  unit: 'm',   def: 3.0  },
      { name: 'max_speed_reference',     label: 'Max speed reference',type: P_DOUBLE, min: 0.5, max: 20,  step: 0.1,  unit: 'm/s', def: 5.56 },
    ],
  },

  sensor_toggles: {
    node: 'safety_zone',  // placeholder — booleans often live on their own node
    params: [
      { name: 'estop_all_arcs',   label: 'E-stop all arcs',      type: P_BOOL, unit: '', def: false, desc: 'Any sector triggers e-stop' },
      { name: 'lidar_enabled',    label: 'LIDAR enabled',         type: P_BOOL, unit: '', def: true,  desc: '/scan input active' },
      { name: 'uwb_enabled',      label: 'UWB positioning',       type: P_BOOL, unit: '', def: true,  desc: 'UWB anchors active' },
    ],
  },

  imu_fusion: {
    node: 'uwb_imu_fusion',
    params: [
      { name: 'uwb_weight',   label: 'UWB weight',    type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.7 },
      { name: 'imu_weight',   label: 'IMU weight',    type: P_DOUBLE, min: 0, max: 1, step: 0.05, unit: '', def: 0.3 },
      { name: 'publish_rate', label: 'Publish rate',  type: P_DOUBLE, min: 1, max: 200, step: 1,   unit: 'Hz', def: 50.0 },
    ],
  },

};

// ── Runtime state: current values per section ──────────────────────────────
const values = {};   // values[sectionId][paramName] = currentValue
const dirty  = {};   // dirty[sectionId][paramName]  = true if changed vs loaded

// Initialise values from defaults
Object.keys(SECTIONS).forEach(sid => {
  values[sid] = {};
  dirty[sid]  = {};
  SECTIONS[sid].params.forEach(p => {
    values[sid][p.name] = p.def;
    dirty[sid][p.name]  = false;
  });
});

// ── ROS ────────────────────────────────────────────────────────────────────
let ros = null;

function getService(nodeName, type) {
  return new ROSLIB.Service({ ros, name: `/${nodeName}/${type}`, serviceType: `rcl_interfaces/srv/${type === 'get_parameters' ? 'GetParameters' : 'SetParameters'}` });
}

function extractValue(rosVal) {
  switch (rosVal.type) {
    case P_BOOL:   return rosVal.bool_value;
    case P_INT:    return rosVal.integer_value;
    case P_DOUBLE: return rosVal.double_value;
    default: return undefined;
  }
}

function makeRosValue(type, value) {
  const v = { type };
  if (type === P_BOOL)   v.bool_value    = !!value;
  if (type === P_INT)    v.integer_value = Math.round(value);
  if (type === P_DOUBLE) v.double_value  = parseFloat(value);
  return v;
}

// ── Section load / apply ───────────────────────────────────────────────────
window.loadSection = function(sid) {
  if (!ros) { setStatus(sid, 'err', 'Not connected'); return; }
  const sec = SECTIONS[sid];
  const svc = getService(sec.node, 'get_parameters');
  const names = sec.params.map(p => p.name);

  setStatus(sid, 'loading', `Loading from /${sec.node}…`);

  svc.callService({ names }, (resp) => {
    if (!resp || !resp.values) {
      setStatus(sid, 'err', 'No response'); return;
    }
    sec.params.forEach((p, i) => {
      const v = extractValue(resp.values[i]);
      if (v !== undefined) {
        values[sid][p.name] = v;
        dirty[sid][p.name]  = false;
        updateFieldUI(sid, p.name, v, false);
      }
    });
    setStatus(sid, 'ok', `Loaded ${resp.values.length} params`);
  }, (err) => {
    setStatus(sid, 'err', `Error: ${err}`);
  });
};

window.applySection = function(sid) {
  if (!ros) { setStatus(sid, 'err', 'Not connected'); return; }
  const sec = SECTIONS[sid];
  const svc = getService(sec.node, 'set_parameters');

  const parameters = sec.params.map(p => ({
    name:  p.name,
    value: makeRosValue(p.type, values[sid][p.name]),
  }));

  setStatus(sid, 'loading', `Applying to /${sec.node}…`);

  svc.callService({ parameters }, (resp) => {
    if (!resp || !resp.results) {
      setStatus(sid, 'err', 'No response'); return;
    }
    const failures = resp.results.filter(r => !r.successful);
    if (failures.length === 0) {
      sec.params.forEach(p => { dirty[sid][p.name] = false; });
      refreshFieldDirty(sid);
      setStatus(sid, 'ok', `Applied ${parameters.length} params ✓`);
    } else {
      const reasons = failures.map(r => r.reason).join('; ');
      setStatus(sid, 'err', `${failures.length} failed: ${reasons}`);
    }
  }, (err) => {
    setStatus(sid, 'err', `Error: ${err}`);
  });
};

// ── UI builder ─────────────────────────────────────────────────────────────
function buildFields() {
  Object.keys(SECTIONS).forEach(sid => {
    const sec = SECTIONS[sid];
    const container = document.getElementById(`${sid}-fields`);
    if (!container) return;

    sec.params.forEach(p => {
      if (p.type === P_BOOL) {
        // Toggle row
        const row = document.createElement('div');
        row.className = 'toggle-row';
        row.innerHTML = `
          <span class="param-label">${p.label}</span>
          <span class="toggle-desc">${p.desc || ''}</span>
          <label class="toggle-switch">
            <input type="checkbox" id="tgl-${sid}-${p.name}"
              ${p.def ? 'checked' : ''}>
            <div class="toggle-track"></div>
          </label>`;
        container.appendChild(row);

        const cb = row.querySelector(`#tgl-${sid}-${p.name}`);
        cb.addEventListener('change', () => {
          values[sid][p.name] = cb.checked;
          dirty[sid][p.name]  = true;
        });
      } else {
        // Slider + number input row
        const row = document.createElement('div');
        row.className = 'param-row';
        const sliderMin = p.min !== undefined ? p.min : 0;
        const sliderMax = p.max !== undefined ? p.max : 100;
        const sliderStep = p.step || 0.01;
        const defVal = p.def !== undefined ? p.def : 0;

        row.innerHTML = `
          <span class="param-label">${p.label}</span>
          <input type="range" class="param-slider"
            id="sld-${sid}-${p.name}"
            min="${sliderMin}" max="${sliderMax}" step="${sliderStep}"
            value="${defVal}">
          <input type="number" class="param-input"
            id="inp-${sid}-${p.name}"
            min="${sliderMin}" max="${sliderMax}" step="${sliderStep}"
            value="${defVal}">
          <span class="param-unit">${p.unit || ''}</span>`;
        container.appendChild(row);

        const slider = row.querySelector(`#sld-${sid}-${p.name}`);
        const input  = row.querySelector(`#inp-${sid}-${p.name}`);

        slider.addEventListener('input', () => {
          const v = parseFloat(slider.value);
          input.value = v;
          values[sid][p.name] = v;
          dirty[sid][p.name]  = true;
          input.classList.add('changed');
          slider.classList.add('changed');
        });

        input.addEventListener('change', () => {
          let v = parseFloat(input.value);
          v = Math.max(sliderMin, Math.min(sliderMax, v));
          input.value  = v;
          slider.value = v;
          values[sid][p.name] = v;
          dirty[sid][p.name]  = true;
          input.classList.add('changed');
          slider.classList.add('changed');
        });
      }
    });
  });
}

function updateFieldUI(sid, paramName, value, markDirty) {
  const sec = SECTIONS[sid];
  const p   = sec.params.find(x => x.name === paramName);
  if (!p) return;

  if (p.type === P_BOOL) {
    const cb = document.getElementById(`tgl-${sid}-${paramName}`);
    if (cb) cb.checked = !!value;
  } else {
    const sld = document.getElementById(`sld-${sid}-${paramName}`);
    const inp = document.getElementById(`inp-${sid}-${paramName}`);
    if (sld) sld.value = value;
    if (inp) inp.value = value;
    if (!markDirty) {
      if (sld) sld.classList.remove('changed');
      if (inp) inp.classList.remove('changed');
    }
  }
}

function refreshFieldDirty(sid) {
  const sec = SECTIONS[sid];
  sec.params.forEach(p => {
    if (p.type !== P_BOOL) {
      const sld = document.getElementById(`sld-${sid}-${p.name}`);
      const inp = document.getElementById(`inp-${sid}-${p.name}`);
      if (!dirty[sid][p.name]) {
        if (sld) sld.classList.remove('changed');
        if (inp) inp.classList.remove('changed');
      }
    }
  });
}

function setStatus(sid, cls, msg) {
  const el = document.getElementById(`${sid}-status`);
  if (!el) return;
  el.className = `sec-status ${cls}`;
  el.textContent = msg;
  if (cls === 'ok') setTimeout(() => { el.textContent = ''; el.className = 'sec-status'; }, 4000);
}

// ── Tabs ───────────────────────────────────────────────────────────────────
document.querySelectorAll('.tab-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById(`tab-${btn.dataset.tab}`).classList.add('active');
    if (btn.dataset.tab === 'system') startDiagRefresh();
  });
});

// ── Presets ────────────────────────────────────────────────────────────────
const PRESET_KEY = 'saltybot_settings_presets';

function getPresets() {
  try { return JSON.parse(localStorage.getItem(PRESET_KEY) || '{}'); } catch(_) { return {}; }
}

function savePresetsToStorage(presets) {
  localStorage.setItem(PRESET_KEY, JSON.stringify(presets));
}

function refreshPresetSelect() {
  const sel = document.getElementById('preset-select');
  const cur = sel.value;
  sel.innerHTML = '<option value="">— select —</option>';
  const presets = getPresets();
  Object.keys(presets).sort().forEach(name => {
    const opt = document.createElement('option');
    opt.value = name;
    opt.textContent = name;
    sel.appendChild(opt);
  });
  if (cur) sel.value = cur;
}

function snapshotAllValues() {
  const snap = {};
  Object.keys(values).forEach(sid => {
    snap[sid] = Object.assign({}, values[sid]);
  });
  return snap;
}

window.savePreset = function() {
  const nameInput = document.getElementById('preset-name');
  const name = nameInput.value.trim();
  if (!name) { alert('Enter a preset name'); return; }
  const presets = getPresets();
  presets[name] = snapshotAllValues();
  savePresetsToStorage(presets);
  nameInput.value = '';
  refreshPresetSelect();
  document.getElementById('preset-select').value = name;
  flashSaved();
};

window.loadPreset = function() {
  const name = document.getElementById('preset-select').value;
  if (!name) return;
  const presets = getPresets();
  const snap = presets[name];
  if (!snap) return;
  Object.keys(snap).forEach(sid => {
    if (!values[sid]) return;
    Object.keys(snap[sid]).forEach(paramName => {
      values[sid][paramName] = snap[sid][paramName];
      dirty[sid][paramName]  = true;
      updateFieldUI(sid, paramName, snap[sid][paramName], true);
    });
  });
  flashSaved();
};

window.deletePreset = function() {
  const name = document.getElementById('preset-select').value;
  if (!name) return;
  if (!confirm(`Delete preset "${name}"?`)) return;
  const presets = getPresets();
  delete presets[name];
  savePresetsToStorage(presets);
  refreshPresetSelect();
};

window.resetAllToDefaults = function() {
  if (!confirm('Reset all fields to built-in defaults?')) return;
  Object.keys(SECTIONS).forEach(sid => {
    SECTIONS[sid].params.forEach(p => {
      values[sid][p.name] = p.def;
      dirty[sid][p.name]  = false;
      updateFieldUI(sid, p.name, p.def, false);
    });
  });
};

function flashSaved() {
  const el = document.getElementById('save-indicator');
  el.classList.remove('hidden');
  el.style.animation = 'none';
  void el.offsetHeight;
  el.style.animation = 'fadeout 2s forwards';
  setTimeout(() => el.classList.add('hidden'), 2100);
}

// ── System tab: diagnostics ────────────────────────────────────────────────
let diagTopic = null;
let diagRefreshTimer = null;
let diagState = {
  cpuTemp: null, gpuTemp: null, boardTemp: null,
  motorTempL: null, motorTempR: null,
  ramUsed: null, ramTotal: null,
  gpuUsed: null, gpuTotal: null,
  diskUsed: null, diskTotal: null,
  rssi: null, latency: null, mqttConnected: null,
  nodes: [],
  lastUpdate: null,
};

function startDiagRefresh() {
  if (!ros || diagTopic) return;
  diagTopic = new ROSLIB.Topic({
    ros, name: '/diagnostics', messageType: 'diagnostic_msgs/DiagnosticArray',
    throttle_rate: 2000,
  });
  diagTopic.subscribe(onDiagnostics);
}

function stopDiagRefresh() {
  if (diagTopic) { diagTopic.unsubscribe(); diagTopic = null; }
}

function onDiagnostics(msg) {
  const kv = {};
  (msg.status || []).forEach(status => {
    (status.values || []).forEach(pair => {
      kv[pair.key] = pair.value;
    });
    diagState.nodes.push({ name: status.name, level: status.level, msg: status.message });
    if (diagState.nodes.length > 40) diagState.nodes.splice(0, diagState.nodes.length - 40);
  });

  // Extract known keys
  if (kv.cpu_temp_c)        diagState.cpuTemp    = parseFloat(kv.cpu_temp_c);
  if (kv.gpu_temp_c)        diagState.gpuTemp    = parseFloat(kv.gpu_temp_c);
  if (kv.board_temp_c)      diagState.boardTemp  = parseFloat(kv.board_temp_c);
  if (kv.motor_temp_l_c)    diagState.motorTempL = parseFloat(kv.motor_temp_l_c);
  if (kv.motor_temp_r_c)    diagState.motorTempR = parseFloat(kv.motor_temp_r_c);
  if (kv.ram_used_mb)       diagState.ramUsed    = parseFloat(kv.ram_used_mb);
  if (kv.ram_total_mb)      diagState.ramTotal   = parseFloat(kv.ram_total_mb);
  if (kv.gpu_used_mb)       diagState.gpuUsed    = parseFloat(kv.gpu_used_mb);
  if (kv.gpu_total_mb)      diagState.gpuTotal   = parseFloat(kv.gpu_total_mb);
  if (kv.disk_used_gb)      diagState.diskUsed   = parseFloat(kv.disk_used_gb);
  if (kv.disk_total_gb)     diagState.diskTotal  = parseFloat(kv.disk_total_gb);
  if (kv.wifi_rssi_dbm)     diagState.rssi       = parseFloat(kv.wifi_rssi_dbm);
  if (kv.wifi_latency_ms)   diagState.latency    = parseFloat(kv.wifi_latency_ms);
  if (kv.mqtt_connected !== undefined) diagState.mqttConnected = kv.mqtt_connected === 'true';

  diagState.lastUpdate = new Date();
  renderDiag();
  renderNet();
}

window.refreshDiag = function() {
  startDiagRefresh();
  renderDiag();
};

function tempColor(t) {
  if (t === null) return '';
  if (t > 80) return 'err';
  if (t > 65) return 'warn';
  return 'ok';
}

function pct(used, total) {
  if (!total) return '—';
  return ((used / total) * 100).toFixed(0) + '%';
}

function renderDiag() {
  const g = document.getElementById('diag-grid');
  if (!g) return;
  const d = diagState;
  if (d.lastUpdate === null) {
    g.innerHTML = '<div class="diag-placeholder">Waiting for /diagnostics…</div>';
    return;
  }

  const cards = [
    {
      title: 'Temperature',
      rows: [
        { k: 'CPU', v: d.cpuTemp !== null ? d.cpuTemp.toFixed(1) + ' °C' : '—', cls: tempColor(d.cpuTemp) },
        { k: 'GPU', v: d.gpuTemp !== null ? d.gpuTemp.toFixed(1) + ' °C' : '—', cls: tempColor(d.gpuTemp) },
        { k: 'Board',v:d.boardTemp !== null ? d.boardTemp.toFixed(1)+' °C':'—', cls:tempColor(d.boardTemp)},
        { k: 'Motor L', v: d.motorTempL !== null ? d.motorTempL.toFixed(1)+' °C':'—', cls:tempColor(d.motorTempL)},
        { k: 'Motor R', v: d.motorTempR !== null ? d.motorTempR.toFixed(1)+' °C':'—', cls:tempColor(d.motorTempR)},
      ],
    },
    {
      title: 'Memory',
      rows: [
        { k: 'RAM used',  v: d.ramUsed  !== null ? d.ramUsed.toFixed(0)+' MB':  '—', cls:'' },
        { k: 'RAM total', v: d.ramTotal !== null ? d.ramTotal.toFixed(0)+' MB': '—', cls:'' },
        { k: 'RAM %',     v: pct(d.ramUsed, d.ramTotal), cls: d.ramUsed/d.ramTotal > 0.85 ? 'warn' : 'ok' },
        { k: 'GPU mem',   v: d.gpuUsed  !== null ? d.gpuUsed.toFixed(0)+' MB':  '—', cls:'' },
        { k: 'GPU %',     v: pct(d.gpuUsed, d.gpuTotal), cls:'' },
      ],
    },
    {
      title: 'Storage',
      rows: [
        { k: 'Disk used',  v: d.diskUsed  !== null ? d.diskUsed.toFixed(1)+' GB':  '—', cls:'' },
        { k: 'Disk total', v: d.diskTotal !== null ? d.diskTotal.toFixed(1)+' GB': '—', cls:'' },
        { k: 'Disk %',     v: pct(d.diskUsed, d.diskTotal), cls: d.diskUsed/d.diskTotal > 0.9 ? 'err' : d.diskUsed/d.diskTotal > 0.75 ? 'warn' : 'ok' },
      ],
    },
    {
      title: 'Updated',
      rows: [
        { k: 'Last msg', v: d.lastUpdate ? d.lastUpdate.toLocaleTimeString() : '—', cls:'' },
      ],
    },
  ];

  g.innerHTML = cards.map(c => `
    <div class="diag-card">
      <div class="diag-card-title">${c.title}</div>
      ${c.rows.map(r => `<div class="diag-kv"><span class="diag-k">${r.k}</span><span class="diag-v ${r.cls||''}">${r.v}</span></div>`).join('')}
    </div>`).join('');
}

function rssiColor(rssi) {
  if (rssi === null) return '';
  if (rssi > -50) return 'ok';
  if (rssi > -70) return 'warn';
  return 'err';
}

function rssiBarCount(rssi) {
  if (rssi === null) return 0;
  if (rssi > -50) return 5;
  if (rssi > -60) return 4;
  if (rssi > -70) return 3;
  if (rssi > -80) return 2;
  return 1;
}

function latencyColor(ms) {
  if (ms === null) return '';
  if (ms < 50)  return 'ok';
  if (ms < 150) return 'warn';
  return 'err';
}

function renderNet() {
  const g = document.getElementById('net-grid');
  if (!g) return;
  const d = diagState;
  const bars = rssiBarCount(d.rssi);
  const heights = [4, 6, 9, 12, 15];

  const barsHtml = heights.map((h, i) =>
    `<div class="sig-bar ${i < bars ? 'lit' : ''}" style="height:${h}px"></div>`
  ).join('');

  g.innerHTML = `
    <div class="diag-card">
      <div class="diag-card-title">WiFi</div>
      <div class="diag-kv">
        <span class="diag-k">RSSI</span>
        <span class="diag-v ${rssiColor(d.rssi)}">${d.rssi !== null ? d.rssi + ' dBm' : '—'}</span>
      </div>
      <div class="diag-kv">
        <span class="diag-k">Signal</span>
        <span class="diag-v"><div class="sig-bars">${barsHtml}</div></span>
      </div>
      <div class="diag-kv">
        <span class="diag-k">Latency</span>
        <span class="diag-v ${latencyColor(d.latency)}">${d.latency !== null ? d.latency.toFixed(0) + ' ms' : '—'}</span>
      </div>
    </div>
    <div class="diag-card">
      <div class="diag-card-title">Services</div>
      <div class="diag-kv">
        <span class="diag-k">MQTT</span>
        <span class="diag-v ${d.mqttConnected === null ? '' : d.mqttConnected ? 'ok' : 'err'}">
          ${d.mqttConnected === null ? '—' : d.mqttConnected ? 'Connected' : 'Disconnected'}
        </span>
      </div>
      <div class="diag-kv">
        <span class="diag-k">rosbridge</span>
        <span class="diag-v ${ros ? 'ok' : 'err'}">${ros ? 'Connected' : 'Disconnected'}</span>
      </div>
    </div>`;
}

// ── Node list ──────────────────────────────────────────────────────────────
window.loadNodeList = function() {
  if (!ros) return;
  const svc = new ROSLIB.Service({
    ros, name: '/rosapi/nodes', serviceType: 'rosapi/Nodes',
  });
  svc.callService({}, (resp) => {
    const wrap = document.getElementById('node-list-wrap');
    if (!resp || !resp.nodes) { wrap.innerHTML = '<div class="diag-placeholder">No response</div>'; return; }
    const sorted = [...resp.nodes].sort();
    wrap.innerHTML = sorted.map(n =>
      `<span class="node-chip active-node">${n}</span>`
    ).join('');
  }, () => {
    document.getElementById('node-list-wrap').innerHTML =
      '<div class="diag-placeholder">Service unavailable — ensure rosapi is running</div>';
  });
};

// ── Connection ─────────────────────────────────────────────────────────────
const $connDot   = document.getElementById('conn-dot');
const $connLabel = document.getElementById('conn-label');

function connect() {
  const url = document.getElementById('ws-input').value.trim() || 'ws://localhost:9090';
  if (ros) { stopDiagRefresh(); try { ros.close(); } catch(_){} }

  $connLabel.textContent = 'Connecting…';
  $connLabel.style.color = '#d97706';
  $connDot.className = '';

  ros = new ROSLIB.Ros({ url });

  ros.on('connection', () => {
    $connDot.className = 'connected';
    $connLabel.style.color = '#22c55e';
    $connLabel.textContent = 'Connected';
    localStorage.setItem('settings_ws_url', url);
    // Auto-start diagnostics if system tab visible
    const sysPanel = document.getElementById('tab-system');
    if (sysPanel && sysPanel.classList.contains('active')) startDiagRefresh();
  });

  ros.on('error', () => {
    $connDot.className = 'error';
    $connLabel.style.color = '#ef4444';
    $connLabel.textContent = 'Error';
  });

  ros.on('close', () => {
    $connDot.className = '';
    $connLabel.style.color = '#6b7280';
    $connLabel.textContent = 'Disconnected';
    stopDiagRefresh();
    ros = null;
  });
}

document.getElementById('btn-connect').addEventListener('click', connect);

// ── Init ───────────────────────────────────────────────────────────────────
buildFields();
refreshPresetSelect();

const savedUrl = localStorage.getItem('settings_ws_url');
if (savedUrl) {
  document.getElementById('ws-input').value = savedUrl;
  document.getElementById('footer-ws').textContent = savedUrl;
  connect();
}
