// Claude の会話 JSON (chat_messages) を ASA の発言形へ変換する共有部品。
//
// PL-134: 公式エクスポート ZIP の取込アドオンは、拡張が site API から受け取るのと
// 同じ形の JSON を扱う。変換を Python 側へ書き写すと同じ処理が 2 か所になり、
// 片方だけ直る食い違い (PL-056) を再発させるため、この file を唯一の変換元とし
// 拡張 (content.js の前に読み込み) と取込ページの両方から使う。
//
// 依存: chrome API には触れない。変換そのものは純粋だが、SVG → PNG の
// ラスタライズ (rasterizeSvgBlocks) だけは Image / canvas を使う。呼び手は
// 拡張と取り込みページの両方ともブラウザのため、ここに置いて 1 つに保つ。
(function (root) {
  'use strict';
  const CLAUDE_ROOT_SENTINEL = '00000000-0000-4000-8000-000000000000';

  function extractAttachmentPlaceholders(msg) {
    const placeholders = [];
    const buckets = [
      Array.isArray(msg && msg.attachments) ? msg.attachments : [],
      Array.isArray(msg && msg.files) ? msg.files : [],
      Array.isArray(msg && msg.sync_sources) ? msg.sync_sources : [],
    ];
    for (const bucket of buckets) {
      for (const a of bucket) {
        if (!a) continue;
        const name = a.file_name || a.name || a.title || '';
        if (name) placeholders.push('[添付ファイル: ' + String(name) + ']');
      }
    }
    return placeholders;
  }

  // ===== PL-078 Phase C: Claude の図 (SVG) を出力に埋め込む =====
  // Claude は AI ラスター画像を生成しない。代わりに「図」を visualize:show_widget
  // (input.widget_code) / create_file (.svg, input.file_text) の tool_use として
  // 会話データに SVG テキストで持つ。描画は別 origin sandbox iframe (claudemcpcontent
  // .com 等) で行われ content.js から読めないが、 元 SVG テキストは会話 API 応答
  // (page_hook が既に sniff) に丸ごと在る (spike probe_claude_svg_source で確定)。
  // → ChatGPT の raster と同 contract で image_blocks に載せ、本文は [[ASA_IMAGE_N]]
  //   マーカー参照 (asa_core renderer が html/pdf 埋め込み、md/txt は印に degrade)。
  // SVG は数 KB のテキストのため data_uri を snapshot に直載せ (raster と違い別チャネル
  // 不要、 ASA HTTP 上限に余裕)。 React/HTML widget (widget_code が <svg 始まりでない)
  // は動くアプリで静止画化不可 = 従来の [AI生成: title] 印を維持。
  const _PL078C_SVG_BYTES_MAX = 512 * 1024;  // 1 図上限 (snapshot 肥大 backstop)
  const _PL078C_SVG_HEAD_RE = /^\s*<svg[\s>]/i;

  function _pl078cSvgToDataUri(svg) {
    const s = String(svg || '');
    if (!_PL078C_SVG_HEAD_RE.test(s)) return '';        // 純 SVG のみ (React/HTML 除外)
    if (s.length > _PL078C_SVG_BYTES_MAX) return '';     // 過大 → 印 degrade
    try {
      // UTF-8 安全 base64 (title 等に日本語 → btoa 直接は不可)。crash_buffer の
      // sidecar 倉庫が ;base64, 形を要求するため utf8 形でなく base64 で持つ。
      const b64 = btoa(unescape(encodeURIComponent(s)));
      return 'data:image/svg+xml;base64,' + b64;
    } catch (_e) { return ''; }
  }

  // tool_use part から「埋め込むべき SVG とその題名」を取り出す。SVG でなければ null。
  function _pl078cSvgFromToolUse(part) {
    const name = String((part && part.name) || '');
    const input = (part && part.input) || {};
    let svg = '', title = '';
    if (name === 'visualize:show_widget') {
      svg = String(input.widget_code || '');
      title = String(input.title || '').trim();
    } else if (name === 'create_file') {
      const path = String(input.path || '');
      if (!/\.svg$/i.test(path)) return null;            // .svg 出力のみ
      svg = String(input.file_text || '');
      title = String(input.description || '').trim() ||
        (path.split(/[\\/]/).pop() || '').replace(/\.svg$/i, '');
    } else {
      return null;
    }
    const dataUri = _pl078cSvgToDataUri(svg);
    if (!dataUri) return null;                            // 非 SVG / 過大 → caller が印 fallback
    return { dataUri: dataUri, title: title };
  }

  // PL-112: web 検索の tool_result に第三者サイトの画像 (jpg/png) が
  //   .content[].images[].url で入る (probe 確定)。 従来 parse は tool_result を
  //   skip し全ドロップしていた。 公開画像 URL を集めて返す (src=URL で image_blocks に
  //   載せ、 保存時に asa_core 本体が公開 URL を fetch→data_uri 埋込=offline 永続化)。
  function _pl112WebImagesFromToolResult(part) {
    const out = [];
    const seen = new Set();
    const content = part && part.content;
    if (!Array.isArray(content)) return out;
    for (const item of content) {
      const imgs = item && item.images;
      if (!Array.isArray(imgs)) continue;
      for (const im of imgs) {
        const u = String((im && (im.url || im.thumbnail_url)) || '').trim();
        if (!u || !/^https?:\/\//i.test(u) || seen.has(u)) continue;   // 公開 http(s) のみ・重複排除
        seen.add(u);
        out.push(u);
      }
    }
    return out;
  }

  // SVG → PNG ラスタライズ。 asa_core の PDF renderer (reportlab/PIL) は SVG の data
  // URI を画像化できず本文へ raw 文字列を吐く。 そこで拡張側 (= ブラウザの SVG エンジン)
  // で canvas に描いて PNG data URI 化し、 html/pdf 両方が描ける形にする (Python 依存
  // 追加なし)。 自己完結 SVG (外部参照なし) のため canvas は tainted せず toDataURL 可。
  const _PL078C_PNG_CACHE = new Map();      // svg dataUri -> png dataUri (再変換回避)
  const _PL078C_PNG_CACHE_MAX = 40;

  // 描く大きさ。 viewBox 優先 (width="100%" の図は viewBox にしか実寸が無い)。
  function _pl078cSvgDims(svgText) {
    let w = 0, h = 0;
    try {
      const vb = String(svgText || '').match(/viewBox\s*=\s*["']\s*[\d.+-]+\s+[\d.+-]+\s+([\d.+-]+)\s+([\d.+-]+)/i);
      if (vb) { w = parseFloat(vb[1]); h = parseFloat(vb[2]); }
      if (!w || !h) {
        const wm = String(svgText || '').match(/\bwidth\s*=\s*["']\s*([\d.]+)/i);
        const hm = String(svgText || '').match(/\bheight\s*=\s*["']\s*([\d.]+)/i);
        if (wm) w = parseFloat(wm[1]);
        if (hm) h = parseFloat(hm[1]);
      }
    } catch (_e) {}
    if (!w || !h || !isFinite(w) || !isFinite(h)) { w = 800; h = 600; }
    const scale = Math.min(2, 1600 / Math.max(w, h, 1));   // 2x まで、 最大辺 1600px
    return { w: Math.max(1, Math.round(w * scale)), h: Math.max(1, Math.round(h * scale)) };
  }

  function _pl078cRasterizeSvg(svgDataUri) {
    const cached = _PL078C_PNG_CACHE.get(svgDataUri);
    if (cached !== undefined) return Promise.resolve(cached);
    return new Promise((resolve) => {
      const done = (val) => {
        if (_PL078C_PNG_CACHE.size >= _PL078C_PNG_CACHE_MAX) {
          const k = _PL078C_PNG_CACHE.keys().next().value;
          if (k !== undefined) _PL078C_PNG_CACHE.delete(k);
        }
        _PL078C_PNG_CACHE.set(svgDataUri, val);
        resolve(val);
      };
      try {
        let svgText = '';
        try { svgText = atob(String(svgDataUri).split(';base64,')[1] || ''); } catch (_e) { svgText = ''; }
        const dims = _pl078cSvgDims(svgText);
        const img = new Image();
        img.onload = () => {
          try {
            const canvas = document.createElement('canvas');
            canvas.width = dims.w; canvas.height = dims.h;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, dims.w, dims.h);
            done(canvas.toDataURL('image/png'));
          } catch (_e) { done(''); }
        };
        img.onerror = () => done('');
        img.src = svgDataUri;
      } catch (_e) { resolve(''); }
    });
  }

  // msgs の image_blocks 内 SVG data URI を PNG に置換 (html/pdf 両対応化)。
  // 変換失敗時は SVG のまま残す (html は描ける / pdf は印に degrade)。
  // 呼び手は 2 つ: 拡張 (content.js) と 取り込みアドオンの画面。 どちらもブラウザで
  // 動くため、 この 1 か所だけが後段を持てば両経路が同じ絵になる (片方だけ抜ける
  // PL-056 型の食い違いを作らない)。
  async function rasterizeSvgBlocks(msgs) {
    if (!Array.isArray(msgs)) return msgs;
    for (const m of msgs) {
      if (!m || !Array.isArray(m.image_blocks)) continue;
      for (const ib of m.image_blocks) {
        if (!ib) continue;
        const du = String(ib.data_uri || '');
        if (du.indexOf('data:image/svg+xml') !== 0) continue;
        try {
          const png = await _pl078cRasterizeSvg(du);
          if (png) ib.data_uri = png;
        } catch (_e) {}
      }
    }
    return msgs;
  }

  function _pl115IsRenderable(code) {
    const c = String(code || '');
    if (!/^\s*</.test(c)) return false;                 // HTML 断片で始まる物のみ (JS module は除外)
    if (_PL078C_SVG_HEAD_RE.test(c)) return false;       // 純 SVG は PL-078 Phase C 経路
    return /<canvas[\s>]/i.test(c) && /\bnew\s+Chart\s*\(/.test(c);  // Chart.js 図に限定 (v1)
  }


  function parseClaudeConversationApiResponse(jsonText) {
    let data;
    try {
      data = JSON.parse(String(jsonText || ''));
    } catch (_e) {
      return null;
    }
    const chatMessages = data && data.chat_messages;
    if (!Array.isArray(chatMessages)) return null;
    const leaf = data.current_leaf_message_uuid;
    if (!leaf) return null;
    const byUuid = Object.create(null);
    for (const m of chatMessages) {
      if (m && m.uuid) byUuid[m.uuid] = m;
    }
    const chain = [];
    const visited = new Set();
    let cur = leaf;
    while (cur && cur !== CLAUDE_ROOT_SENTINEL && byUuid[cur] && !visited.has(cur)) {
      chain.push(byUuid[cur]);
      visited.add(cur);
      cur = byUuid[cur].parent_message_uuid;
    }
    chain.reverse();
    return _convertClaudeMessageChain(chain);
  }

  // PL-120: chain (= 発言順 chat_message 配列) → 保存用 messages 変換。 通常会話 (leaf chain) と
  // 共有 snapshot (index 順) で共通利用。
  // PL-133 横展開: 中身を持つ発言を捨てた時だけ件数を数える。 本 converter は part の
  // `type` と tool 名の allowlist で拾っており、 Claude 側が新種別 / 別名の道具を出すと
  // 本文が空になり `if (!text) continue` で turn ごと消える (ChatGPT の PL-133 と同型)。
  // 従来は完全に無言だったため、 起きても偶然見つけるまで気付けなかった。
  function _pl133CountIfLossy(msg, tally) {
    const arr = Array.isArray(msg && msg.content) ? msg.content : [];
    if (arr.length === 0) return;
    const sender = String((msg && msg.sender) || '?').toLowerCase();
    for (const part of arr) {
      if (!part || typeof part !== 'object') continue;
      const type = String(part.type || '?');
      const key = `${sender}/${type}`;
      const hit = tally[key] || (tally[key] = { parts: 0, chars: 0 });
      hit.parts += 1;
      if (type === 'text') hit.chars += String(part.text || '').length;
    }
  }

  function _convertClaudeMessageChain(chain) {
    const messages = [];
    const droppedTally = {};
    for (const msg of chain) {
      const sender = String(msg.sender || '').toLowerCase();
      let role;
      if (sender === 'human') role = 'user';
      else if (sender === 'assistant') role = 'assistant';
      else { _pl133CountIfLossy(msg, droppedTally); continue; }
      const contentArr = Array.isArray(msg.content) ? msg.content : [];
      const textParts = [];
      const imageBlocks = [];
      const pendingWidgets = [];   // PL-115: 後段で sandbox 描画する Chart.js widget
      for (const part of contentArr) {
        if (!part || typeof part !== 'object') continue;
        const t = String(part.type || '');
        if (t === 'text') {
          const s = String(part.text || '');
          if (s) textParts.push(s);
        } else if (t === 'tool_use') {
          // PL-039 Phase 1.1: Claude artifact = visualize integration の tool_use。
          // `visualize:show_widget` のみ chat 内 inline render される 描画系
          // (= SVG / HTML / Mermaid / interactive 等)、 user 視点は「絵」 と同じ。
          // 他 tool_use (= `visualize:read_me` の design guide 取得 / search 系等)
          // は AI 内部準備で user 不要 = skip。
          //
          // PL-078 Phase C: 描画系が純 SVG (widget_code / create_file の .svg) の場合は
          // 元 SVG テキストを image_blocks に載せ、本文は [[ASA_IMAGE_N]] マーカーで参照
          // (asa_core renderer が html/pdf に埋め込み、md/txt は印に degrade)。
          // SVG でない widget (React/HTML 等 = 動くアプリで静止画化不可) は従来の
          // [AI生成: title] 印を維持。
          const toolName = String(part.name || '');
          const svgInfo = _pl078cSvgFromToolUse(part);
          if (svgInfo) {
            textParts.push(`[[ASA_IMAGE_${imageBlocks.length}]]`);
            imageBlocks.push({
              alt: svgInfo.title, src: '', data_uri: svgInfo.dataUri,
              title: svgInfo.title, img_ref: '',
              // PL-134: AI が作った図。格子に押し込まず元の形で縦に並べる
              // (ChatGPT の生成画像と同じ扱い。 web 引用画像は対象外)。
              gen: true,
            });
          } else if (toolName === 'visualize:show_widget') {
            const title = String((part.input && part.input.title) || '').trim();
            // PL-115: Chart.js 等の動的グラフ widget は sandbox で実描画→画像化 (後段の
            // _pl115RasterizeWidgets)。ここでは sentinel を置き widget_code を pending に記録。
            // 描画不能 (非対応 widget) / 描画失敗時は後段が [AI生成: title] 印に degrade。
            const wcode = String((part.input && part.input.widget_code) || '');
            if (_pl115IsRenderable(wcode)) {
              textParts.push(`[[ASA_WIDGET_${pendingWidgets.length}]]`);
              pendingWidgets.push({ code: wcode, title: title });
            } else {
              const label = title ? `AI生成: ${title}` : 'AI生成: ビジュアル';
              textParts.push(`[${label}]`);
            }
          }
        } else if (t === 'tool_result') {
          // PL-112: web 検索結果の画像 (第三者サイト jpg/png) を採用。 src=URL で
          // image_blocks に載せ本文は [[ASA_IMAGE_N]] で参照 (連続 2 枚以上は PL-110 の
          // 正方タイル格子に整列、 保存時に本体が公開 URL を fetch→data_uri 埋込)。
          // tool_result の他の本文 (検索の生テキスト等) は従来どおり本文混入させない。
          for (const url of _pl112WebImagesFromToolResult(part)) {
            textParts.push(`[[ASA_IMAGE_${imageBlocks.length}]]`);
            imageBlocks.push({ alt: '', src: url, data_uri: '', title: '', img_ref: '' });
          }
        }
        // type:'thinking' / 他全 type は skip。 user UI に出ない AI 内部情報
        // (思考 / tool 内部 feedback / 内部準備 manual) を本文混入させない。
      }
      let text = textParts.join('\n\n').trim();
      if (role === 'user') {
        const placeholders = extractAttachmentPlaceholders(msg);
        if (placeholders.length > 0) {
          const ph = placeholders.join('\n\n');
          text = text ? (ph + '\n\n' + text) : ph;
        }
      }
      if (!text) {
        // PL-133 横展開: 中身はあるのに本文化できなかった発言 (未知の part type /
        // 別名の描画 tool 等)。 これが出たら Claude 側の構造が変わった合図。
        _pl133CountIfLossy(msg, droppedTally);
        continue;
      }
      const out = { role, text, code_blocks: [], image_blocks: imageBlocks, msg_id: msg.uuid };
      if (pendingWidgets.length) out._pl115pending = pendingWidgets;  // PL-115: 後段で消費・削除
      messages.push(out);
    }
    if (Object.keys(droppedTally).length > 0) {
      try {
        console.log('[PL-133-drop]', JSON.stringify({
          t: Date.now(), kept: messages.length, chain: chain.length, dropped: droppedTally,
        }));
      } catch (_e) {}
    }
    return messages;
  }

  // PL-120: 共有 snapshot API (org スコープ, render_all_tools=true) は通常会話 API と同じ
  // content-parts 構造を返すが current_leaf_message_uuid を持たない。 chat_messages を index 順に
  // 並べ共通変換へ流す (artifact widget_code 込みで _pl115 / _pl078c rasterize 可)。
  function parseClaudeSnapshotApiResponse(jsonText) {
    let data;
    try {
      data = JSON.parse(String(jsonText || ''));
    } catch (_e) {
      return null;
    }
    const chatMessages = data && data.chat_messages;
    if (!Array.isArray(chatMessages)) return null;
    const chain = chatMessages
      .filter((m) => m && typeof m === 'object')
      .slice()
      .sort((a, b) => (a.index || 0) - (b.index || 0));
    return _convertClaudeMessageChain(chain);
  }

  root.ASA_CLAUDE_PARSE = {
    parseClaudeConversationApiResponse: parseClaudeConversationApiResponse,
    parseClaudeSnapshotApiResponse: parseClaudeSnapshotApiResponse,
    rasterizeSvgBlocks: rasterizeSvgBlocks,
  };
})(typeof globalThis !== 'undefined' ? globalThis : this);
