/* Settings - robot discovery, network, transport and safety limits.

   This is where the "works on any IP" promise lives: the dashboard never stores
   a fixed robot address, it discovers one on whatever network you are currently
   attached to, and it reports every address it is itself reachable on.
*/

import { store, get, post, toast, num } from '../core.js';
import {
  el, card, pageHead, button, note, badge, table, input, select, field,
  emptyState, toggle, range, kv, segmented, setChildren,
} from '../ui.js';

export default {
  id: 'settings',
  label: 'Settings',
  icon: 'settings',

  async render() {
    const root = el('div.stack');
    let config = store.settings?.values || {};
    let network = store.network || {};

    root.appendChild(pageHead(
      'Settings',
      'Connection, transport and safety limits. Changes are saved to config.json immediately.',
    ));

    /* ==================================================================
       Robot connection + discovery
       ================================================================== */

    const hostInput = input({ value: config.robot_host || '', placeholder: 'IP address or hostname' });
    const probeResult = el('div');
    const scanResult = el('div');
    const scanProgress = el('div', { style: { display: 'none' } });

    const saveHost = async (value) => {
      const data = await post('/api/settings', { robot_host: value });
      config = data.settings.values;
      store.settings = data.settings;
      toast('Robot host saved', value || 'cleared',
            'good');
      if (data.needs_restart) restartNote.style.display = '';
    };

    const probeBtn = button('Test', async (node) => {
      const host = hostInput.value.trim();
      if (!host) { hostInput.focus(); return; }
      node.disabled = true;
      probeResult.replaceChildren(el('span.muted', { text: 'Probing…' }));
      try {
        const data = await post('/api/network/probe', { host });
        const info = data.detail || {};
        setChildren(probeResult,
          el('div.row', {},
            badge(data.ok ? 'reachable' : 'no answer', data.ok ? 'good' : 'critical'),
            info.hostname ? badge(info.hostname) : null,
            info.open_ports?.length ? badge(`ports ${info.open_ports.join(', ')}`, 'accent') : null,
            badge(`${num(info.latency_ms, 0)} ms`),
          ),
          info.is_pc1
            ? note('This is PC1, the motion-control unit. The documentation forbids using it as a '
                 + 'build or run host. Point the dashboard at PC2 instead.', 'critical', '⚠')
            : null,
        );
      } catch (err) {
        probeResult.replaceChildren(note(err.message, 'critical', '⚠'));
      } finally {
        node.disabled = false;
      }
    });

    const scanBtn = button('Scan this network', async (node) => {
      node.disabled = true;
      scanProgress.style.display = '';
      scanProgress.replaceChildren(el('span.muted', { text: 'Starting sweep…' }));
      scanResult.replaceChildren();
      try {
        const data = await post('/api/network/scan', {}, );
        const hosts = data.hosts || [];
        scanResult.replaceChildren(
          el('div.row', { style: { marginBottom: '10px' } },
            badge(`${hosts.length} responding`, hosts.length ? 'good' : 'warning'),
            ...(data.networks || []).map((n) => badge(n, 'accent')),
          ),
          hosts.length
            ? table([
                { key: 'host', label: 'Address' },
                { key: 'hostname', label: 'Name', get: (r) => r.hostname || '—' },
                { key: 'ports', label: 'Open ports', get: (r) => r.open_ports.join(', ') },
                { key: 'latency', label: 'Latency', align: 'right', get: (r) => `${num(r.latency_ms, 0)} ms` },
                {
                  key: 'tags', label: '',
                  get: (r) => el('div.row.tight', {},
                    r.is_self ? badge('this machine', 'default') : null,
                    r.is_pc1 ? badge('PC1 — do not use', 'critical') : null,
                  ),
                },
                {
                  key: 'use', label: '',
                  get: (r) => button('Use', async () => {
                    hostInput.value = r.host;
                    await saveHost(r.host);
                  }, { size: 'sm', style: r.is_pc1 ? 'warn' : 'primary' }),
                },
              ], hosts)
            : emptyState('Nothing answered',
                         'No host on this subnet responded on the ports an X2 exposes. '
                       + 'Check that the robot is powered and on the same network.'),
        );
      } catch (err) {
        scanResult.replaceChildren(note(err.message, 'critical', '⚠'));
      } finally {
        node.disabled = false;
        scanProgress.style.display = 'none';
      }
    }, { iconName: 'scan', style: 'primary' });

    const restartNote = el('div', { style: { display: 'none' } },
      note('Transport settings changed. Restart the bridge to apply them.', 'warning', '⚠'),
    );

    const restartBtn = button('Restart bridge', async (node) => {
      node.disabled = true;
      try {
        const data = await post('/api/bridge/restart');
        toast(data.ok ? 'Bridge restarted' : 'Restart failed', data.message,
              data.ok ? 'good' : 'critical');
        if (data.ok) {
          restartNote.style.display = 'none';
          const boot = await get('/api/bootstrap');
          store.bridge = boot.bridge;
          store.state = boot.state;
        }
      } catch (err) {
        toast('Restart failed', err.message, 'critical');
      } finally {
        node.disabled = false;
      }
    }, { iconName: 'refresh' });

    root.appendChild(card('Robot connection', { sub: 'No fixed address is stored' },
      el('div.stack', {},
        field('Robot host',
          el('div.row', { style: { gap: '8px', flexWrap: 'nowrap' } },
            hostInput,
            probeBtn,
            button('Save', () => saveHost(hostInput.value.trim())),
          ),
          'Leave empty to rely on ROS 2 DDS multicast discovery, which finds the robot on the local '
          + 'segment without an address. Set it when you want the dashboard to report and probe a '
          + 'specific host.',
        ),
        probeResult,
        el('div.row.between', {},
          el('span', { text: 'Find the robot automatically', style: { fontSize: '12px', color: 'var(--text-2)' } }),
          scanBtn,
        ),
        scanProgress,
        scanResult,
        note('The sweep covers whichever subnets this machine is on right now. Move to a different '
           + 'Wi-Fi network and scan again — nothing is cached.', 'info'),

        el('div', { style: { borderTop: '1px solid var(--border-soft)', margin: '4px 0 2px' } }),

        toggle('Find the robot automatically if its address changes', config.auto_discover,
               (value) => saveSetting('auto_discover', value)),

        toggle('Start the agent over SSH if the robot is on but not answering',
               config.auto_start_agent,
               (value) => saveSetting('auto_start_agent', value)),

        el('div.grid.cols-2', { style: { gap: '10px' } },
          field('SSH user',
            input({
              value: config.robot_ssh_user,
              onchange: (e) => saveSetting('robot_ssh_user', e.target.value),
            }),
          ),
          field('SSH password',
            input({
              type: 'password',
              value: config.robot_ssh_password,
              placeholder: config.robot_ssh_password ? '' : 'not saved',
              onchange: (e) => saveSetting('robot_ssh_password', e.target.value),
            }),
          ),
        ),

        note('The robot cannot start the dashboard agent by itself: the agi account is not '
           + 'allowed to enable systemd lingering, and the robot\'s clock jumps backwards after '
           + 'boot, which stalls cron. With these credentials the dashboard logs in and starts '
           + 'it for you after a power cycle. Stored in config.json on this machine.',
           'warning', '⚠'),

        restartNote,
      ),
    ));

    /* ==================================================================
       Dashboard reachability
       ================================================================== */

    const urlHost = el('div');

    function paintUrls() {
      const urls = network.urls || [];

      const primary = network.address?.ip
        ? `http://${network.address.ip}:${network.port}` : null;

      const describe = (url) => {
        if (url === primary) return { label: 'use this — works everywhere', tone: 'good' };
        if (url.includes('localhost') || url.includes('127.0.0.1')) {
          return { label: 'this machine only', tone: 'default' };
        }
        // Names need mDNS, which many access points block between clients.
        if (!/\/\/\d+\.\d+\.\d+\.\d+/.test(url)) {
          return { label: 'needs mDNS — often blocked on Wi-Fi', tone: 'warning' };
        }
        return { label: 'other adapter', tone: 'accent' };
      };

      urlHost.replaceChildren(
        el('div.stack', { style: { gap: '8px' } },
          ...urls.map((url) => {
            const meta = describe(url);
            return el('div.row.between', {
              style: {
                padding: '10px 13px', background: 'var(--surface-2)',
                border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
              },
            },
              el('span', { text: url, style: { fontFamily: 'var(--mono)', fontSize: '12.5px' } }),
              el('div.row.tight', {},
                badge(meta.label, meta.tone),
                button('Copy', async () => {
                  try {
                    await navigator.clipboard.writeText(url);
                    toast('Copied', url, 'good', 1800);
                  } catch {
                    toast('Copy blocked', 'Select and copy the address manually.', 'warning');
                  }
                }, { size: 'sm', style: 'ghost' }),
              ),
            );
          }),
        ),
      );
    }

    const nameHost = el('div');

    function paintName() {
      const adv = network.advertised;
      setChildren(nameHost,
        field('Dashboard name on the network',
          el('div.row', { style: { gap: '8px', flexWrap: 'nowrap' } },
            input({
              value: config.dashboard_name,
              placeholder: 'agibot',
              onchange: (e) => saveSetting('dashboard_name', e.target.value.trim().toLowerCase()),
            }),
            adv?.running ? badge('publishing', 'good')
              : adv?.conflict ? badge('name taken', 'critical')
              : badge('not publishing', 'warning'),
          ),
          'The dashboard publishes this over mDNS, so the link is about the robot rather than '
          + 'this PC. Takes effect when the server restarts.',
        ),
        adv?.error ? el('div', { style: { marginTop: '8px' } }, note(adv.error, 'warning', '⚠')) : null,
      );
    }

    const qrHost = el('div');
    const devicesHost = el('div');

    function paintQr() {
      const detail = network.address || {};

      if (!detail.ip) {
        setChildren(qrHost,
          note(`No network address right now — ${detail.reason_text || 'not connected'}. `
             + 'The dashboard keeps checking and will show the link the moment the '
             + 'network is back.', 'warning', '⚠'),
        );
        return;
      }

      const url = `http://${detail.ip}:${network.port}`;
      setChildren(qrHost,
        el('div.row', { style: { gap: '18px', alignItems: 'center', flexWrap: 'wrap' } },
          el('img', {
            src: `/api/qr?url=${encodeURIComponent(url)}&t=${Date.now()}`,
            alt: `QR code for ${url}`,
            style: {
              width: '156px', height: '156px', background: '#fff',
              padding: '8px', borderRadius: 'var(--radius-sm)',
            },
          }),
          el('div', { style: { flex: '1', minWidth: '220px' } },
            el('div', { text: 'Open this on your phone',
                        style: { fontWeight: '600', marginBottom: '6px' } }),
            el('div', {
              text: url,
              style: { fontFamily: 'var(--mono)', fontSize: '17px', color: 'var(--text)' },
            }),
            (() => {
              const kindLabel = detail.kind === 'wifi' ? 'Wi-Fi' : 'wired';
              const adapter = detail.adapter || '';
              return el('div.row.tight', { style: { marginTop: '8px' } },
                badge(kindLabel, detail.kind === 'wifi' ? 'good' : 'accent'),
                // The adapter is usually just called "Wi-Fi" too - only worth a
                // second badge when it actually says something different.
                adapter && adapter.toLowerCase() !== kindLabel.toLowerCase()
                  ? badge(adapter) : null,
                detail.gateway ? badge(`via ${detail.gateway}`) : null,
                badge('live', 'good'),
              );
            })(),
            el('div.hint', {
              text: 'Read from the network adapter and re-checked continuously — if this PC '
                  + 'gets a new address the link above updates by itself. Scan or type it; '
                  + 'it needs no name resolution, so it works even where .local does not.',
              style: { marginTop: '8px' },
            }),
            el('div.row.tight', { style: { marginTop: '8px' } },
              button('Copy link', async () => {
                try {
                  await navigator.clipboard.writeText(url);
                  toast('Copied', url, 'good', 1800);
                } catch {
                  toast('Copy blocked', 'Select and copy it manually.', 'warning');
                }
              }, { size: 'sm' }),
            ),
          ),
        ),
        (detail.rejected || []).length
          ? el('details', { style: { marginTop: '10px' } },
              el('summary', {
                text: `${detail.rejected.length} other address(es) ignored`,
                style: { cursor: 'pointer', fontSize: '12px', color: 'var(--text-3)' },
              }),
              el('div', { style: { marginTop: '8px' } },
                table([
                  { key: 'ip', label: 'Address' },
                  { key: 'adapter', label: 'Adapter' },
                  { key: 'why', label: 'Why it is not used' },
                ], detail.rejected),
              ),
            )
          : null,
      );
    }

    function paintDevices() {
      const devices = network.other_devices || [];
      setChildren(devicesHost,
        el('div.row', { style: { marginBottom: '8px' } },
          el('strong', { text: 'Devices that have opened this dashboard',
                         style: { fontSize: '12.5px' } }),
          badge(String(devices.length), devices.length ? 'good' : 'warning'),
        ),
        devices.length
          ? table([
              { key: 'address', label: 'Address' },
              { key: 'hits', label: 'Requests', align: 'right' },
              {
                key: 'agent', label: 'Browser',
                get: (r) => {
                  const a = r.agent || '';
                  if (/iPhone|iPad/i.test(a)) return 'iPhone / iPad';
                  if (/Android/i.test(a)) return 'Android';
                  if (/Macintosh/i.test(a)) return 'Mac';
                  if (/Windows/i.test(a)) return 'Windows';
                  return a.slice(0, 28) || '—';
                },
              },
            ], devices)
          : note('Nothing but this PC has loaded the dashboard yet. If your phone appears here '
               + 'after scanning the QR code, the network is fine and only the .local name is '
               + 'being blocked — which is the access point filtering multicast, not something '
               + 'this dashboard can fix.', 'warning', '⚠'),
      );
    }

    root.appendChild(card('Open on another device', {
      sub: 'The server binds every interface',
      actions: [button('Refresh', async () => {
        network = await get('/api/network');
        store.network = network;
        paintUrls();
        paintName();
        paintQr();
        paintDevices();
      }, { size: 'sm', style: 'ghost', iconName: 'refresh' })],
    },
      el('div.stack', {},
        qrHost,
        urlHost,
        nameHost,
        devicesHost,
        note('The .local link goes over mDNS, which iPhone, iPad, Mac, Android 12+ and Windows '
           + 'understand. Many access points block multicast between wireless clients, and then '
           + 'no .local name resolves from a phone no matter what the PC does — scan the QR code '
           + 'instead.', 'info'),
      ),
    ));
    paintUrls();
    paintName();
    paintQr();
    paintDevices();

    // The server pushes a "network" message whenever the detected address
    // changes, so a page left open never shows a link that stopped working.
    const unsubscribeNetwork = store.on('network', (data) => {
      network = { ...network, ...data };
      store.network = network;
      paintUrls();
      paintQr();
      toast('Network address changed',
            data.address?.ip ? `Now at ${data.address.ip}` : 'No network address',
            data.address?.ip ? 'good' : 'warning');
    });

    /* ==================================================================
       Transport
       ================================================================== */

    const transportFields = el('div.stack');

    function paintTransport() {
      const bridge = store.bridge || {};
      const online = store.state?.connection?.online;
      setChildren(transportFields,
        el('div.row', { style: { marginBottom: '4px' } },
          badge(bridge.simulated ? 'Simulation' : online ? 'Live' : 'Robot off',
                bridge.simulated ? 'warning' : online ? 'good' : 'critical'),
          bridge.agent?.agent_version ? badge(`agent ${bridge.agent.agent_version}`, 'accent') : null,
          bridge.agent?.hostname ? badge(bridge.agent.hostname) : null,
        ),
        bridge.simulated
          ? note('No robot address is set, so the dashboard is driving the built-in simulator. '
               + 'Enter the robot address above and restart the bridge to attach to the real X2.',
               'warning', '⚠')
          : null,
        bridge.error ? note(bridge.error, 'critical', '⚠') : null,

        field('Bridge mode',
          select([
            { value: 'auto', label: 'Auto — use the robot agent when an address is set' },
            { value: 'agent', label: 'Robot only — never fall back to simulation' },
            { value: 'mock', label: 'Simulation only — never touch a real robot' },
          ], {
            value: config.bridge_mode,
            onChange: (value) => saveSetting('bridge_mode', value),
          }),
        ),

        field('Agent port',
          input({
            type: 'number', min: 1, max: 65535, value: config.agent_port,
            onchange: (e) => saveSetting('agent_port', Number(e.target.value)),
          }),
          'The TCP port x2_agent.py listens on aboard the robot.',
        ),

        field('ROS_DOMAIN_ID',
          input({
            type: 'number', min: 0, max: 232, value: config.ros_domain_id,
            onchange: (e) => saveSetting('ros_domain_id', Number(e.target.value)),
          }),
          'Reference only — the agent inherits the domain from the shell that launched it.',
        ),

        el('div.row', {}, restartBtn),
      );
    }

    /* ==================================================================
       Safety and server
       ================================================================== */

    const limits = store.spec?.velocity_limits || {};

    const safetyCard = card('Safety limits', { sub: 'Applied server-side to every command' },
      el('div.stack', {},
        el('div', {},
          el('div', { text: 'Max forward velocity', style: labelStyle }),
          range({
            min: 0.1, max: limits.forward?.max ?? 0.8, step: 0.05,
            value: config.max_forward_velocity, precision: 2, unit: ' m/s',
            onChange: (v) => saveSetting('max_forward_velocity', v),
          }),
        ),
        el('div', {},
          el('div', { text: 'Max lateral velocity', style: labelStyle }),
          range({
            min: 0.1, max: limits.lateral?.max ?? 0.7, step: 0.05,
            value: config.max_lateral_velocity, precision: 2, unit: ' m/s',
            onChange: (v) => saveSetting('max_lateral_velocity', v),
          }),
        ),
        el('div', {},
          el('div', { text: 'Max yaw rate', style: labelStyle }),
          range({
            min: 0.1, max: limits.angular?.max ?? 0.8, step: 0.05,
            value: config.max_angular_velocity, precision: 2, unit: ' rad/s',
            onChange: (v) => saveSetting('max_angular_velocity', v),
          }),
        ),
        el('div', {},
          el('div', { text: 'Dead-man timeout', style: labelStyle }),
          range({
            min: 0.2, max: 3, step: 0.1,
            value: config.locomotion_deadman_s, precision: 1, unit: ' s',
            onChange: (v) => saveSetting('locomotion_deadman_s', v),
          }),
          el('div.hint', { text: 'Velocity is zeroed if the browser stops sending for this long.' }),
        ),
        toggle('Confirm before zero-torque', config.require_confirm_zero_torque,
               (value) => saveSetting('require_confirm_zero_torque', value)),
      ),
    );

    const serverCard = card('Server', { sub: store.settings?.config_path || 'config.json' },
      el('div.stack', {},
        field('Display name',
          input({
            value: config.robot_label,
            onchange: (e) => saveSetting('robot_label', e.target.value),
          }),
        ),
        field('HTTP port',
          input({
            type: 'number', min: 1, max: 65535, value: config.port,
            onchange: (e) => saveSetting('port', Number(e.target.value)),
          }),
          'Takes effect the next time the server starts.',
        ),
        el('div', {},
          el('div', { text: 'Telemetry rate', style: labelStyle }),
          range({
            min: 1, max: 30, step: 1, value: config.telemetry_hz, precision: 0, unit: ' Hz',
            onChange: (v) => saveSetting('telemetry_hz', v),
          }),
          el('div.hint', { text: 'How often state is pushed to the browser. Lower it on a weak link.' }),
        ),
        el('div', { style: { marginTop: '4px' } },
          kv([
            ['Server version', store.server?.version || '—'],
            ['Connected clients', String(store.server?.clients ?? '—')],
            ['Config file', store.settings?.config_path || '—'],
          ]),
        ),
      ),
    );

    root.appendChild(el('div.grid.cols-2', {},
      card('Transport', { sub: 'How the dashboard reaches the robot' }, transportFields),
      safetyCard,
    ));
    root.appendChild(serverCard);

    paintTransport();

    /* ==================================================================
       Helpers
       ================================================================== */

    async function saveSetting(key, value) {
      try {
        const data = await post('/api/settings', { [key]: value });
        config = data.settings.values;
        store.settings = data.settings;
        if (data.needs_restart) restartNote.style.display = '';
        toast('Saved', `${key} = ${value}`, 'good', 1800);
      } catch (err) {
        toast('Could not save', err.message, 'critical');
      }
    }

    const unsubscribe = store.on('scan_progress', (data) => {
      scanProgress.replaceChildren(
        el('div', {},
          el('div.row.between', { style: { fontSize: '12px', color: 'var(--text-2)', marginBottom: '5px' } },
            el('span', { text: 'Sweeping subnet' }),
            el('span', { text: `${data.done} / ${data.total}` }),
          ),
          el('div.meter', {},
            el('div.meter-fill', { style: { width: `${(data.done / data.total) * 100}%` } }),
          ),
        ),
      );
    });

    return {
      node: root,
      dispose: () => { unsubscribe(); unsubscribeNetwork(); },
    };
  },
};

const labelStyle = { fontSize: '12px', color: 'var(--text-2)', marginBottom: '5px' };
