/* ==========================================================================
   Application shell: bootstrap, rail, routing, topbar, global shortcuts.
   ========================================================================== */

import { store, socket, get, post, toast, num, batteryTone, confirmDialog } from './core.js';
import { el, clear, icon, emptyState, button, badge } from './ui.js';

import overview from './tabs/overview.js';
import control from './tabs/control.js';
import motion from './tabs/motion.js';
import sensors from './tabs/sensors.js';
import vision from './tabs/vision.js';
import lidar from './tabs/lidar.js';
import model from './tabs/model.js';
import interaction from './tabs/interaction.js';
import power from './tabs/power.js';
import navigation from './tabs/navigation.js';
import extensions from './tabs/extensions.js';
import consoleTab from './tabs/console.js';
import settings from './tabs/settings.js';

const TABS = [
  { ...overview,    group: 'Monitor' },
  { ...power,       group: 'Monitor' },
  { ...sensors,     group: 'Monitor' },
  { ...vision,      group: 'Monitor' },
  { ...lidar,       group: 'Monitor' },
  { ...model,       group: 'Monitor' },
  { ...control,     group: 'Operate' },
  { ...motion,      group: 'Operate' },
  { ...interaction, group: 'Operate' },
  { ...navigation,  group: 'Operate' },
  { ...extensions,  group: 'System' },
  { ...consoleTab,  group: 'System' },
  { ...settings,    group: 'System' },
];

// Report which build this bundle came from. index.html (always served fresh)
// compares it against the server's build and warns if they differ, which is the
// only reliable way to notice that a browser served a stale cached bundle.
try {
  window.__MAIN_BUILD__ = new URL(import.meta.url).searchParams.get('b') || 'dev';
} catch { /* non-critical */ }

const content = document.getElementById('content');
const rail = document.getElementById('rail');

let activeId = null;
let activeDispose = null;

/* -- Routing -------------------------------------------------------------- */

function buildRail() {
  clear(rail);
  let lastGroup = null;

  for (const tab of TABS) {
    if (tab.group !== lastGroup) {
      rail.appendChild(el('div.rail-group', { text: tab.group }));
      lastGroup = tab.group;
    }
    const item = el('button.rail-item', {
      type: 'button',
      dataset: { tab: tab.id },
      'aria-current': tab.id === activeId ? 'page' : 'false',
      onclick: () => navigate(tab.id),
    },
      icon(tab.icon),
      el('span', { text: tab.label }),
    );
    rail.appendChild(item);
  }
}

function markActive() {
  for (const item of rail.querySelectorAll('.rail-item')) {
    item.setAttribute('aria-current', item.dataset.tab === activeId ? 'page' : 'false');
  }
}

export function setRailBadge(tabId, value, tone = 'default') {
  const item = rail.querySelector(`.rail-item[data-tab="${tabId}"]`);
  if (!item) return;
  let badge = item.querySelector('.rail-badge');
  if (!value) { badge?.remove(); return; }
  if (!badge) {
    badge = el('span.rail-badge');
    item.appendChild(badge);
  }
  badge.textContent = String(value);
  badge.dataset.tone = tone;
}

async function navigate(id, { replace = false } = {}) {
  const tab = TABS.find((t) => t.id === id) || TABS[0];
  if (activeDispose) { try { activeDispose(); } catch (err) { console.error(err); } }
  activeDispose = null;
  activeId = tab.id;

  markActive();
  if (replace) history.replaceState({ tab: tab.id }, '', `#${tab.id}`);
  else if (location.hash.slice(1) !== tab.id) history.pushState({ tab: tab.id }, '', `#${tab.id}`);

  clear(content);
  content.scrollTop = 0;

  try {
    const result = await tab.render();
    if (result && result.node) {
      content.appendChild(result.node);
      activeDispose = result.dispose || null;
    } else if (result instanceof Node) {
      content.appendChild(result);
    }
  } catch (err) {
    console.error(`Tab "${tab.id}" failed:`, err);
    const stale = window.__MAIN_BUILD__ !== window.__BUILD__;
    content.appendChild(emptyState(
      'This section failed to render',
      stale
        ? `${err?.message || err}\n\nThis page is running an outdated copy of the dashboard, `
          + 'which is very likely the cause. Load the current version and try again.'
        : String(err?.message || err),
      el('div.row', {},
        button('Retry', () => navigate(tab.id, { replace: true }), { style: 'primary' }),
        stale
          ? button('Load the current version',
                   () => location.replace(`${location.pathname}?r=${Date.now()}`))
          : null,
      ),
    ));
  }
}

