/* Extensions - renders whatever plugins the backend found.

   Nothing here knows about any specific plugin. Each control in the manifest
   maps to a widget, and using it POSTs to /api/plugins/<id>/<control>. Add a
   Python file, press Reload, and it appears.
*/

import { store, get, post, command, toast, num, confirmDialog } from '../core.js';
import {
  el, card, pageHead, button, note, badge, range, select, input, textarea,
  emptyState, stat, field, kv,
} from '../ui.js';
import { sparkline } from '../charts.js';

export default {
  id: 'extensions',
  label: 'Extensions',
  icon: 'extensions',

  async render() {
    const root = el('div.stack');
    const body = el('div.stack');

    const reloadBtn = button('Reload extensions', async () => {
      reloadBtn.disabled = true;
      reloadBtn.querySelector('.ico')?.classList.add('spin');
      try {
        const data = await post('/api/plugins/reload');
        store.plugins = data.manifest;
        toast('Extensions reloaded',
              `${data.summary.count} loaded${data.summary.errors.length ? `, ${data.summary.errors.length} failed` : ''}`,
              data.summary.errors.length ? 'warning' : 'good');
        paint();
      } catch (err) {
        toast('Reload failed', err.message, 'critical');
      } finally {
        reloadBtn.disabled = false;
        reloadBtn.querySelector('.ico')?.classList.remove('spin');
      }
    }, { iconName: 'refresh', style: 'primary' });

    root.appendChild(pageHead(
      'Extensions',
      'Custom controls you have added. Drop a Python file into the plugins folder and reload — '
      + 'no frontend changes needed.',
      [reloadBtn],
    ));

    root.appendChild(body);

    /* -- Rendering -------------------------------------------------------- */

    const readoutNodes = new Map();   // `${pluginId}.${key}` -> { value, spark }
    let seriesCache = {};

    function paint() {
      const manifest = store.plugins || { plugins: [], errors: [] };
      body.replaceChildren();
      readoutNodes.clear();

      /* How-to, always visible so the workflow is discoverable. */
      body.appendChild(card('How to add a control', { sub: manifest.directory || 'backend/plugins/' },
        el('div.stack', { style: { gap: '10px' } },
          el('p', {
            text: 'Create a file in the plugins folder, build a Plugin, decorate a handler. '
                + 'It becomes a card on this page the moment you press Reload.',
            style: { margin: '0', color: 'var(--text-2)', fontSize: '13px' },
          }),
          el('pre', {
            style: {
              margin: '0', padding: '13px 15px', background: 'var(--bg)',
              border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
              fontFamily: 'var(--mono)', fontSize: '11.5px', lineHeight: '1.65',
              overflowX: 'auto', color: 'var(--text-2)',
            },
            text: `from backend.plugin_api import Plugin

plugin = Plugin(id="my_tool", name="My tool", icon="⚙")

@plugin.action("go", label="Do the thing", style="primary")
async def go(ctx):
    await ctx.bridge.play_preset(motion=1002, area=2)
    return "Done"

@plugin.slider("speed", label="Speed", min=0, max=0.6, default=0.2, unit="m/s")
async def speed(ctx, value):
    ctx.storage["speed"] = value
    return f"Speed {value:.2f}"

plugin.readout("speed", "Current speed", unit="m/s", chart=True)`,
          }),
          el('div.row.tight', {},
            badge('action → button'), badge('slider → range'), badge('toggle → switch'),
            badge('select → dropdown'), badge('text → input'), badge('number → stepper'),
            badge('color → picker'), badge('readout → stat tile'),
          ),
          note('ctx gives you .bridge (every robot command), .state (live telemetry), '
             + '.storage (persists across calls), .log(), .push() and .record().', 'info'),
        ),
      ));

      /* Load errors */
      if (manifest.errors?.length) {
        body.appendChild(card('Extensions that failed to load', {},
          el('div.stack', { style: { gap: '10px' } },
            ...manifest.errors.map((error) => el('div', {
              style: {
                padding: '12px 14px', borderRadius: 'var(--radius-sm)',
                background: 'rgba(208,59,59,.07)', border: '1px solid rgba(208,59,59,.3)',
              },
            },
              el('div.row.between', { style: { marginBottom: '6px' } },
                el('strong', { text: error.file, style: { fontSize: '13px' } }),
                badge('failed', 'critical'),
              ),
              el('div', { text: error.error, style: { fontSize: '12px', color: 'var(--text-2)' } }),
              error.trace ? el('pre', {
                text: error.trace,
                style: {
                  margin: '8px 0 0', fontSize: '10.5px', fontFamily: 'var(--mono)',
                  color: 'var(--text-3)', whiteSpace: 'pre-wrap', maxHeight: '150px',
                  overflowY: 'auto',
                },
              }) : null,
            )),
          ),
        ));
      }

      /* Plugin cards */
      if (!manifest.plugins?.length) {
        body.appendChild(emptyState(
          'No extensions loaded yet',
          'The plugins folder has no usable files. Copy _template.py to a new name to start.',
        ));
        return;
      }

      const grid = el('div.grid.cols-2');
      for (const plugin of manifest.plugins) grid.appendChild(renderPlugin(plugin));
      body.appendChild(grid);

      paintReadouts();
    }

    function renderPlugin(plugin) {
      const controls = el('div.stack', { style: { gap: '13px' } });

      for (const control of plugin.controls) {
        const node = renderControl(plugin, control);
        if (node) controls.appendChild(node);
      }

      const readouts = el('div.grid.cols-2', { style: { gap: '9px' } });
      for (const readout of plugin.readouts || []) {
        const valueNode = el('div.stat-value', { text: '—' });
        const sparkHost = readout.chart ? el('div.stat-spark') : null;
        const tile = el('div.stat', {},
          el('div.stat-label', { text: readout.label }),
          valueNode,
          sparkHost,
        );
        readoutNodes.set(`${plugin.id}.${readout.key}`, { readout, valueNode, sparkHost });
        readouts.appendChild(tile);
      }

      return card(plugin.name, {
        sub: plugin.description,
        actions: [
          el('span', { text: plugin.icon, style: { fontSize: '16px' } }),
          plugin.has_tick ? badge(`ticks ${plugin.tick_interval}s`, 'accent') : null,
        ].filter(Boolean),
      },
        el('div.stack', {},
          plugin.readouts?.length ? readouts : null,
          plugin.controls.length ? controls
            : note('This extension declares no controls.', 'default'),
        ),
      );
    }

    function renderControl(plugin, control) {
      const path = `/api/plugins/${plugin.id}/${control.key}`;

      const run = async (value, node) => {
        if (control.confirm) {
          const ok = await confirmDialog(control.label, control.confirm,
                                         { danger: control.style === 'danger' });
          if (!ok) return;
        }
        if (node) node.disabled = true;
        try {
          const data = await post(path, { value });
          toast(control.label, data?.message || 'Done', data?.ok === false ? 'critical' : 'good');
        } catch (err) {
          toast(control.label, err.message, 'critical');
        } finally {
          if (node) node.disabled = false;
        }
      };

      switch (control.kind) {
        case 'action': {
          const btn = button(control.label, (node) => run(null, node), {
            style: control.style || 'default',
            title: control.help || '',
            block: true,
          });
          return control.help
            ? el('div', {}, btn, el('div.hint', { text: control.help, style: { marginTop: '4px' } }))
            : btn;
        }

        case 'slider': {
          const control_ = range({
            min: control.min, max: control.max, step: control.step,
            value: control.default, unit: control.unit ? ` ${control.unit}` : '',
            precision: decimalsFor(control.step),
            onInput: control.live ? throttle((v) => run(v), 200) : undefined,
            onChange: control.live ? undefined : (v) => run(v),
          });
          return field(control.label, control_, control.help);
        }

        case 'toggle': {
          const box = el('input', { type: 'checkbox', checked: control.default });
          box.addEventListener('change', () => run(box.checked));
          return el('div', {},
            el('label.switch', {}, box, el('span.switch-track'),
               el('span.switch-label', { text: control.label })),
            control.help ? el('div.hint', { text: control.help, style: { marginTop: '4px' } }) : null,
          );
        }

        case 'select': {
          const node = select(control.options || [], {
            value: control.default,
            onChange: (value) => run(value),
          });
          return field(control.label, node, control.help);
        }

        case 'text': {
          const box = control.multiline
            ? textarea({ placeholder: control.placeholder || '', value: control.default || '' })
            : input({ placeholder: control.placeholder || '', value: control.default || '' });
          const submit = button(control.submit_label || 'Send', (node) => {
            const value = box.value;
            if (!value.trim()) { box.focus(); return; }
            run(value, node).then(() => { box.value = ''; });
          }, { style: 'primary', size: 'sm' });
          if (!control.multiline) {
            box.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit.click(); });
          }
          return field(control.label,
            el('div.row', { style: { gap: '8px', flexWrap: 'nowrap' } }, box, submit),
            control.help);
        }

        case 'number': {
          const box = input({
            type: 'number', value: control.default,
            min: control.min ?? undefined, max: control.max ?? undefined, step: control.step,
          });
          box.addEventListener('change', () => run(Number(box.value)));
          return field(
            control.unit ? `${control.label} (${control.unit})` : control.label,
            box, control.help,
          );
        }

        case 'color': {
          const box = el('input.input', { type: 'color', value: control.default || '#3987e5' });
          box.addEventListener('change', () => run(box.value));
          return field(control.label, box, control.help);
        }

        default:
          return note(`Unsupported control type "${control.kind}"`, 'warning', '⚠');
      }
    }

    /* -- Readouts --------------------------------------------------------- */

    function paintReadouts() {
      const custom = store.state?.custom || {};

      for (const [key, entry] of readoutNodes) {
        const [pluginId, readoutKey] = splitOnce(key, '.');
        const value = custom[pluginId]?.[readoutKey];
        const { readout, valueNode, sparkHost } = entry;

        if (value === undefined || value === null) {
          valueNode.textContent = '—';
        } else if (readout.format === 'text' || typeof value === 'string') {
          valueNode.textContent = String(value);
          valueNode.style.fontSize = '17px';
        } else {
          valueNode.textContent = num(value, readout.precision ?? 2);
          valueNode.style.fontSize = '';
          if (readout.unit) valueNode.appendChild(el('span.unit', { text: readout.unit }));
        }

        if (sparkHost) {
          const points = seriesCache[`${pluginId}.${readoutKey}`] || [];
          sparkHost.replaceChildren(
            points.length > 2 ? sparkline(points, { height: 30, colorIndex: 2 }) : el('span'),
          );
        }
      }
    }

    async function refreshSeries() {
      const wanted = [...readoutNodes.entries()]
        .filter(([, entry]) => entry.readout.chart)
        .map(([key]) => key);
      if (!wanted.length) return;
      try {
        seriesCache = await get(`/api/series?keys=${wanted.join(',')}&limit=120`);
      } catch { /* best-effort */ }
    }

    paint();
    await refreshSeries();
    paintReadouts();

    const unsubPlugins = store.on('plugins', () => paint());
    let last = 0;
    const unsubState = store.on('state', () => {
      const now = Date.now();
      if (now - last < 900) return;
      last = now;
      paintReadouts();
    });
    const timer = setInterval(() => refreshSeries().then(paintReadouts), 4000);

    return {
      node: root,
      dispose: () => { unsubPlugins(); unsubState(); clearInterval(timer); },
    };
  },
};

function decimalsFor(step) {
  const text = String(step);
  const dot = text.indexOf('.');
  return dot === -1 ? 0 : Math.min(4, text.length - dot - 1);
}

function splitOnce(text, separator) {
  const index = text.indexOf(separator);
  return index === -1 ? [text, ''] : [text.slice(0, index), text.slice(index + 1)];
}

function throttle(fn, ms) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last < ms) return;
    last = now;
    fn(...args);
  };
}
