'use strict';
// ModelRailPro PWA — realtime besturing via WebSocket.
(function () {
  const $ = (s) => document.querySelector(s);
  let roster = { locos: [], turnouts: [], blocks: [], routes: [], runs: [], consists: [] };
  let state = { power: false, connected: false, mode: '', locos: {}, turnouts: {}, sensors: {} };
  let sel = null;          // actief loc-adres
  let selConsist = null;   // actieve tractie (consist) of null
  let es = null, wsReady = false;
  let lic = { enforced: false, locked: false, tier: 'free', message: '' };

  // ---- Transport: lokaal (SSE) of via de relay-cloud (poll) ------------
  // Relay-modus als de app op p33k.nl/baanbrein/app draait. Dan praat 'ie via de relay
  // met het lokale agentje (koppelcode), i.p.v. rechtstreeks met een lokale server.
  const RELAY_MODE = location.pathname.indexOf('/app') === 0 || location.pathname.indexOf('/baanbrein/app') === 0;
  const RELAY_URL = (location.pathname.indexOf('/baanbrein/') === 0 ? '/baanbrein/relay.php' : '/relay.php');
  let relayCode = null, relayOutSeq = 0, relayTimer = null;

  function handleMsg(m) {
    if (m.type === 'roster') { roster = { locos: m.locos || [], turnouts: m.turnouts || [], blocks: m.blocks || [], routes: m.routes || [], runs: m.runs || [], consists: m.consists || [] }; renderRoster(); renderTurnouts(); renderBlocks(); renderRoutes(); renderRuns(); if (window.BBManage) window.BBManage.refresh(); }
    else if (m.type === 'state') { state = m; onState(); }
    else if (m.type === 'log') { addLog(m.line); }
    else if (m.type === 'license') { lic = m; renderLicense(); }
    else if (m.type === 'update') { showUpdate(m); }
    else if (m.type === 'route_result') { if (!m.ok) alert((window.t ? window.t('route_busy') : 'bezet') + ': ' + (m.reason || '')); }
  }

  function connect() {
    if (RELAY_MODE) { relayConnect(); return; }
    try { if (es) es.close(); } catch (e) {}
    es = new EventSource('api/stream');
    es.onopen = () => { wsReady = true; setConn(); };
    es.onerror = () => { wsReady = false; setConn(); };
    es.onmessage = (ev) => { let m; try { m = JSON.parse(ev.data); } catch (e) { return; } handleMsg(m); };
  }

  function relayConnect() {
    relayCode = null;
    try { relayCode = localStorage.getItem('bb-relay-code'); } catch (e) {}
    const urlCode = new URLSearchParams(location.search).get('code');
    if (urlCode) { relayCode = urlCode; try { localStorage.setItem('bb-relay-code', urlCode); } catch (e) {} }
    if (!relayCode) {
      relayCode = (prompt(window.t ? window.t('relay_prompt') : 'Koppelcode (zie het agentje thuis):') || '').trim();
      if (relayCode) { try { localStorage.setItem('bb-relay-code', relayCode); } catch (e) {} }
    }
    if (!relayCode) return;
    clearInterval(relayTimer);
    relayTimer = setInterval(relayPoll, 400);
    relayPoll();
  }
  async function relayPoll() {
    try {
      const r = await (await fetch(RELAY_URL + '?code=' + encodeURIComponent(relayCode) + '&role=out&since=' + relayOutSeq)).json();
      if (r && Array.isArray(r.msgs)) {
        relayOutSeq = r.seq || relayOutSeq;
        wsReady = true;
        for (const it of r.msgs) handleMsg(it.v);
        if (!r.msgs.length) setConn();
      }
    } catch (e) { wsReady = false; setConn(); }
  }

  function send(o) {
    if (RELAY_MODE) {
      if (!relayCode) return;
      try { fetch(RELAY_URL + '?code=' + encodeURIComponent(relayCode) + '&role=cmd', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(o), keepalive: true }); } catch (e) {}
      return;
    }
    try { fetch('api/cmd', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(o), keepalive: true }); } catch (e) {}
  }
  // Bediening die rekening houdt met tractie: rijden/functies waaieren uit over alle leden.
  function tx(o) {
    if (selConsist && (o.type === 'throttle' || o.type === 'func')) {
      (selConsist.members || []).forEach((mb) => {
        if (o.type === 'throttle') send({ type: 'throttle', addr: mb.addr, speed: o.speed, dir: mb.reverse ? (o.dir ? 0 : 1) : o.dir });
        else send({ type: 'func', addr: mb.addr, fn: o.fn, on: o.on });
      });
    } else send(o);
  }

  // ---- render ----------------------------------------------------------
  function setConn() {
    const el = $('#conn');
    const T = window.t || ((k) => k);
    if (wsReady && state.connected) { el.className = 'pill on'; el.textContent = (state.mode === 'sim' ? T('sim_live') : T('connected_live')); }
    else if (wsReady) { el.className = 'pill'; el.textContent = T('waiting_central'); }
    else { el.className = 'pill off'; el.textContent = T('offline'); }
  }

  function renderRoster() {
    const r = $('#roster'); r.innerHTML = '';
    roster.locos.forEach((l) => {
      const c = document.createElement('div');
      c.className = 'lococard' + (sel === l.addr ? ' sel' : '');
      c.style.borderLeftColor = l.color || 'var(--accent)';
      const st = state.locos[l.addr] || { speed: 0, dir: 1 };
      const photo = l.photo ? '<div class="locphoto" style="background-image:url(' + JSON.stringify(l.photo) + ')"></div>' : '';
      c.innerHTML = photo + '<div class="ln"></div><div class="la"></div>' +
        '<div class="ls">' + (st.dir ? '▶' : '◀') + ' <b>' + (st.speed || 0) + '</b></div>';
      c.querySelector('.ln').textContent = l.name;
      c.querySelector('.la').textContent = (window.t?window.t('address'):'adres') + ' ' + l.addr;
      c.onclick = () => { sel = l.addr; selConsist = null; renderRoster(); renderCab(); };
      r.appendChild(c);
    });
    // tractie-kaarten (consists)
    (roster.consists || []).forEach((cs) => {
      const c = document.createElement('div');
      c.className = 'lococard consist' + (selConsist && selConsist.id === cs.id ? ' sel' : '');
      c.style.borderLeftColor = cs.color || 'var(--warn)';
      c.innerHTML = '<div class="ln"></div><div class="la"></div>';
      c.querySelector('.ln').textContent = '🔗 ' + (cs.name || 'Tractie');
      c.querySelector('.la').textContent = (cs.members || []).map((m) => m.addr).join(' + ');
      c.onclick = () => { selConsist = cs; sel = (cs.members[0] || {}).addr; renderRoster(); renderCab(); };
      r.appendChild(c);
    });
    if (sel === null && roster.locos.length) { sel = roster.locos[0].addr; renderCab(); }
  }

  function locoDef(addr) { return roster.locos.find((l) => l.addr === addr); }

  function renderCab() {
    const def = locoDef(sel);
    const cab = $('#cab');
    if (!def) { cab.hidden = true; return; }
    cab.hidden = false;
    const st = state.locos[sel] || { speed: 0, dir: 1, fns: {} };
    $('#cab-dot').style.background = (selConsist ? (selConsist.color || 'var(--warn)') : (def.color || 'var(--accent)'));
    const cabPhoto = $('#cab-photo');
    if (cabPhoto) { if (!selConsist && def.photo) { cabPhoto.style.backgroundImage = 'url(' + JSON.stringify(def.photo) + ')'; cabPhoto.hidden = false; } else cabPhoto.hidden = true; }
    $('#cab-name').textContent = selConsist ? ('🔗 ' + (selConsist.name || 'Tractie')) : def.name;
    $('#cab-addr').textContent = selConsist ? (selConsist.members || []).map((m) => m.addr).join(' + ') : ((window.t?window.t('address'):'adres') + ' ' + def.addr);
    setSpeedUI(st.speed || 0);
    setDirUI(st.dir);
    renderFuncs(def, st);
  }

  function setSpeedUI(v) {
    $('#speedval').textContent = v;
    const t = $('#throttle'); t.value = v;
    const pct = (v / 126) * 100;
    t.style.background = 'linear-gradient(90deg,var(--accent) ' + pct + '%,var(--panel2) ' + pct + '%)';
  }
  function setDirUI(dir) {
    $('#dir-fwd').classList.toggle('on', !!dir);
    $('#dir-rev').classList.toggle('on', !dir);
  }

  function renderFuncs(def, st) {
    const f = $('#funcs'); f.innerHTML = '';
    (def.functions || []).forEach((fn) => {
      const b = document.createElement('button');
      const on = !!(st.fns && st.fns[fn.fn]);
      b.className = 'fbtn' + (on ? ' on' : '');
      b.innerHTML = '<span class="fi">' + (fn.icon || '⚙') + '</span>' + fn.label +
        (fn.momentary ? '<span class="mo">'+(window.t?window.t('hold'):'(hou vast)')+'</span>' : '');
      if (fn.momentary) {
        const down = (e) => { e.preventDefault(); tx({ type: 'func', addr: sel, fn: fn.fn, on: true }); };
        const up = (e) => { e.preventDefault(); tx({ type: 'func', addr: sel, fn: fn.fn, on: false }); };
        b.addEventListener('pointerdown', down);
        b.addEventListener('pointerup', up);
        b.addEventListener('pointerleave', up);
        b.addEventListener('pointercancel', up);
      } else {
        b.onclick = () => tx({ type: 'func', addr: sel, fn: fn.fn, on: !on });
      }
      f.appendChild(b);
    });
  }

  function turnoutGrid(grid) {
    grid.innerHTML = '';
    roster.turnouts.forEach((t) => {
      const thrown = !!state.turnouts[t.id];
      const b = document.createElement('button');
      b.className = 'tbtn' + (thrown ? ' thrown' : '');
      b.innerHTML = '<span></span><span class="tstate">' + (thrown ? '↱ ' + (window.t?window.t('diverging'):'AFBUIGEND') : '↑ ' + (window.t?window.t('straight'):'RECHTDOOR')) + '</span>';
      b.querySelector('span').textContent = t.name;
      b.onclick = () => send({ type: 'turnout', id: t.id, thrown: !thrown });
      grid.appendChild(b);
    });
  }
  function renderTurnouts() {
    const sec = $('#turnouts-sec');
    if (!roster.turnouts.length) { sec.hidden = true; }
    else { sec.hidden = false; turnoutGrid($('#turnouts')); turnoutGrid($('#turnouts2')); }
  }

  function renderBlocks() {
    const grid = $('#blocks');
    grid.innerHTML = '';
    if (!roster.blocks.length) { grid.innerHTML = '<p class="muted small">'+(window.t?window.t('no_blocks'):'Geen blokken gedefinieerd.')+'</p>'; return; }
    roster.blocks.forEach((bl) => {
      const bezet = !!state.sensors[bl.id];
      const d = document.createElement('div');
      d.className = 'block' + (bezet ? ' bezet' : '');
      d.innerHTML = '<span class="dot"></span><span></span><span class="bs">' + (bezet ? (window.t?window.t('block_busy'):'BEZET') : (window.t?window.t('block_free'):'vrij')) + '</span>';
      d.querySelectorAll('span')[1].textContent = bl.name;
      grid.appendChild(d);
    });
  }

  function renderRuns() {
    const grid = $('#runs'); if (!grid) return;
    const runs = roster.runs || [];
    const autoState = state.auto || {};
    grid.innerHTML = '';
    if (!runs.length) { grid.innerHTML = '<p class="muted small">' + (window.t ? window.t('no_runs') : '—') + '</p>'; return; }
    runs.forEach((run) => {
      const a = autoState[run.addr];
      const running = a && String(a.runId) === String(run.id);
      const b = document.createElement('button');
      b.className = 'rbtn' + (running ? ' active' : '');
      b.innerHTML = '<span></span><span class="rst"></span>';
      b.querySelectorAll('span')[0].textContent = (running ? '⏹ ' : '▶ ') + (run.name || ('Rit ' + run.id));
      const T = window.t || ((k) => k);
      b.querySelector('.rst').textContent = running
        ? (T('auto_running') + ' · ' + T('auto_step') + ' ' + a.step + '/' + a.steps + ' (' + a.phase + ')')
        : (T('auto_start_lbl') + ' · loc ' + run.addr);
      b.onclick = () => running ? send({ type: 'auto_stop', addr: run.addr }) : send({ type: 'auto_start', id: run.id });
      grid.appendChild(b);
    });
  }

  function renderRoutes() {
    const grid = $('#routes'); if (!grid) return;
    const routes = roster.routes || [];
    grid.innerHTML = '';
    routes.forEach((r) => {
      const blocks = r.blocks || [];
      const busy = blocks.some((b) => state.sensors && state.sensors[b]);
      const reserved = blocks.some((b) => state.reservations && state.reservations[b] !== undefined);
      const b = document.createElement('button');
      b.className = 'rbtn' + (busy ? ' busy' : (reserved ? ' active' : ''));
      const T = window.t || ((k) => k);
      const st = busy ? T('route_busy') : (reserved ? T('route_reserved') : T('route_free'));
      b.innerHTML = '<span></span><span class="rst"></span>';
      b.querySelectorAll('span')[0].textContent = r.name || ('Route ' + r.id);
      b.querySelector('.rst').textContent = (window.t ? window.t('route_set') : 'Instellen') + ' · ' + st;
      b.onclick = () => {
        if (reserved && !busy) send({ type: 'release', id: r.id });
        else send({ type: 'route', id: r.id, addr: sel });
      };
      grid.appendChild(b);
    });
  }

  // ---- versie + update-check (footer + topbar-chip) --------------------
  let updateInfo = null;
  function showVersion() {
    if (state.version) {
      const v = $('#app-version'); if (v) v.textContent = 'v' + state.version;
      const vb = $('#ver-btn'); if (vb && !updateInfo) vb.textContent = 'v' + state.version;
    }
  }
  function markUpdateChip(u) {
    updateInfo = u;
    const vb = $('#ver-btn'); if (!vb) return;
    const T = window.t || ((k) => k);
    vb.classList.add('has-update');
    vb.textContent = '⬆️ ' + (u.latest || '');
    vb.title = T('update_to') + ' ' + u.latest;
  }
  const _verBtn = $('#ver-btn');
  if (_verBtn) _verBtn.onclick = async () => {
    if (updateInfo && updateInfo.url) { window.open(updateInfo.url, '_blank', 'noopener'); return; }
    const T = window.t || ((k) => k);
    try {
      const u = await (await fetch('api/update')).json();
      if (u.updateAvailable) { markUpdateChip(u); window.open(u.url || 'https://p33k.nl/baanbrein/', '_blank', 'noopener'); }
      else alert('v' + (u.current || state.version || '') + ' — ' + T('up_to_date'));
    } catch (e) { /* offline */ }
  };
  const _chk = $('#checkupd');
  if (_chk) _chk.onclick = async () => {
    const r = $('#upd-result'); const T = window.t || ((k) => k);
    r.textContent = '…';
    try {
      const u = await (await fetch('api/update')).json();
      if (u.updateAvailable) {
        r.innerHTML = '';
        const a = document.createElement('a');
        a.href = u.url || 'https://p33k.nl/baanbrein/'; a.target = '_blank'; a.rel = 'noopener';
        a.className = 'upd-new'; a.textContent = '⬆️ ' + T('update_to') + ' ' + u.latest;
        r.appendChild(a);
      } else if (u.checkedOk) {
        r.textContent = '✓ ' + T('up_to_date');
      } else {
        r.textContent = T('upd_offline');
      }
    } catch (e) { r.textContent = T('upd_offline'); }
  };

  function renderLearn() {
    const st = $('#learn-status'); if (!st) return;
    const L = state.learn || {};
    const T = window.t || ((k) => k);
    const sb = $('#learn-start');
    if (sb) { sb.textContent = L.recording ? T('learn_stop') : T('learn_start'); sb.classList.toggle('on', !!L.recording); }
    const seen = (L.seen || []).join(', ');
    st.innerHTML = (L.recording ? '🔴 ' + T('learn_recording') + ' · ' : '') +
      T('learn_detected') + ': ' + (seen || '—') + (L.steps ? ' · ' + L.steps + ' ' + T('learn_steps') : '');
  }

  function onState() {
    setConn();
    showVersion();
    renderLearn();
    $('#power').setAttribute('aria-pressed', state.power ? 'true' : 'false');
    // alleen de actieve loc + roster-snelheden verversen (geen full rerender van inputs tijdens slepen)
    if (!dragging) { renderRoster(); renderCab(); }
    else { renderRosterSpeedsOnly(); }
    renderTurnouts();
    renderBlocks();
    renderRoutes();
    renderRuns();
  }
  function renderRosterSpeedsOnly() {
    document.querySelectorAll('.lococard').forEach((c, i) => {
      const l = roster.locos[i]; if (!l) return;
      const st = state.locos[l.addr] || { speed: 0, dir: 1 };
      const ls = c.querySelector('.ls'); if (ls) ls.innerHTML = (st.dir ? '▶' : '◀') + ' <b>' + (st.speed || 0) + '</b>';
    });
  }

  // ---- bediening -------------------------------------------------------
  let dragging = false;
  const thr = $('#throttle');
  thr.addEventListener('pointerdown', () => { dragging = true; });
  thr.addEventListener('pointerup', () => { dragging = false; });
  thr.addEventListener('input', () => {
    const v = parseInt(thr.value, 10);
    $('#speedval').textContent = v;
    const pct = (v / 126) * 100;
    thr.style.background = 'linear-gradient(90deg,var(--accent) ' + pct + '%,var(--panel2) ' + pct + '%)';
    const dir = $('#dir-fwd').classList.contains('on') ? 1 : 0;
    if (sel !== null) tx({ type: 'throttle', addr: sel, speed: v, dir: dir });
  });

  $('#stop').onclick = () => { if (sel === null) return; setSpeedUI(0); const dir = $('#dir-fwd').classList.contains('on') ? 1 : 0; tx({ type: 'throttle', addr: sel, speed: 0, dir: dir }); };
  function setDir(dir) {
    if (sel === null) return;
    setDirUI(dir); setSpeedUI(0);                 // veilig: eerst stoppen bij richtingswissel
    tx({ type: 'throttle', addr: sel, speed: 0, dir: dir });
  }
  $('#dir-fwd').onclick = () => setDir(1);
  $('#dir-rev').onclick = () => setDir(0);

  $('#power').onclick = () => send({ type: 'power', on: !state.power });
  $('#estop').onclick = () => send({ type: 'estop' });

  // ---- log -------------------------------------------------------------
  const logEl = $('#log'); const logLines = [];
  function addLog(line) { logLines.push(line); if (logLines.length > 200) logLines.shift(); logEl.textContent = logLines.join('\n'); logEl.scrollTop = logEl.scrollHeight; }

  // publieke API voor losse modules (bv. de schema-editor)
  const subs = [];
  window.BB = {
    send: send,
    roster: () => roster,
    state: () => state,
    onState: (cb) => { subs.push(cb); cb(state, roster); },
  };
  const _onState = onState;
  onState = function () { _onState(); subs.forEach((cb) => { try { cb(state, roster); } catch (e) {} }); };

  // ---- hulp: de baan koppelen ------------------------------------------
  const CENTRALS = {
    sim:        { label: 'Simulator', kind: 'none' },
    dccex:      { label: 'DCC-EX', kind: 'net', port: 2560 },
    z21:        { label: 'Roco/Fleischmann Z21', kind: 'net', port: 21105 },
    dr5000:     { label: 'Digikeijs DR5000', kind: 'net', port: 21105 },
    ecos:       { label: 'ESU ECoS', kind: 'net', port: 15471 },
    marklin:    { label: 'Märklin CS2/CS3', kind: 'net', port: 15731 },
    twincenter: { label: 'Fleischmann Twin Center', kind: 'serial' },
    xpressnet:  { label: 'Lenz/multiMaus (XpressNet)', kind: 'serial' },
    loconet:    { label: 'Uhlenbrock/LoDi (LocoNet)', kind: 'serial' },
  };
  function renderHelp() {
    const T = window.t || ((k) => k);
    const mode = (state && state.mode) || 'sim';
    const c = CENTRALS[mode] || { label: mode, kind: 'net' };
    const connected = state && state.connected;
    // statusregel
    const st = $('#help-status');
    const ok = connected && mode !== 'sim';
    st.className = 'help-status ' + (mode === 'sim' ? 'sim' : (connected ? 'ok' : 'wait'));
    st.innerHTML = '<b>' + T('help_current') + ':</b> ' + c.label +
      ' — ' + (mode === 'sim' ? T('help_sim_on') : (connected ? T('help_connected') : T('help_notconn')));

    const body = $('#help-body');
    let steps = [];
    if (mode === 'sim') {
      steps = [T('help_sim_1'), T('help_sim_2')];
    } else if (c.kind === 'net') {
      steps = [
        T('help_net_1'),
        T('help_net_2').replace('{mode}', mode),
        T('help_net_3') + (c.port ? ' (poort ' + c.port + ')' : ''),
        T('help_net_4'),
        T('help_check'),
      ];
    } else {
      steps = [
        T('help_ser_1'),
        T('help_ser_2'),
        T('help_ser_3').replace('{mode}', mode),
        T('help_ser_4'),
        T('help_check'),
      ];
    }
    body.innerHTML = '<ol>' + steps.map((s) => '<li>' + s + '</li>').join('') + '</ol>' +
      '<p class="muted small">' + T('help_more') + ' <a href="https://p33k.nl/baanbrein/#download" target="_blank" rel="noopener">p33k.nl/baanbrein</a> · HARDWARE.md</p>';
  }
  $('#help-btn').onclick = () => { renderHelp(); $('#help-dialog').showModal(); };
  $('#help-close').onclick = () => $('#help-dialog').close();

  // ---- leer-modus ------------------------------------------------------
  const _ls = $('#learn-start');
  if (_ls) _ls.onclick = () => send({ type: (state.learn && state.learn.recording) ? 'learn_stop' : 'learn_start' });
  const _la = $('#learn-apply'); if (_la) _la.onclick = () => send({ type: 'learn_apply' });
  const _lc = $('#learn-clear'); if (_lc) _lc.onclick = () => send({ type: 'learn_clear' });

  // ---- telefoon koppelen (QR) ------------------------------------------
  function renderQR(url) {
    const box = $('#qr-canvas'); if (!box || !window.QR) return;
    box.innerHTML = '';
    const m = window.QR.make(url, 'M'); const n = m.count, scale = 6, quiet = 4;
    const dim = (n + quiet * 2) * scale;
    const cv = document.createElement('canvas'); cv.width = cv.height = dim;
    const ctx = cv.getContext('2d');
    ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, dim, dim);
    ctx.fillStyle = '#000';
    for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (m.isDark(r, c)) ctx.fillRect((c + quiet) * scale, (r + quiet) * scale, scale, scale);
    box.appendChild(cv);
  }
  $('#qr-btn').onclick = () => {
    const url = (state && state.serverUrl) || location.origin;
    $('#qr-url').textContent = url;
    renderQR(url);
    $('#qr-dialog').showModal();
  };
  $('#qr-close').onclick = () => $('#qr-dialog').close();

  // ---- update-melding --------------------------------------------------
  function showUpdate(u) {
    markUpdateChip(u);
    const b = $('#update-banner');
    b.hidden = false;
    b.innerHTML = (window.t?window.t('update_avail',{latest:u.latest||'',current:u.current||''}):('Nieuwe versie '+(u.latest||''))) + '<a href="' + (u.url || 'https://p33k.nl/baanbrein/') + '" target="_blank" rel="noopener">' + (window.t?window.t('download'):'Downloaden') + '</a>';
  }

  // ---- licentie --------------------------------------------------------
  function renderLicense() {
    const btn = $('#lic-btn'); const banner = $('#lic-banner');
    btn.className = 'pwr' + (lic.locked ? ' locked' : (lic.tier === 'pro' ? ' pro' : ''));
    btn.textContent = lic.tier === 'pro' ? (window.t?window.t('lic_pro'):'🔑 Pro') : (lic.locked ? (window.t?window.t('lic_locked'):'🔒 Vergrendeld') : (window.t?window.t('lic_free'):'🔓 Gratis'));
    $('#lic-status').textContent = lic.message || '';
    if (lic.locked) {
      banner.hidden = false;
      banner.innerHTML = (window.t?window.t('lic_locked_banner'):'🔒 Besturen is vergrendeld') + ' <a id="lic-open">' + (window.t?window.t('lic_enter'):'Licentie invoeren') + '</a>';
      const o = $('#lic-open'); if (o) o.onclick = openLic;
    } else { banner.hidden = true; }
  }
  function openLic() { $('#lic-msg').textContent = ''; $('#lic-dialog').showModal(); }
  $('#lic-btn').onclick = openLic;
  $('#lic-cancel').onclick = () => $('#lic-dialog').close();
  $('#lic-activate').onclick = async () => {
    const key = $('#lic-input').value.trim(); const msg = $('#lic-msg');
    if (!key) { msg.className = 'lic-msg err'; msg.textContent = 'Plak eerst een sleutel.'; return; }
    try {
      const r = await fetch('api/license', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
      const j = await r.json();
      if (j.ok) { msg.className = 'lic-msg ok'; msg.textContent = '✓ Geactiveerd: ' + (j.status.message || ''); lic = j.status; renderLicense(); setTimeout(() => $('#lic-dialog').close(), 1200); }
      else { msg.className = 'lic-msg err'; msg.textContent = '✗ ' + (j.error || 'ongeldige sleutel'); }
    } catch (e) { msg.className = 'lic-msg err'; msg.textContent = '✗ kon niet activeren'; }
  };

  // In relay-modus: Beheer + Schema-bewerken vereisen het lokale agentje → verberg ze.
  // (Rijden + Baan werken volledig via de relay.)
  if (RELAY_MODE) {
    document.querySelectorAll('.tab[data-tab="beheer"], .tab[data-tab="schema"]').forEach((b) => b.remove());
  }
  // tabs (generiek over alle tabviews)
  document.querySelectorAll('.tab').forEach((t) => {
    t.onclick = () => {
      const which = t.dataset.tab;
      document.querySelectorAll('.tab').forEach((x) => x.classList.toggle('on', x === t));
      document.querySelectorAll('.tabview').forEach((v) => { v.hidden = (v.id !== 'tab-' + which); });
      if (which === 'baan' && window.BBSchema) window.BBSchema.renderLive();
      if (which === 'beheer' && window.BBManage) window.BBManage.refresh();
    };
  });

  // ---- taalkeuze -------------------------------------------------------
  (function initLang() {
    const sel = $('#lang');
    if (!sel || !window.i18nLangs) return;
    window.i18nLangs().forEach((l) => { const o = document.createElement('option'); o.value = l.code; o.textContent = l.code.toUpperCase(); sel.appendChild(o); });
    sel.value = window.getLang();
    sel.onchange = () => window.setLang(sel.value);
    window.applyI18n();
  })();
  // bij taalwissel: alle dynamisch gerenderde delen opnieuw tekenen
  document.addEventListener('langchange', () => {
    renderRoster(); renderCab(); renderTurnouts(); renderBlocks(); renderRoutes(); renderRuns();
    renderLicense(); setConn();
    const h = $('#baan-hint'); if (h && window.t) h.textContent = window.t('baan_hint');
  });

  // ---- thema (licht/donker) --------------------------------------------
  (function initTheme() {
    let th; try { th = localStorage.getItem('bb-theme'); } catch (e) {}
    if (!th) th = (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
    document.documentElement.dataset.theme = th;
    const btn = $('#theme-btn');
    if (btn) {
      const paint = () => { btn.textContent = document.documentElement.dataset.theme === 'light' ? '🌙' : '☀️'; };
      paint();
      btn.onclick = () => { const n = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light'; document.documentElement.dataset.theme = n; try { localStorage.setItem('bb-theme', n); } catch (e) {} paint(); };
    }
  })();

  // ---- scherm-aan houden (wake lock) -----------------------------------
  let wakeLock = null;
  async function keepAwake() {
    try { if ('wakeLock' in navigator && !wakeLock) { wakeLock = await navigator.wakeLock.request('screen'); wakeLock.addEventListener('release', () => { wakeLock = null; }); } } catch (e) {}
  }
  document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') keepAwake(); });
  keepAwake();

  // ---- haptisch: trillen bij noodstop ----------------------------------
  const _estopBtn = $('#estop');
  if (_estopBtn) _estopBtn.addEventListener('click', () => { if (navigator.vibrate) navigator.vibrate([40, 30, 40]); });

  // ---- toetsenbord-bediening (pc) --------------------------------------
  document.addEventListener('keydown', (e) => {
    const tag = (e.target && e.target.tagName) || '';
    if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
    if (sel === null) return;
    const st = state.locos[sel] || { speed: 0, dir: 1 };
    const dir = $('#dir-fwd').classList.contains('on') ? 1 : 0;
    if (e.key === 'ArrowUp') { e.preventDefault(); const v = Math.min(126, (st.speed || 0) + 5); setSpeedUI(v); tx({ type: 'throttle', addr: sel, speed: v, dir: dir }); }
    else if (e.key === 'ArrowDown') { e.preventDefault(); const v = Math.max(0, (st.speed || 0) - 5); setSpeedUI(v); tx({ type: 'throttle', addr: sel, speed: v, dir: dir }); }
    else if (e.key === ' ') { e.preventDefault(); setSpeedUI(0); tx({ type: 'throttle', addr: sel, speed: 0, dir: dir }); }
    else if (e.key === 'ArrowLeft') { e.preventDefault(); setDir(0); }
    else if (e.key === 'ArrowRight') { e.preventDefault(); setDir(1); }
    else if (e.key === 'Escape') { e.preventDefault(); send({ type: 'estop' }); if (navigator.vibrate) navigator.vibrate([40, 30, 40]); }
  });

  // ---- hamburgermenu (mobiel) ------------------------------------------
  (function initMenu() {
    const btn = $('#menu-btn'), menu = $('#menu');
    if (!btn || !menu) return;
    const close = () => { menu.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); };
    btn.onclick = (e) => { e.stopPropagation(); const open = menu.classList.toggle('open'); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); };
    menu.addEventListener('click', (e) => { if (e.target.closest('button, a')) close(); });
    document.addEventListener('click', (e) => { if (!menu.contains(e.target) && e.target !== btn) close(); });
  })();

  // service worker (PWA)
  if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js').catch(() => {});

  connect();
})();