window.addEventListener('popstate', () => {
  const id = location.hash.slice(1);
  if (id && id !== activeId) navigate(id, { replace: true });
});

/* -- Topbar --------------------------------------------------------------- */

const pills = {
  connection: document.getElementById('pill-connection'),
  mode: document.getElementById('pill-mode'),
  battery: document.getElementById('pill-battery'),
  host: document.getElementById('pill-host'),
};

function setPill(node, label, { state, level } = {}) {
  node.querySelector('.pill-label').textContent = label;
  if (state) node.querySelector('.dot')?.setAttribute('data-state', state);
  if (level) node.dataset.level = level; else delete node.dataset.level;
}

function updateTopbar() {
  const state = store.state;
  const bridge = store.bridge;

  if (!store.connected) {
    setPill(pills.connection, 'Reconnecting…', { state: 'offline' });
  } else if (bridge?.simulated) {
    setPill(pills.connection, 'Simulation', { state: 'sim' });
    pills.connection.title = 'No robot attached - the dashboard is running against the built-in simulator.';
  } else if (state?.connection?.online) {
    setPill(pills.connection, 'Live', { state: 'online' });
    pills.connection.title = `Connected to the robot agent at ${state.connection.host}`;
  } else {
    setPill(pills.connection, 'Robot off', { state: 'offline' });
    pills.connection.title = state?.connection?.error || 'The robot agent is not reachable.';
  }

  const modeSpec = store.spec?.modes?.find((m) => m.id === state?.mode);
  setPill(pills.mode, modeSpec?.label || state?.mode || '—', {
    level: modeSpec?.danger ? 'critical' : undefined,
  });

  const pct = state?.battery_pct;
  const tone = batteryTone(pct);
  setPill(pills.battery, pct === null || pct === undefined ? '—' : `${num(pct, 0)}%`, {
    level: tone === 'good' ? undefined : tone,
  });

  const host = state?.connection?.host || store.settings?.values?.robot_host;
  setPill(pills.host, host || 'not set');
  pills.host.dataset.optional = '';
}

/* -- Offline gate ---------------------------------------------------------

   The dashboard server runs on this machine, not on the robot, so the page
   stays up when the X2 is switched off. That is the whole point of the gate:
   instead of a dead link, the operator gets "power the robot on", and the
   dashboard reattaches by itself the moment the agent answers again.
   -------------------------------------------------------------------------- */

const gate = {
  root: document.getElementById('gate'),
  title: document.getElementById('gate-title'),
  message: document.getElementById('gate-message'),
  status: document.getElementById('gate-status-text'),
  facts: document.getElementById('gate-facts'),
  hint: document.getElementById('gate-hint'),
  address: document.getElementById('gate-address'),
  hostInput: document.getElementById('gate-host'),
  results: document.getElementById('gate-results'),
  visible: false,
  since: 0,
  poll: null,
  busy: false,
};

function gateReason() {
  // Simulation is a deliberate choice, not an outage - never gate on it.
  if (store.bridge?.simulated) return null;
  if (!store.connected) return 'server';
  if (!store.state?.connection?.online) return 'robot';
  return null;
}

function showGate(reason) {
  const host = store.settings?.values?.robot_host || '—';
  const port = store.settings?.values?.agent_port || 8781;

  if (reason === 'server') {
    gate.title.textContent = 'Dashboard server not responding';
    gate.message.textContent =
      'The page loaded but lost its connection to the dashboard server. '
      + 'It will reconnect automatically once the server is running again.';
    gate.hint.textContent = 'This is the server on this computer, not the robot.';
  } else {
    gate.title.textContent = 'Robot is powered off';
    gate.message.textContent =
      'The dashboard cannot reach the X2. Switch the robot on and this page will '
      + 'connect by itself — no need to reload.';
    gate.hint.textContent =
      'If the robot is already on, check that it is on this Wi-Fi network and that '
      + 'the dashboard agent is running on it.';
  }

  const error = store.state?.connection?.error || store.bridge?.error || '';
  clear(gate.facts);
  const facts = [
    ['Robot address', host === '—' ? 'not set' : `${host}:${port}`],
    ['Last seen', gate.since ? new Date(gate.since).toLocaleTimeString([], { hour12: false }) : 'not yet this session'],
  ];
  if (error) facts.push(['Detail', error]);
  for (const [key, value] of facts) {
    gate.facts.appendChild(el('dt', { text: key }));
    gate.facts.appendChild(el('dd', { text: value }));
  }

  if (!gate.visible) {
    gate.visible = true;
    gate.root.hidden = false;
    document.getElementById('app').setAttribute('inert', '');
    if (!gate.hostInput.value) {
      gate.hostInput.value = store.settings?.values?.robot_host || '';
    }
    if (!gate.poll) gate.poll = setInterval(pollRobot, 2500);
  }
}

