<?xml version="1.0" encoding="UTF-8"?>
<feature name="iframe-region-renderer" version="0.1">
  <description>
    Reader for &lt;iframe-region&gt; regions. Renders the embed as an actual sandboxed
    iframe (srcdoc when prefer=snapshot, src when prefer=live), surfaces metadata
    (title, description, source, captured-at, captured-by, purpose), and verifies
    the &lt;captured-hash&gt; against the embedded &lt;snapshot&gt; CDATA content via WebCrypto
    SHA-256. Mismatch shows in red; match shows in green.
  </description>
  <requires>
    <feature name="registries"/>
    <feature name="readers-base"/>
  </requires>
  <provides>
    <reader name="iframe-region"/>
  </provides>
  <implementation lang="js"><![CDATA[
    const Readers = MR.registry.get('readers');

    function findChild(r, tagName) {
      for (const c of r.children) {
        if (c.tagName() === tagName) return c;
      }
      return null;
    }
    function findChildText(r, tagName) {
      const c = findChild(r, tagName);
      return c ? c.directText().trim() : '';
    }
    // When CDATA is present, return only the CDATA content (semantic intent of CDATA:
    // "this exact byte sequence is the content"; surrounding XML formatting whitespace
    // is not part of the value). When no CDATA, fall back to text nodes.
    function directTextWithCdata(region) {
      let cdata = '';
      let txt = '';
      const children = region.element.childNodes;
      for (let i = 0; i < children.length; i++) {
        const n = children[i];
        if (n.nodeType === 4) cdata += n.nodeValue;
        else if (n.nodeType === 3) txt += n.nodeValue;
      }
      return cdata || txt;
    }
    async function sha256Hex(text) {
      const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
      return Array.from(new Uint8Array(buf)).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
    }
    function esc(s) {
      return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    }

    Readers.register({
      name: 'iframe-region',
      priority: 10,
      claims: function(r) { return r.tagName() === 'iframe-region'; },
      read: async function(r) {
        return {
          type: 'mount',
          mount: async function(target) {
            const source = findChild(r, 'source');
            const snapshot = findChild(r, 'snapshot');
            const render = findChild(r, 'render');
            const provenance = findChild(r, 'provenance');

            const title = findChildText(r, 'title');
            const description = findChildText(r, 'description');
            const url = source ? findChildText(source, 'url') : '';
            const capturedAt = source ? findChildText(source, 'captured-at') : '';
            const capturedHashRaw = source ? findChildText(source, 'captured-hash') : '';
            const capturedHashHex = capturedHashRaw.replace(/^sha256:/i, '');
            const width = (render ? findChildText(render, 'width') : '') || '100%';
            const height = (render ? findChildText(render, 'height') : '') || '360px';
            const sandbox = render ? findChildText(render, 'sandbox') : '';
            const referrer = render ? findChildText(render, 'referrer-policy') : '';
            const prefer = (render ? findChildText(render, 'prefer') : '') || (snapshot ? 'snapshot' : 'live');
            const capturedBy = provenance ? findChildText(provenance, 'captured-by') : '';
            const purpose = provenance ? findChildText(provenance, 'purpose') : '';

            const snapshotContent = snapshot ? directTextWithCdata(snapshot) : '';

            let hashStatus = '';
            if (snapshotContent && capturedHashHex) {
              const computed = await sha256Hex(snapshotContent);
              if (computed === capturedHashHex.toLowerCase()) {
                hashStatus = '<span style="color:#7eb87e;">✓ matches embedded snapshot</span>';
              } else {
                hashStatus = '<span style="color:#c97a7a;">✗ MISMATCH (computed ' + esc(computed.slice(0, 16)) + '…)</span>';
              }
            } else if (snapshotContent && !capturedHashHex) {
              hashStatus = '<span style="color:#c97a7a;">snapshot present but no captured-hash</span>';
            } else if (!snapshotContent && capturedHashHex) {
              hashStatus = '<span style="color:#5e6770;">recorded; no embedded snapshot to verify against locally</span>';
            }

            const useSnapshot = prefer === 'snapshot' && snapshotContent;
            const useLive = !useSnapshot && url;

            target.innerHTML = '';
            target.style.whiteSpace = 'normal';
            target.style.fontFamily = 'inherit';

            const wrap = document.createElement('div');
            wrap.style.cssText = 'font-family:var(--mono);font-size:12px;color:var(--ink);';

            if (title) {
              const t = document.createElement('div');
              t.style.cssText = 'font-family:Georgia,serif;font-size:15px;color:var(--accent,#d4a574);font-style:italic;margin-bottom:6px;';
              t.textContent = title;
              wrap.appendChild(t);
            }
            if (description) {
              const d = document.createElement('div');
              d.style.cssText = 'font-family:Georgia,serif;font-size:13px;color:var(--ink-soft,#a8b0bb);margin-bottom:10px;line-height:1.5;';
              d.textContent = description;
              wrap.appendChild(d);
            }

            const meta = document.createElement('div');
            meta.style.cssText = 'font-family:var(--mono);font-size:10px;line-height:1.6;color:var(--ink-soft,#a8b0bb);border-left:2px solid var(--rule,#232930);padding:6px 10px;margin-bottom:10px;';
            const rows = [];
            if (url) rows.push(['source', '<a href="' + esc(url) + '" target="_blank" style="color:#8fa9c4;">' + esc(url) + '</a>']);
            if (capturedAt) rows.push(['captured-at', esc(capturedAt)]);
            if (capturedHashRaw) rows.push(['captured-hash', '<span style="word-break:break-all;">sha256:' + esc(capturedHashHex.slice(0, 24)) + '…</span> ' + hashStatus]);
            if (capturedBy) rows.push(['captured-by', esc(capturedBy)]);
            if (purpose) rows.push(['purpose', esc(purpose)]);
            rows.push(['render', useSnapshot ? 'srcdoc · embedded snapshot · no network' : (useLive ? 'src · live URL · network fetch' : '— no render —')]);
            rows.push(['sandbox', sandbox ? esc(sandbox) : '<span style="color:#c97a7a;">(strict — empty)</span>']);

            meta.innerHTML = rows.map(function(row) {
              return '<div><span style="color:#5e6770;text-transform:uppercase;letter-spacing:.1em;">' + row[0] + ':</span> ' + row[1] + '</div>';
            }).join('');
            wrap.appendChild(meta);

            if (useSnapshot || useLive) {
              const iframe = document.createElement('iframe');
              iframe.style.cssText = 'width:' + width + ';height:' + height + ';border:1px solid #232930;display:block;background:white;';
              iframe.setAttribute('sandbox', sandbox);
              if (referrer) iframe.referrerPolicy = referrer;
              if (useSnapshot) iframe.srcdoc = snapshotContent;
              else iframe.src = url;
              wrap.appendChild(iframe);
            } else {
              const empty = document.createElement('div');
              empty.style.cssText = 'color:#5e6770;font-style:italic;padding:20px;text-align:center;';
              empty.textContent = '— no renderable content —';
              wrap.appendChild(empty);
            }

            target.appendChild(wrap);
          }
        };
      }
    });

    MR.console.log('READER-REG iframe-region (priority 10)', 'info');
  ]]></implementation>
</feature>
