// PL-115: sandbox (opaque origin) chart renderer。
// 親 (content.js) から widget_code を受け取り、Chart.js で図を描画し、各 <canvas> を
// 直接 PNG 化して配列で返す。AI 製コードはこの opaque origin で動くため claude.ai の
// cookie / API に触れられない (= 安全に任意 JS 実行)。
//
// html2canvas は使わない: 隔離枠 (opaque origin) 内では内部の一時 iframe アクセスが
// cross-origin 拒否される (SecurityError)。Chart.js は <canvas> に描くため canvas を
// 直接 toDataURL すれば足りる。
//
// PL-115 follow-up (タブ全取り): widget には (a) タブ毎に別 canvas を持つ型と
// (b) 1 つの canvas のデータを差し替える型 (showChart で chart.destroy→new Chart) があり、
// (b) は描画時点で canvas が 1 枚しか無く 1 タブしか撮れなかった。タブボタンを実際に
// click し各状態の可視 canvas を撮ることで両型を網羅する。
//
// PL-115 follow-up (凡例救出): Claude の widget は Chart.js 標準凡例を
// `plugins.legend.display:false` で切り、色↔項目の対応を canvas 外の HTML 凡例
// (例: <span class="legend-dot" style="background:#hex">食費 32%</span>) だけが持つ。
// canvas だけ写すと凡例が全消えするため、各 canvas の属する section から凡例
// (色 + ラベル文字) を DOM から読み取り、グラフ画像の上に描き込んで 1 枚化する
// (出力描画エンジン無改修・html2canvas/foreignObject 不要)。class 名非依存:
// 「小さい正方形 + 背景色」を swatch とみなしその親要素の文字をラベルにする。