function hideGate() {
  if (!gate.visible) return;
  gate.visible = false;
  gate.root.hidden = true;
  document.getElementById('app').removeAttribute('inert');
  if (gate.poll) { clearInterval(gate.poll); gate.poll = null; }
  toast('Robot connected', 'Live telemetry is flowing again.', 'good');
  // Re-render the current tab so it rebuilds against real data.
  if (activeId) navigate(activeId, { replace: true });
}

async function pollRobot() {
  // Anything the operator kicked off owns the status line until it finishes.
  if (gate.busy) return;
  try {
    const status = await get('/api/robot/status');
    if (status.online) {
      gate.status.textContent = 'Robot answered — reconnecting…';
      store.state = store.state || {};
      store.state.connection = { ...(store.state.connection || {}), online: true };
      updateGate();
      return;
    }
    // The bridge reports what its automatic recovery is doing; showing that is
    // far more use than a fixed "waiting" message.
    gate.status.textContent = status.recovery
      || `Waiting for the robot at ${status.host || 'no address set'}…`;

    if (!status.has_ssh_password && status.auto_start_agent) {
      gate.hint.textContent =
        'Tip: save the robot\'s SSH password in Settings and the dashboard can start the '
        + 'agent for you after a power cycle, instead of waiting for the robot to do it.';
    }
  } catch {
    gate.status.textContent = 'Waiting for the dashboard server…';
  }
}

function updateGate() {
  const reason = gateReason();
  if (reason) showGate(reason);
  else {
    if (store.state?.connection?.online) gate.since = Date.now();
    hideGate();
  }
}

document.getElementById('gate-retry').addEventListener('click', async (event) => {
  const button = event.currentTarget;
  button.disabled = true;
  gate.busy = true;
  gate.status.textContent = 'Reconnecting…';
  try {
    await post('/api/bridge/restart');
    gate.busy = false;
    await pollRobot();
  } catch (err) {
    gate.status.textContent = err.message;
  } finally {
    gate.busy = false;
    button.disabled = false;
  }
});

document.getElementById('gate-toggle-address').addEventListener('click', (event) => {
  const shown = !gate.address.hidden;
  gate.address.hidden = shown;
  event.currentTarget.textContent = shown ? 'Set address manually' : 'Hide address box';
  if (!shown) gate.hostInput.focus();
});

async function saveGateHost(host) {
  if (!host) return;
  gate.busy = true;
  gate.status.textContent = `Connecting to ${host}…`;
  try {
    await post('/api/settings', { robot_host: host });
    await post('/api/bridge/restart');
    // Give the bridge a moment to dial before we report anything.
    await new Promise((resolve) => setTimeout(resolve, 2500));
    gate.busy = false;
    await pollRobot();
  } catch (err) {
    gate.status.textContent = `Could not connect: ${err.message}`;
  } finally {
    gate.busy = false;
  }
}

document.getElementById('gate-save').addEventListener('click',
  () => saveGateHost(gate.hostInput.value.trim()));
gate.hostInput.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') saveGateHost(gate.hostInput.value.trim());
});

document.getElementById('gate-find').addEventListener('click', async (event) => {
  const button = event.currentTarget;
  button.disabled = true;
  gate.busy = true;
  gate.results.hidden = true;
  gate.status.textContent = 'Scanning this network for the robot…';

  try {
    const data = await post('/api/robot/find');
    const found = data.candidates || [];
    clear(gate.results);
    gate.results.hidden = !found.length;

    if (!found.length) {
      gate.status.textContent =
        'Nothing found. Is the robot powered on and joined to this Wi-Fi?';
      return;
    }

    gate.status.textContent = `Found ${found.length} device${found.length > 1 ? 's' : ''}.`;
    for (const item of found) {
      const ready = item.agent;
      gate.results.appendChild(el('div.gate-result', {},
        el('span.host', { text: item.host }),
        item.hostname ? el('span', { text: item.hostname, style: { color: 'var(--text-3)' } }) : null,
        badge(ready ? 'agent running' : 'reachable, agent off', ready ? 'good' : 'warning'),
        el('span.spacer'),
        button_(ready ? 'Connect' : 'Start & connect', async (btn) => {
          btn.disabled = true;
          gate.busy = true;
          try {
            if (!ready) {
              gate.status.textContent = `Starting the agent on ${item.host}…`;
              const woke = await post('/api/robot/wake', { host: item.host });
              if (!woke.ok) {
                gate.status.textContent = woke.message;
                return;
              }
            }
            await saveGateHost(item.host);
          } finally {
            gate.busy = false;
            btn.disabled = false;
          }
        }),
      ));
    }
  } catch (err) {
    gate.status.textContent = `Scan failed: ${err.message}`;
  } finally {
    gate.busy = false;
    button.disabled = false;
  }
});

