/* * AdminRender — turns admin-canvas JSON into a canvas of AdminWindows. * * A trimmed sibling of digest-engine/render/digest-canvas-sdk/render.js: no * section-list/detail-level resolution (one flat {"windows": [...]} document, not * three sections at two detail levels) and no globe kind (nothing here is a lat/lon * marker). Same degrade-instead-of-throw philosophy as the digest, applied to four * new kinds instead of one — admin-canvas/server.py already rejects malformed * writes before they ever reach output/latest.json, but this is the second, * independent line of defense on the read side: * * - unparseable input -> a
 dump of the raw text
 *   - not a {"windows": [...]}   -> a 
 dump of whatever was there instead
 *   - a window that throws       -> a 
 dump of that window, siblings still render
 *   - a chart/stat with bad data -> the same per-window 
 dump, not a broken canvas
 *
 * A blank page is the one outcome that must never happen — same rule as the digest.
 */

(function (global) {
  'use strict';

  var SVG_NS = 'http://www.w3.org/2000/svg';

  // Mirrors admin-canvas/server.py's SRC_RE exactly. This is defense in depth, not
  // the primary check — the server already refuses to write a 'show' payload whose
  // image/video src doesn't match this shape — but a page that trusted the server
  // unconditionally would have no second line of defense if that check ever
  // regressed. src is always relative to output/, added below.
  var SRC_RE = /^media\/[A-Za-z0-9_.-]+$/;

  var TREND_CLASSES = { up: 'up', down: 'down', flat: 'flat' };

  function rawDump(container, value, note) {
    var pre = document.createElement('pre');
    pre.className = 'admin-canvas-raw';
    var text = typeof value === 'string' ? value : safeStringify(value);
    pre.textContent = (note ? note + '\n\n' : '') + text;
    container.appendChild(pre);
    return pre;
  }

  function safeStringify(value) {
    try {
      return JSON.stringify(value, null, 2);
    } catch (err) {
      return String(value);
    }
  }

  function parse(input) {
    if (typeof input !== 'string') { return input; }
    try {
      return JSON.parse(input);
    } catch (err) {
      return { __unparseable: input };
    }
  }

  function formatStatValue(value) {
    if (typeof value === 'number') {
      return Number.isInteger(value) ? String(value) : value.toFixed(1);
    }
    return value === undefined || value === null ? '—' : String(value);
  }

  function renderStatWindow(container, win) {
    var content = win.content || {};
    var mount = document.createElement('div');
    mount.className = 'admin-stat';

    var valueLine = document.createElement('div');

    var valueSpan = document.createElement('span');
    valueSpan.className = 'admin-stat-value admin-glow';
    valueSpan.textContent = formatStatValue(content.value);
    valueLine.appendChild(valueSpan);

    if (content.unit) {
      var unitSpan = document.createElement('span');
      unitSpan.className = 'admin-stat-unit';
      unitSpan.textContent = String(content.unit);
      valueLine.appendChild(unitSpan);
    }

    if (content.trend) {
      var trend = String(content.trend).toLowerCase();
      var arrow = trend === 'up' ? '▲' : trend === 'down' ? '▼' : '▬';
      var trendSpan = document.createElement('span');
      trendSpan.className = 'admin-stat-trend-' + (TREND_CLASSES[trend] || 'flat');
      trendSpan.textContent = ' ' + arrow;
      valueLine.appendChild(trendSpan);
    }

    mount.appendChild(valueLine);

    if (content.label) {
      var label = document.createElement('div');
      label.className = 'admin-stat-label';
      label.textContent = String(content.label);
      mount.appendChild(label);
    }

    return global.AdminWindow.open({
      title: win.title || '',
      content: mount,
      container: container,
      variant: 'stat',
      x: win.x,
      y: win.y,
      w: win.w,
      h: win.h
    });
  }

  function renderMediaWindow(container, win) {
    var content = win.content || {};
    var src = content.src;
    if (typeof src !== 'string' || !SRC_RE.test(src)) {
      throw new Error((win.kind || 'media') + " window needs content.src matching 'media/'");
    }

    // admin-web (nginx) serves the shared output/ volume this service writes into
    // under the 'output/' path, same convention as digest-web's output/latest.json
    // — the JSON only ever names paths relative to that directory.
    var el = document.createElement(win.kind === 'video' ? 'video' : 'img');
    el.className = 'admin-media';
    el.src = 'output/' + src;

    if (win.kind === 'video') {
      el.controls = true;
      el.muted = content.muted !== false;
      el.loop = !!content.loop;
      el.autoplay = content.autoplay !== false;
      el.playsInline = true;
    } else {
      el.alt = content.alt || win.title || '';
    }

    return global.AdminWindow.open({
      title: win.title || '',
      content: el,
      container: container,
      variant: win.kind,
      x: win.x,
      y: win.y,
      w: win.w,
      h: win.h
    });
  }

  function renderBarChart(mount, content) {
    var points = Array.isArray(content.points) ? content.points : [];
    if (!points.length) { throw new Error('bar chart has no points'); }
    var values = points.map(function (p) { return Number(p && p.value); });
    if (values.some(function (v) { return !isFinite(v); })) {
      throw new Error('bar chart has a non-numeric point value');
    }

    var w = 100, h = 56, padTop = 6, padBottom = 14, gap = 3;
    var barW = (w - gap * (points.length + 1)) / points.length;
    var chartH = h - padTop - padBottom;
    var max = Math.max.apply(null, values.concat([0]));

    var svg = document.createElementNS(SVG_NS, 'svg');
    svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
    svg.setAttribute('class', 'admin-chart-svg');
    svg.setAttribute('role', 'img');

    points.forEach(function (p, i) {
      var value = values[i];
      var barH = max > 0 ? (value / max) * chartH : 0;
      var x = gap + i * (barW + gap);
      var y = padTop + (chartH - barH);

      var rect = document.createElementNS(SVG_NS, 'rect');
      rect.setAttribute('class', 'admin-chart-bar');
      rect.setAttribute('x', x);
      rect.setAttribute('y', y);
      rect.setAttribute('width', barW);
      rect.setAttribute('height', Math.max(barH, 0.5));
      svg.appendChild(rect);

      var valueLabel = document.createElementNS(SVG_NS, 'text');
      valueLabel.setAttribute('class', 'admin-chart-bar-value');
      valueLabel.setAttribute('x', x + barW / 2);
      valueLabel.setAttribute('y', Math.max(y - 1.5, 5));
      valueLabel.setAttribute('text-anchor', 'middle');
      valueLabel.textContent = formatStatValue(value);
      svg.appendChild(valueLabel);

      var label = document.createElementNS(SVG_NS, 'text');
      label.setAttribute('class', 'admin-chart-bar-label');
      label.setAttribute('x', x + barW / 2);
      label.setAttribute('y', h - 3);
      label.setAttribute('text-anchor', 'middle');
      label.textContent = String((p && p.label) || '');
      svg.appendChild(label);
    });

    mount.appendChild(svg);
  }

  function renderSparkline(mount, content) {
    var values = (Array.isArray(content.points) ? content.points : []).map(Number);
    if (!values.length || values.some(function (v) { return !isFinite(v); })) {
      throw new Error('sparkline has no valid numeric points');
    }

    var w = 100, h = 40, pad = 3;
    var min = Math.min.apply(null, values);
    var max = Math.max.apply(null, values);
    var range = max - min || 1;

    var coords = values.map(function (v, i) {
      var x = values.length > 1 ? (i / (values.length - 1)) * (w - pad * 2) + pad : w / 2;
      var y = h - pad - ((v - min) / range) * (h - pad * 2);
      return [x, y];
    });

    var linePath = coords.map(function (c, i) {
      return (i === 0 ? 'M' : 'L') + c[0].toFixed(2) + ',' + c[1].toFixed(2);
    }).join(' ');
    var last = coords[coords.length - 1];
    var first = coords[0];
    var fillPath = linePath +
      ' L' + last[0].toFixed(2) + ',' + (h - pad) +
      ' L' + first[0].toFixed(2) + ',' + (h - pad) + ' Z';

    var svg = document.createElementNS(SVG_NS, 'svg');
    svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
    svg.setAttribute('class', 'admin-chart-svg');
    svg.setAttribute('role', 'img');

    var defs = document.createElementNS(SVG_NS, 'defs');
    var gradient = document.createElementNS(SVG_NS, 'linearGradient');
    gradient.setAttribute('id', 'admin-chart-sparkline-gradient');
    gradient.setAttribute('x1', '0');
    gradient.setAttribute('x2', '0');
    gradient.setAttribute('y1', '0');
    gradient.setAttribute('y2', '1');
    var stop1 = document.createElementNS(SVG_NS, 'stop');
    stop1.setAttribute('offset', '0%');
    stop1.style.stopColor = 'var(--admin-accent)';
    stop1.style.stopOpacity = '0.9';
    var stop2 = document.createElementNS(SVG_NS, 'stop');
    stop2.setAttribute('offset', '100%');
    stop2.style.stopColor = 'var(--admin-accent)';
    stop2.style.stopOpacity = '0';
    gradient.appendChild(stop1);
    gradient.appendChild(stop2);
    defs.appendChild(gradient);
    svg.appendChild(defs);

    var fill = document.createElementNS(SVG_NS, 'path');
    fill.setAttribute('class', 'admin-chart-sparkline-fill');
    fill.setAttribute('d', fillPath);
    svg.appendChild(fill);

    var line = document.createElementNS(SVG_NS, 'path');
    line.setAttribute('class', 'admin-chart-sparkline-path');
    line.setAttribute('d', linePath);
    svg.appendChild(line);

    mount.appendChild(svg);
  }

  function renderChartWindow(container, win) {
    var content = win.content || {};
    var mount = document.createElement('div');
    mount.className = 'admin-chart';

    if (content.type === 'sparkline') {
      renderSparkline(mount, content);
    } else {
      renderBarChart(mount, content);
    }

    if (content.unit) {
      var caption = document.createElement('p');
      caption.className = 'admin-window-body-caption';
      caption.textContent = content.unit;
      mount.appendChild(caption);
    }

    return global.AdminWindow.open({
      title: win.title || '',
      content: mount,
      container: container,
      variant: 'chart',
      x: win.x,
      y: win.y,
      w: win.w,
      h: win.h
    });
  }

  function renderWindow(container, win) {
    if (!win || typeof win !== 'object') {
      return rawDump(container, win, '// malformed window');
    }
    switch (win.kind) {
      case 'stat':
        return renderStatWindow(container, win);
      case 'image':
      case 'video':
        return renderMediaWindow(container, win);
      case 'chart':
        return renderChartWindow(container, win);
      default:
        // No kind (or an unrecognized one, kept here as a soft landing rather than
        // a hard failure) -> plain markdown-ish content, same as a digest window.
        return global.AdminWindow.open({
          title: win.title || '',
          content: win.content,
          container: container,
          x: win.x,
          y: win.y,
          w: win.w,
          h: win.h
        });
    }
  }

  var AdminRender = {
    render: function (container, input) {
      container.innerHTML = '';
      container.classList.add('admin-canvas');

      var parsed = parse(input);

      if (parsed && parsed.__unparseable !== undefined) {
        rawDump(container, parsed.__unparseable, '// admin canvas JSON did not parse');
        return;
      }

      if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.windows)) {
        rawDump(container, parsed, '// expected {"windows": [...]}, got something else');
        return;
      }

      parsed.windows.forEach(function (win) {
        try {
          renderWindow(container, win);
        } catch (err) {
          if (global.console) { global.console.warn('admin-canvas: window fell back to plain text', err); }
          rawDump(container, win, '// window failed to render: ' + err);
        }
      });
    },

    // Convenience used by canvas.html: fetch, render, and put the failure on
    // screen rather than only in the console if the fetch itself fails.
    load: function (container, url) {
      return global.fetch(url, { cache: 'no-store' })
        .then(function (response) {
          if (!response.ok) { throw new Error(response.status + ' ' + response.statusText); }
          return response.text();
        })
        .then(function (text) {
          AdminRender.render(container, text);
        })
        .catch(function (err) {
          container.innerHTML = '';
          container.classList.add('admin-canvas');
          rawDump(container, String(err), '// could not load ' + url);
        });
    }
  };

  global.AdminRender = AdminRender;
})(window);