(() => {
  const state = { aborter: null, socket: null, cursor: 0 };
  const byId = id => document.getElementById(id);
  const token = () => byId('token').value.trim();
  const headers = json => ({ ...(json ? {'Content-Type': 'application/json'} : {}), ...(token() ? {Authorization: `Bearer ${token()}`} : {}) });
  const api = async (path, options = {}) => {
    const response = await fetch(`/api/v1${path}`, {...options, headers: {...headers(options.body), ...(options.headers || {})}});
    const body = response.status === 204 ? {} : await response.json();
    if (!response.ok) throw new Error(body.error?.message || `${response.status} ${response.statusText}`);
    return body;
  };
  const show = (id, value) => { byId(id).textContent = JSON.stringify(value, null, 2); };
  const cell = (row, value, className = '') => { const td = row.insertCell(); td.textContent = value ?? ''; td.className = className; };
  const activate = id => {
    document.querySelectorAll('.tabs button').forEach(button => button.classList.toggle('active', button.dataset.tab === id));
    document.querySelectorAll('.view').forEach(view => view.classList.toggle('active', view.id === id));
  };
  document.querySelectorAll('.tabs button').forEach(button => button.addEventListener('click', () => {
    activate(button.dataset.tab);
    button.scrollIntoView({block: 'nearest', inline: 'nearest'});
  }));
  window.addEventListener('resize', () => requestAnimationFrame(() => {
    document.querySelector('.tabs button.active')?.scrollIntoView({block: 'nearest', inline: 'nearest'});
  }));

  async function refreshRuntime() {
    try {
      const [info, health] = await Promise.all([api('/info'), api('/health')]);
      byId('health').textContent = 'Online'; byId('health').classList.add('good');
      byId('metrics').innerHTML = [['Version', info.product_version], ['Channels', health.broker_channels], ['Clients', health.clients], ['History', health.history_events]]
        .map(([label, value]) => `<div class="metric"><strong>${String(value)}</strong><span>${label}</span></div>`).join('');
      show('runtime', {info, health});
    } catch (error) {
      byId('health').textContent = 'Offline'; byId('health').classList.remove('good'); show('runtime', {error: error.message});
    }
  }
  async function refreshChannels() {
    const body = await api('/channels'); const tbody = byId('channel-rows'); tbody.replaceChildren();
    body.channels.forEach(channel => {
      const row = tbody.insertRow(); cell(row, channel.slot); cell(row, channel.name); cell(row, `${channel.serial}/${channel.port}`);
      cell(row, channel.bus); cell(row, channel.mode); cell(row, channel.bus === 'can' ? `${channel.nominal_bitrate_bps}/${channel.data_bitrate_bps}` : channel.nominal_bitrate_bps);
      cell(row, channel.broker ? `${channel.broker.received} rx / ${channel.broker.consumers} users` : 'detached');
    });
  }
  function addMessage(event) {
    const tbody = byId('message-rows'); const row = tbody.insertRow(0);
    cell(row, event.event_id); cell(row, event.timestamp_us); cell(row, event.channel); cell(row, event.bus); cell(row, event.direction);
    cell(row, `0x${Number(event.id).toString(16).toUpperCase()}`); cell(row, event.data);
    while (tbody.rows.length > 500) tbody.deleteRow(tbody.rows.length - 1);
    state.cursor = Math.max(state.cursor, Number(event.event_id || 0));
  }
  async function refreshHistory() {
    const channel = byId('stream-channel').value.trim();
    const body = await api(`/messages/history?after=${state.cursor}&limit=500${channel ? `&channel=${encodeURIComponent(channel)}` : ''}`);
    body.events.forEach(addMessage);
  }
  function stopStreams() {
    state.aborter?.abort(); state.aborter = null; state.socket?.close(); state.socket = null;
    byId('stream-state').textContent = 'Stopped'; byId('stream-state').classList.remove('good');
  }
  async function startSse(path = '/messages/stream', eventName = 'message', handler = addMessage) {
    stopStreams(); state.aborter = new AbortController(); const channel = byId('stream-channel').value.trim();
    const separator = path.includes('?') ? '&' : '?';
    const response = await fetch(`/api/v1${path}${separator}from=${state.cursor || 'now'}${channel && path.startsWith('/messages') ? `&channel=${encodeURIComponent(channel)}` : ''}`, {headers: headers(false), signal: state.aborter.signal});
    if (!response.ok) throw new Error((await response.json()).error?.message || response.statusText);
    byId('stream-state').textContent = `SSE ${eventName}`; byId('stream-state').classList.add('good');
    const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffered = '';
    while (true) {
      const {value, done} = await reader.read(); if (done) break; buffered += decoder.decode(value, {stream: true});
      let split; while ((split = buffered.indexOf('\n\n')) >= 0) {
        const block = buffered.slice(0, split); buffered = buffered.slice(split + 2);
        const type = block.match(/^event: (.+)$/m)?.[1]; const data = block.match(/^data: (.+)$/m)?.[1];
        if (type === eventName && data) handler(JSON.parse(data));
      }
    }
  }
  function decodeWs(buffer) {
    const view = new DataView(buffer); if (view.byteLength < 32 || String.fromCharCode(...new Uint8Array(buffer, 0, 4)) !== 'BMF1') return;
    const bus = view.getUint8(5) === 2 ? 'lin' : 'can'; const length = view.getUint8(10);
    const bytes = [...new Uint8Array(buffer, 32, length)].map(value => value.toString(16).padStart(2, '0').toUpperCase()).join('');
    addMessage({event_id: Number(view.getBigUint64(16)), timestamp_us: Number(view.getBigUint64(24)), channel: view.getUint16(8), bus, direction: view.getUint8(6) ? 'tx' : 'rx', id: view.getUint32(12), data: bytes});
  }
  function startWs() {
    stopStreams(); const channel = byId('stream-channel').value.trim(); const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
    const url = `${scheme}://${location.host}/api/v1/messages/ws?from=${state.cursor || 'now'}${channel ? `&channel=${encodeURIComponent(channel)}` : ''}`;
    const protocols = ['bmcli.frames.v1']; if (token()) protocols.push(`bmcli.token.${token()}`);
    state.socket = new WebSocket(url, protocols); state.socket.binaryType = 'arraybuffer';
    state.socket.onopen = () => { byId('stream-state').textContent = 'WebSocket'; byId('stream-state').classList.add('good'); };
    state.socket.onmessage = event => { if (event.data instanceof ArrayBuffer) decodeWs(event.data); };
    state.socket.onclose = () => { byId('stream-state').textContent = 'Stopped'; byId('stream-state').classList.remove('good'); };
  }
  async function refreshPlans() {
    const body = await api('/signals'); const tbody = byId('plan-rows'); tbody.replaceChildren();
    body.plans.forEach(plan => { const row = tbody.insertRow(); cell(row, plan.plan_id); cell(row, plan.database); cell(row, plan.revision); cell(row, plan.message); cell(row, plan.channel); cell(row, plan.decoded_frames); cell(row, plan.decode_errors); });
  }
  async function refreshContract() {
    const body = await api('/operations'); const tbody = byId('operation-rows'); tbody.replaceChildren();
    body.operations.forEach(operation => { const row = tbody.insertRow(); cell(row, operation.operation_id); cell(row, operation.permission); cell(row, operation.execution); cell(row, operation.http_policy); cell(row, operation.migrated ? 'yes' : 'no', operation.migrated ? 'yes' : 'no'); cell(row, operation.reason); });
  }
  const modbusContext = () => {
    const resource = Object.fromEntries(new FormData(byId('modbus-form')));
    const poll = Object.fromEntries(new FormData(byId('modbus-poll-form')));
    const common = {database: resource.database, channel: resource.channel};
    if (resource.project) common.project = resource.project;
    return {resource, poll, common};
  };
  const modbusRun = async (target, work) => {
    try { show(target, await work()); } catch (error) { show(target, {error: error.message}); }
  };

  byId('refresh').onclick = () => Promise.allSettled([refreshRuntime(), refreshChannels()]);
  byId('channels-refresh').onclick = () => refreshChannels().catch(error => alert(error.message));
  byId('history-refresh').onclick = () => refreshHistory().catch(error => alert(error.message));
  byId('sse-start').onclick = () => startSse().catch(error => { if (error.name !== 'AbortError') alert(error.message); });
  byId('ws-start').onclick = startWs; byId('stream-stop').onclick = stopStreams;
  byId('plans-refresh').onclick = () => refreshPlans().catch(error => alert(error.message));
  byId('contract-refresh').onclick = () => refreshContract().catch(error => alert(error.message));
  byId('modbus-list').onclick = () => modbusRun('modbus-result', async () => {
    const {common} = modbusContext();
    return api(`/modbus/resources?database=${encodeURIComponent(common.database)}`);
  });
  byId('modbus-read').onclick = () => modbusRun('modbus-result', async () => {
    const {resource, common} = modbusContext();
    return api(`/modbus/resources/${encodeURIComponent(resource.resource)}/read`, {method:'POST', body:JSON.stringify(common)});
  });
  byId('modbus-write').onclick = () => modbusRun('modbus-result', async () => {
    const {resource, common} = modbusContext();
    return api(`/modbus/resources/${encodeURIComponent(resource.resource)}`, {method:'PUT', body:JSON.stringify({...common, value:resource.value})});
  });
  byId('modbus-poll-start').onclick = () => modbusRun('modbus-poll-result', async () => {
    const {resource, poll, common} = modbusContext();
    return api('/modbus/polls', {method:'POST', body:JSON.stringify({...common, resource:resource.resource, job:poll.job, interval:Number(poll.interval), stale_after:Number(poll.stale_after), on_change:poll.on_change === 'true'})});
  });
  byId('modbus-poll-stop').onclick = () => modbusRun('modbus-poll-result', async () => {
    const {poll} = modbusContext(); return api(`/modbus/polls/${encodeURIComponent(poll.job)}`, {method:'DELETE'});
  });
  byId('modbus-poll-status').onclick = () => modbusRun('modbus-poll-result', async () => {
    const {poll} = modbusContext(); return api(`/modbus/polls?job=${encodeURIComponent(poll.job)}`);
  });
  byId('modbus-history').onclick = () => modbusRun('modbus-poll-result', async () => {
    const {poll} = modbusContext(); return api(`/modbus/values/${encodeURIComponent(poll.job)}/history?count=100`);
  });
  byId('send-form').onsubmit = async event => {
    event.preventDefault(); const values = Object.fromEntries(new FormData(event.target)); values.timeout_ms = Number(values.timeout_ms); values.id = values.id.startsWith('0x') ? values.id : Number(values.id);
    try { show('send-result', await api('/messages', {method: 'POST', body: JSON.stringify(values), headers: {'Idempotency-Key': crypto.randomUUID()}})); } catch (error) { show('send-result', {error: error.message}); }
  };
  byId('plan-form').onsubmit = async event => {
    event.preventDefault(); const values = Object.fromEntries(new FormData(event.target)); values.history_capacity = Number(values.history_capacity);
    try { const result = await api('/signals/plans', {method: 'POST', body: JSON.stringify(values)}); show('plan-result', result); byId('signal-plan').value = result.plan_id; await refreshPlans(); } catch (error) { show('plan-result', {error: error.message}); }
  };
  byId('api-form').onsubmit = async event => {
    event.preventDefault();
    const values = Object.fromEntries(new FormData(event.target));
    const method = values.method;
    const path = values.path.startsWith('/') ? values.path : `/${values.path}`;
    const options = {method};
    if (method !== 'GET' && method !== 'DELETE') {
      try { options.body = JSON.stringify(JSON.parse(values.body || '{}')); }
      catch (error) { show('api-result', {error: `Invalid JSON: ${error.message}`}); return; }
    }
    try { show('api-result', await api(path, options)); }
    catch (error) { show('api-result', {error: error.message}); }
  };
  byId('signal-latest').onclick = async () => { try { show('signal-result', await api(`/signals/latest?plan=${encodeURIComponent(byId('signal-plan').value)}&signal=${encodeURIComponent(byId('signal-name').value)}`)); } catch (error) { show('signal-result', {error: error.message}); } };
  byId('signal-history').onclick = async () => { try { show('signal-result', await api(`/signals/history?plan=${encodeURIComponent(byId('signal-plan').value)}&signal=${encodeURIComponent(byId('signal-name').value)}&after=0`)); } catch (error) { show('signal-result', {error: error.message}); } };
  byId('signal-stream').onclick = () => startSse(`/signals/stream?plan=${encodeURIComponent(byId('signal-plan').value)}&signal=${encodeURIComponent(byId('signal-name').value)}`, 'signal', sample => show('signal-result', sample)).catch(error => { if (error.name !== 'AbortError') show('signal-result', {error: error.message}); });
  refreshRuntime(); refreshChannels().catch(() => {}); refreshPlans().catch(() => {}); refreshContract().catch(() => {});
})();