/* Small local button helper - the gate is built before the UI module's styles
   are relevant, and it only needs the one variant. */
function button_(label, onClick) {
  const node = el('button.btn.btn-sm', { type: 'button', text: label });
  node.addEventListener('click', () => onClick(node));
  return node;
}

/* -- Theme ---------------------------------------------------------------- */

function initTheme() {
  const saved = localStorage.getItem('x2-theme');
  if (saved) document.documentElement.dataset.theme = saved;

  document.getElementById('btn-theme').addEventListener('click', () => {
    const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
    document.documentElement.dataset.theme = next;
    localStorage.setItem('x2-theme', next);
    store.emit('theme', next);
    // Charts read their colours from CSS variables, so a re-render picks up the
    // new palette rather than keeping stale hexes.
    if (activeId) navigate(activeId, { replace: true });
  });
}

/* -- Global controls ------------------------------------------------------ */

function initStop() {
  const stop = async () => {
    socket.send('stop', {});
    try {
      await post('/api/stop');
      toast('Stopped', 'Velocity zeroed', 'warning');
    } catch (err) {
      toast('Stop failed', err.message, 'critical');
    }
  };

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

  document.addEventListener('keydown', (event) => {
    const typing = ['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName);

    if (event.code === 'Space' && !typing) {
      event.preventDefault();
      stop();
      return;
    }
    if (typing || event.metaKey || event.ctrlKey || event.altKey) return;

    // Number keys jump between sections.
    const index = Number(event.key) - 1;
    if (Number.isInteger(index) && index >= 0 && index < TABS.length) {
      navigate(TABS[index].id);
    }
  });
}

/* -- Boot ----------------------------------------------------------------- */

async function boot() {
  initTheme();
  initStop();

  try {
    const data = await get('/api/bootstrap');
    store.spec = data.spec;
    store.settings = data.settings;
    store.state = data.state;
    store.plugins = data.plugins;
    store.bridge = data.bridge;
    store.network = data.network;
    store.server = data.server;
    store.events = data.events || [];
  } catch (err) {
    clear(content);
    content.appendChild(emptyState(
      'Cannot reach the dashboard server',
      `${err.message}\n\nThe page loaded but /api/bootstrap did not answer. Check that the server process is still running, then reload.`,
      button('Reload', () => location.reload(), { style: 'primary' }),
    ));
    return;
  }

  const name = store.settings?.values?.robot_label || 'AGIBOT X2';
  document.getElementById('brand-name').textContent = name;
  document.getElementById('brand-sub').textContent =
    store.bridge?.simulated ? 'Simulation mode' : 'Live control';

  buildRail();
  updateTopbar();
  updateGate();

  store.on('state', () => { updateTopbar(); updateGate(); });
  store.on('link', () => { updateTopbar(); updateGate(); });
  store.on('settings', (data) => {
    store.settings = data;
    document.getElementById('brand-name').textContent = data.values.robot_label || 'AGIBOT X2';
    updateTopbar();
  });
  store.on('plugins', (data) => {
    store.plugins = data;
    setRailBadge('extensions', data.errors?.length || 0, 'bad');
  });
  store.on('command_error', (data) => toast('Blocked', data.message, 'warning'));

  setRailBadge('extensions', store.plugins?.errors?.length || 0, 'bad');

  socket.connect();

  const initial = location.hash.slice(1);
  await navigate(TABS.some((t) => t.id === initial) ? initial : TABS[0].id, { replace: true });

  if (store.bridge?.simulated) {
    toast(
      'Simulation mode',
      'No robot address is configured, so the dashboard is driving a simulated X2. '
      + 'Set the address in Settings to attach to the real one.',
      'warning', 9000,
    );
  }
}

boot();

export { navigate, TABS };