(function () {
  'use strict';

  // canvas を含む最大の祖先 section (他の canvas を含まない範囲) を返す。
  // これで「この図だけ」の凡例 (兄弟に置かれる) を捕捉できる。
  function chartSection(canvas, host) {
    var section = canvas;
    var p = canvas.parentElement;
    while (p && p !== host && p.querySelectorAll('canvas').length === 1) {
      section = p; p = p.parentElement;
    }
    return section;
  }

  // section 内の凡例エントリ [{color, text}] を DOM から収集 (class 名非依存)。
  // 小さい正方形 + 背景色を持つ要素を swatch とみなし、その親の文字をラベルにする。
  // form 要素 (checkbox/range 等) は誤検出回避のため除外。
  function collectLegend(section) {
    var out = [], seen = {};
    var all = section.querySelectorAll('*');
    for (var i = 0; i < all.length; i++) {
      var el = all[i];
      if (/^(CANVAS|INPUT|BUTTON|SELECT|TEXTAREA|A|LABEL)$/.test(el.tagName)) continue;
      var r;
      try { r = el.getBoundingClientRect(); } catch (_e) { continue; }
      if (!r.width || !r.height || r.width > 28 || r.height > 28 || Math.abs(r.width - r.height) > 8) continue;
      var bg = '';
      try { bg = getComputedStyle(el).backgroundColor; } catch (_e2) { continue; }
      if (!bg || bg === 'transparent' || bg === 'rgba(0, 0, 0, 0)') continue;
      var entry = el.parentElement;
      var text = entry ? (entry.textContent || '').replace(/\s+/g, ' ').trim() : '';
      if (!text || text.length > 48) continue;
      var key = bg + '|' + text;
      if (seen[key]) continue;
      seen[key] = 1;
      out.push({ color: bg, text: text });
    }
    return out;
  }

  function roundRect(g, x, y, w, h, rad) {
    g.beginPath();
    g.moveTo(x + rad, y);
    g.arcTo(x + w, y, x + w, y + h, rad);
    g.arcTo(x + w, y + h, x, y + h, rad);
    g.arcTo(x, y + h, x, y, rad);
    g.arcTo(x, y, x + w, y, rad);
    g.closePath();
  }

  // canvas の上に凡例ストリップを描いた合成 canvas を返す。凡例なしは canvas をそのまま返す。
  function buildPiece(canvas, legend) {
    if (!legend || !legend.length) return canvas;
    var scale = 2;
    try { if (canvas.clientWidth) scale = canvas.width / canvas.clientWidth; } catch (_e) {}
    if (!(scale > 0) || scale > 4) scale = 2;
    var W = canvas.width;
    var pad = Math.round(8 * scale);
    var fs = Math.round(13 * scale);
    var dot = Math.round(11 * scale);
    var gapDot = Math.round(5 * scale);
    var gapX = Math.round(16 * scale);
    var gapY = Math.round(7 * scale);
    var lineH = Math.max(fs, dot) + gapY;
    var meas = document.createElement('canvas').getContext('2d');
    meas.font = fs + 'px sans-serif';
    function entryW(t) { return dot + gapDot + meas.measureText(t).width; }
    // 行数を折返し計算
    var x = pad, rows = 1;
    for (var i = 0; i < legend.length; i++) {
      var tw = entryW(legend[i].text);
      if (x + tw > W - pad && x > pad) { x = pad; rows++; }
      x += tw + gapX;
    }
    var legendH = pad + rows * lineH + Math.round(2 * scale);
    var piece = document.createElement('canvas');
    piece.width = W;
    piece.height = legendH + canvas.height;
    var g = piece.getContext('2d');
    g.fillStyle = '#ffffff';
    g.fillRect(0, 0, piece.width, piece.height);
    g.font = fs + 'px sans-serif';
    g.textBaseline = 'middle';
    x = pad; var y = pad + lineH / 2;
    for (var j = 0; j < legend.length; j++) {
      var t = legend[j].text;
      var tw2 = entryW(t);
      if (x + tw2 > W - pad && x > pad) { x = pad; y += lineH; }
      g.fillStyle = legend[j].color || '#888';
      roundRect(g, x, y - dot / 2, dot, dot, Math.round(2 * scale)); g.fill();
      g.fillStyle = '#3d3d3a';
      g.fillText(t, x + dot + gapDot, y);
      x += tw2 + gapX;
    }
    g.drawImage(canvas, 0, legendH);
    return piece;
  }

  // タブバー (同じ canvas のデータ差替型 / section 切替型) のボタン群を検出。
  // 優先: role=tab → onclick 付き button を親でグループ化し最大群 → class fallback。
  function findTabButtons(host) {
    var byRole = host.querySelectorAll('[role="tab"]');
    if (byRole.length >= 2) return Array.prototype.slice.call(byRole);
    var btns = host.querySelectorAll('button'), parents = [], groups = [];
    for (var i = 0; i < btns.length; i++) {
      if (!btns[i].getAttribute('onclick')) continue;
      var par = btns[i].parentElement; if (!par) continue;
      var idx = parents.indexOf(par);
      if (idx < 0) { parents.push(par); groups.push([btns[i]]); } else groups[idx].push(btns[i]);
    }
    var best = [];
    for (var k = 0; k < groups.length; k++) if (groups[k].length > best.length) best = groups[k];
    if (best.length >= 2) return best;
    var byClass = host.querySelectorAll('.tab-bar button, .tabs button, [class*="tab"] button');
    if (byClass.length >= 2) return Array.prototype.slice.call(byClass);
    return [];
  }

  // 各 Chart インスタンスを「アニメ無効化」して最終状態を同期描画する。
  // 隠し iframe では rAF が進まず初期アニメが frame0 (棒高 0 等=空) で固着するため、
  // アニメを切って即最終値を描く (待ち time を入れずに中身を出す鍵)。click 直後の
  // display 変更を layout に反映させてから resize し 0 サイズ→実寸を復元する。
  function redrawCharts(host) {
    if (!(window.Chart && typeof window.Chart.getChart === 'function')) return;
    void host.offsetHeight;  // 直前の display:block 化を強制リフロー (resize が実寸を読むため)
    var cs = host.querySelectorAll('canvas');
    for (var i = 0; i < cs.length; i++) {
      var inst = null;
      try { inst = window.Chart.getChart(cs[i]); } catch (_e) {}
      if (!inst) continue;
      try {
        inst.options.animation = false;       // 初期/更新アニメを無効化
        inst.options.animations = false;
        if (typeof inst.stop === 'function') inst.stop();  // 進行中アニメを停止 (frame0 固着回避)
        inst.resize();
        inst.update('none');
        inst.draw();
      } catch (_e2) {}
    }
  }

  // display:none の祖先は offsetParent=null。可視タブの canvas だけ撮るための判定。
  function isVisible(el) {
    try {
      var r = el.getBoundingClientRect();
      return !!(r.width && r.height) && el.offsetParent !== null;
    } catch (_e) { return false; }
  }

  // canvas の祖先で display:none の物を開く (タブ無し複数図 widget の事前 unhide 用)。
  function unhideAll(host) {
    var cs = host.querySelectorAll('canvas');
    for (var i = 0; i < cs.length; i++) {
      var pe = cs[i];
      while (pe && pe !== host) {
        try { if (getComputedStyle(pe).display === 'none') pe.style.display = 'block'; } catch (_e) {}
        pe = pe.parentElement;
      }
    }
  }

  // canvas + 表示中の凡例を焼いた「スナップショット canvas」を返す (失敗時 null)。
  //   1 canvas データ差替型 (showChart) はタブ毎に同じ canvas を上書きするので、
  //   その都度ここで別 canvas へ複写し各タブの絵を保全する。
  function snapshotCanvas(canvas, host) {
    var leg = [];
    try { leg = collectLegend(chartSection(canvas, host)); } catch (_e) {}
    var piece = null;
    try { piece = buildPiece(canvas, leg); } catch (_e2) { piece = null; }
    // buildPiece は凡例なしだと元 canvas をそのまま返すため別 canvas へ複写して保全。
    if (!piece || piece === canvas) {
      try {
        var s = document.createElement('canvas');
        s.width = canvas.width; s.height = canvas.height;
        s.getContext('2d').drawImage(canvas, 0, 0);
        piece = s;
      } catch (_e3) { piece = null; }
    }
    return piece;
  }

  // piece canvas を PNG dataURI 化 (dedup 指紋兼用)。toDataURL は同期で throttle されない。
  function pieceKey(canvas) {
    try { var u = canvas.toDataURL('image/png'); return (u && u.length > 256) ? u : ''; }
    catch (_e) { return ''; }
  }

  // widget_code を展開・実行し、各タブ状態のグラフ (+凡例) を PNG dataURI 配列で返す。
  async function renderWidget(code) {
    var stage = document.getElementById('stage');
    stage.innerHTML = '';
    var host = document.createElement('div');
    host.style.cssText = 'width:760px; background:#ffffff; color:#111; padding:16px 18px; box-sizing:border-box;';
    stage.appendChild(host);

    // markup と script を分離 (外部 CDN script は drop = Chart.js 同梱を preload 済)
    var tmp = document.createElement('div');
    tmp.innerHTML = String(code || '');
    var scripts = [];
    var sn = tmp.querySelectorAll('script');
    for (var i = 0; i < sn.length; i++) {
      if (!sn[i].src) scripts.push(sn[i].textContent || '');
      sn[i].parentNode && sn[i].parentNode.removeChild(sn[i]);
    }
    host.appendChild(tmp);

    // タブを markup から検出。タブ無し widget だけ事前 unhide (隠し section の図を full size で
    // 初期化)。タブ有りは click で可視を制御するため事前 unhide すると inline display が class
    // 切替を上書きし複数 section/凡例が同時可視化する → unhide せず click 駆動に委ねる。
    var tabButtons = findTabButtons(host);
    if (!tabButtons.length) unhideAll(host);

    // 間接 eval で widget script を「グローバル」実行する。これにより widget の
    // function 宣言 (switchTab/showChart 等) が window に乗り、markup の inline
    // onclick="showChart(...)" から解決でき、後段でタブ click が実機同様に効く。
    // const/let は間接 eval 内のレキシカルスコープに留まるため複数 widget 間で再宣言
    // 衝突しない (= 旧 IIFE の目的を維持)。それら function は同 eval スコープを閉包する
    // ので、先頭で network API を undefined に縛り widget からのネットアクセスを遮断
    // (本人会話由来で脅威は低いが多層防御)。
    var netShield = 'let fetch=undefined,XMLHttpRequest=undefined,WebSocket=undefined,EventSource=undefined,importScripts=undefined;\n';
    for (var j = 0; j < scripts.length; j++) {
      try {
        (0, eval)(netShield + scripts[j]);
      } catch (e) { console.error('[PL-115 sandbox] widget script threw:', e && e.message, e); }
    }

    // 初期描画を確定 (click→redraw は同期だが念のため 1 度走らせる)。
    redrawCharts(host);

    // 各図を「別画像」(PNG dataURI) として集める。連結 1 枚化はしない: asa_core 側で
    // render_as='chart' を格子化対象から除外し各図フル幅・縦並びで描く (PL-115 follow-up)。
    // 1 枚連結だと PDF で 1 ページ高に縮小・分割不可で枚数増に破綻するため別画像が正。
    var urls = [], seen = {};

    // タブ駆動: 各タブを実際に click し、その状態の可視 canvas + 表示中凡例を撮る。
    // 1 canvas のデータ差替型 (showChart) も section 別 canvas 型 (switchTab) も同一手順で網羅。
    // click→redrawCharts(同期)→撮影 で待ち時間ゼロ (隠し iframe では setTimeout が
    // 最低 1s に間引かれ delay を入れるとタブ毎 ~2s 増 = 旧 7s の主因だったため撤去)。
    // 同一指紋は dedup (タブ click で絵が変わらない場合の重複排除、key=dataURI)。
    if (tabButtons.length >= 2) {
      for (var t = 0; t < tabButtons.length; t++) {
        try { tabButtons[t].click(); } catch (_e) {}
        redrawCharts(host);
        var cs = host.querySelectorAll('canvas');
        for (var c = 0; c < cs.length; c++) {
          if (!isVisible(cs[c]) || !cs[c].width || !cs[c].height) continue;
          var snap = snapshotCanvas(cs[c], host);
          var key = snap ? pieceKey(snap) : '';
          if (key && !seen[key]) { seen[key] = 1; urls.push(key); }
        }
      }
    }

    // タブ無し / タブ駆動が空振り → 全 canvas を撮る (従来動作・単一図 widget 含む)。
    if (!urls.length) {
      if (tabButtons.length) unhideAll(host);
      redrawCharts(host);
      var cs2 = host.querySelectorAll('canvas');
      for (var c2 = 0; c2 < cs2.length; c2++) {
        if (!cs2[c2].width || !cs2[c2].height) continue;
        var snap2 = snapshotCanvas(cs2[c2], host);
        var key2 = snap2 ? pieceKey(snap2) : '';
        if (key2 && !seen[key2]) { seen[key2] = 1; urls.push(key2); }
      }
    }

    return urls;
  }

  window.addEventListener('message', async function (ev) {
    var d = ev.data;
    if (!d || typeof d !== 'object' || d.type !== 'asa_pl115_render') return;
    var pngs = [];
    try { pngs = await renderWidget(String(d.code || '')); } catch (e) { console.error('[PL-115 sandbox] render threw:', e && e.message); pngs = []; }
    try { ev.source.postMessage({ type: 'asa_pl115_result', id: d.id, pngs: pngs }, '*'); } catch (_e2) {}
  });

  // 親に ready 通知 (iframe ロード完了の同期点)
  try { parent.postMessage({ type: 'asa_pl115_ready' }, '*'); } catch (_e) {}
})();
