/* * DigestRender — turns digest JSON into a canvas of DigestWindows. * * Everything in here is written around one requirement from the plan's testing * checklist: "If a run's LLM output produces malformed canvas-SDK calls, does the * digest fall back to plain text instead of a broken/blank page?" A local model * emitting near-miss JSON four times a day is a routine event, not an exception, * so every layer degrades instead of throwing: * * - unparseable input -> a
 dump of the raw text
 *   - a section that isn't a doc -> a 
 dump of that section, others still render
 *   - a window that throws       -> a 
 dump of that window, siblings still render
 *   - a globe with no markers    -> the globe still draws, just empty
 *
 * A blank page is the one outcome that must never happen.
 *
 * PER-PERSON SECTIONS
 * -------------------
 * A run carries `people` — each household member's own set of digest sections, as
 * identity's admin panel recorded it and run.py copied it in (see
 * digest-engine/preferences.py). Given `options.person` — a name, nickname or id
 * that Home Assistant already resolved — only that person's sections are drawn.
 *
 * This is a DISPLAY FILTER, NOT AN ACCESS CONTROL. digest-web serves this same JSON
 * read-only to anything on the LAN, so a section left out here is off a screen, not
 * out of anyone's reach. Do not describe it to a user as privacy.
 */

(function (global) {
  'use strict';

  function rawDump(container, value, note) {
    var pre = document.createElement('pre');
    pre.className = 'digest-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 };
    }
  }

  // Accepts what run.py writes ({sections: {compact: [...], full: [...]}}), a bare
  // array of section documents, or a single section document.
  function toSectionList(parsed, detailLevel) {
    if (Array.isArray(parsed)) { return parsed; }
    if (!parsed || typeof parsed !== 'object') { return []; }
    if (parsed.sections && typeof parsed.sections === 'object') {
      var sections = parsed.sections;
      if (Array.isArray(sections)) { return sections; }
      var picked = sections[detailLevel] || sections.full || sections.compact;
      return Array.isArray(picked) ? picked : [];
    }
    if (parsed.windows) { return [parsed]; }
    return [];
  }

  // The sections this viewer gets, or null for "nobody was resolved / nothing is
  // known about them" — which the caller, not this function, decides what to do with.
  function sectionsForPerson(parsed, person) {
    if (!person) { return null; }
    var wanted = String(person).trim().toLowerCase();
    if (!wanted) { return null; }
    var people = (parsed && parsed.people) || [];
    for (var i = 0; i < people.length; i++) {
      var entry = people[i] || {};
      var matches =
        String(entry.name || '').toLowerCase() === wanted ||
        String(entry.nickname || '').toLowerCase() === wanted ||
        String(entry.id) === wanted;
      if (matches) { return entry.digest_sections || []; }
    }
    // Named somebody this digest has never heard of — treated exactly like naming
    // nobody, never as "show them everything because they must be new".
    return null;
  }

  function filterSections(sections, parsed, options) {
    var person = String(options.person || '').trim();
    var people = (parsed && parsed.people) || [];

    // Somebody WAS resolved, but this run has no preferences for anyone — identity was
    // unreachable when it generated (see preferences.py). That run already fell back to
    // generating everything, and hiding a resolved person's own personal section on top
    // of it would make one container being down cost them more than it has to. Same
    // direction of failure at both ends: more digest, not less.
    if (person && !people.length) { return sections; }

    var allowed = sectionsForPerson(parsed, person);
    if (allowed) {
      return sections.filter(function (doc) {
        return doc && allowed.indexOf(doc.section) !== -1;
      });
    }
    // No resolved person. The personal section is somebody's mail and messages, and
    // the plan's rule for the shared kiosk is that it is never shown on a guess — see
    // thinclient_agent/digest_canvas.py, which passes `person` only when Home
    // Assistant has already resolved exactly who asked.
    if (options.requirePersonForPersonal) {
      return sections.filter(function (doc) {
        return !doc || doc.section !== 'personal';
      });
    }
    return sections;
  }

  // A marker's own briefing: the summary of what is happening there, and its
  // sources folded away underneath. Rendered below the globe rather than as a
  // tooltip on the marker — the globe rotates, markers pass behind the limb, and a
  // summary you can only read while its marker happens to be facing you is not a
  // summary. The label ties the two together.
  function renderMarkerBriefs(container, markers) {
    var briefed = (markers || []).filter(function (marker) {
      return marker && (marker.summary || (marker.sources && marker.sources.length));
    });
    if (!briefed.length) { return; }

    var wrap = document.createElement('div');
    wrap.className = 'digest-globe-briefs';

    briefed.forEach(function (marker) {
      var brief = document.createElement('div');
      brief.className = 'digest-globe-brief';

      var heading = document.createElement('h4');
      heading.className = 'digest-brief-title';
      // Carries the marker's own colour so the eye can pair a red hammer-and-sickle
      // on the globe with the paragraph explaining it.
      if (marker.color) { heading.style.color = marker.color; }
      heading.textContent = marker.label || 'Marker';
      brief.appendChild(heading);

      if (marker.summary) {
        var text = document.createElement('p');
        text.className = 'digest-brief-summary';
        text.textContent = marker.summary;
        brief.appendChild(text);
      }

      var sources = global.DigestWindow.sources(marker.sources);
      if (sources) { brief.appendChild(sources); }

      wrap.appendChild(brief);
    });

    container.appendChild(wrap);
  }

  function renderGlobeWindow(container, win) {
    var mount = document.createElement('div');
    var el = global.DigestWindow.open({
      title: win.title || 'Globe',
      content: mount,
      container: container,
      variant: 'globe',
      sources: win.sources
    });

    var globe = new global.DigestGlobe(mount);
    (win.globe_markers || []).forEach(function (marker) {
      try {
        globe.addMarker(marker.lat, marker.lon, {
          icon: marker.icon,
          color: marker.color,
          glow: marker.glow,
          label: marker.label
        });
      } catch (err) {
        // One bad marker must not cost the whole globe.
        if (global.console) { global.console.warn('digest: skipped a globe marker', err); }
      }
    });

    if (typeof win.content === 'string' && win.content.trim()) {
      var caption = document.createElement('p');
      caption.className = 'digest-globe-caption';
      caption.textContent = win.content;
      mount.parentNode.appendChild(caption);
    }

    renderMarkerBriefs(mount.parentNode, win.globe_markers);

    return el;
  }

  function renderWindow(container, win) {
    if (!win || typeof win !== 'object') {
      return rawDump(container, win, '// malformed window');
    }
    if (win.kind === 'globe') {
      return renderGlobeWindow(container, win);
    }
    return global.DigestWindow.open({
      title: win.title || '',
      content: win.content,
      container: container,
      // Any window may carry citations, not just the globe — a "Highlights" list
      // and a standalone analysis window both need somewhere to put their receipts.
      sources: win.sources,
      x: win.x,
      y: win.y,
      w: win.w,
      h: win.h
    });
  }

  function renderSection(container, doc) {
    if (!doc || typeof doc !== 'object' || !Array.isArray(doc.windows)) {
      return rawDump(container, doc, '// section did not match the digest schema');
    }

    doc.windows.forEach(function (win) {
      try {
        var el = renderWindow(container, win);
        if (el && doc.section) {
          el.dataset.section = doc.section;
        }
      } catch (err) {
        if (global.console) { global.console.warn('digest: window fell back to plain text', err); }
        rawDump(container, win, '// window failed to render: ' + err);
      }
    });
  }

  var DigestRender = {
    render: function (container, input, options) {
      options = options || {};
      container.innerHTML = '';
      container.classList.add('digest-canvas');

      var parsed = parse(input);

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

      var sections = toSectionList(parsed, options.detailLevel || 'full');

      if (!sections.length) {
        rawDump(container, parsed, '// no digest sections found in this document');
        return;
      }

      var visible = filterSections(sections, parsed, options);

      // Everything this run generated was filtered out — every section this viewer
      // wanted is one nobody generated, or they asked for none at all. That is a
      // legitimate outcome of the settings, not a failure, so it gets a plain window
      // rather than the 
 dump a malformed digest gets. Still never a blank page.
      if (!visible.length) {
        global.DigestWindow.open({
          title: 'Nothing in this digest',
          content:
            'No digest section is switched on for this screen. Sections are chosen ' +
            'per person in the identity admin panel.',
          container: container
        });
        return;
      }

      visible.forEach(function (doc) {
        try {
          renderSection(container, doc);
        } catch (err) {
          if (global.console) { global.console.warn('digest: section fell back to plain text', err); }
          rawDump(container, doc, '// section failed to render: ' + err);
        }
      });
    },

    // Convenience used by both templates: fetch, render, and put the failure on
    // screen rather than only in the console if the fetch itself fails.
    load: function (container, url, options) {
      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) {
          DigestRender.render(container, text, options);
        })
        .catch(function (err) {
          container.innerHTML = '';
          container.classList.add('digest-canvas');
          rawDump(container, String(err), '// could not load ' + url);
        });
    }
  };

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