// Gemini の会話 HTML を ASA の発言形へ変換する共有部品。
//
// PL-134: 公式エクスポート (Google Takeout の「マイ アクティビティ」) は会話が
// **HTML に変換済み**で入っており、ASA の描画へ渡すには文章へ戻す必要がある。
// 同じ処理は拡張が画面から取り込む時に既に行っている (deepText) ため、それを
// この file に切り出して拡張と取込ページで共有する。二つ目の変換を作らないことが、
// 経路ごとの見た目のズレ (PL-056) を防ぐ唯一の方法。
//
// 取込ページはブラウザ上で動くので、HTML の解釈はブラウザに任せられる。
(function (root) {
  'use strict';
  const UI_SKIP = new Set(['button', 'input', 'select', 'textarea', 'option', 'label', 'style', 'script', 'template', 'noscript']);

  // PL-007: <table> をマークダウン pipe 構文に変換。
  // DOM 経路 (scroll-snap / get_dom_messages) で table が単純改行連結に落ち、
  // python-markdown の tables 拡張がヒットせず保存 HTML で <p> 列にフラット化する
  // 問題に対する変換器。失敗時は空文字を返し、呼び出し側で通常処理にフォールバック。
  function tableToMarkdown(table) {
    try {
      const rowsRaw = [];
      function pushRow(tr) {
        if (!tr) return;
        const cells = [];
        let hasTh = false;
        for (const c of tr.children) {
          const tag = (c.localName || '').toLowerCase();
          if (tag !== 'td' && tag !== 'th') continue;
          if (tag === 'th') hasTh = true;
          let cellText = '';
          (function inline(n) {
            if (!n) return;
            if (n.nodeType === Node.TEXT_NODE) { cellText += (n.nodeValue || ''); return; }
            const tg = (n.localName || '').toLowerCase();
            if (UI_SKIP.has(tg)) return;
            if (tg === 'br') { cellText += ' '; return; }
            if (n.shadowRoot) { for (const cn of n.shadowRoot.childNodes) inline(cn); }
            for (const cn of n.childNodes) inline(cn);
          })(c);
          cellText = cellText.replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
          cells.push(cellText);
        }
        if (cells.length) rowsRaw.push({ cells, hasTh });
      }
      function walkRows(parent) {
        if (!parent) return;
        for (const child of parent.children) {
          const tag = (child.localName || '').toLowerCase();
          if (tag === 'tr') pushRow(child);
          else if (tag === 'thead' || tag === 'tbody' || tag === 'tfoot') walkRows(child);
        }
      }
      walkRows(table);
      if (!rowsRaw.length) return '';
      const colCount = rowsRaw.reduce((m, r) => Math.max(m, r.cells.length), 0);
      if (colCount === 0) return '';
      for (const r of rowsRaw) {
        while (r.cells.length < colCount) r.cells.push('');
      }
      const lines = [];
      const firstIsHeader = rowsRaw[0].hasTh;
      if (firstIsHeader) {
        lines.push('| ' + rowsRaw[0].cells.join(' | ') + ' |');
        lines.push('| ' + new Array(colCount).fill('---').join(' | ') + ' |');
        for (let i = 1; i < rowsRaw.length; i++) {
          lines.push('| ' + rowsRaw[i].cells.join(' | ') + ' |');
        }
      } else {
        lines.push('| ' + new Array(colCount).fill(' ').join(' | ') + ' |');
        lines.push('| ' + new Array(colCount).fill('---').join(' | ') + ' |');
        for (const r of rowsRaw) {
          lines.push('| ' + r.cells.join(' | ') + ' |');
        }
      }
      return '\n\n' + lines.join('\n') + '\n\n';
    } catch (_e) {
      return '';
    }
  }

  function deepText(node) {
    // PL-016: <pre> / <code-block> / class="code-block" を捕捉して
    // 本文中はマーカーで置換、本体は code_blocks 配列に積む。
    // md は marker → 平文戻し、html のみ fenced code block 化する。
    // PL-016 Step 2: 外側 <code-block> custom element 内の
    // div.code-block-decoration > span から言語ラベル ("C"/"HTML"/"Python") を
    // 抽出して code_blocks[idx].lang に格納し、ラベルテキストを本文から除去。
    // renderer (asa_core/renderers.py:_render_code_block_html) が
    // <pre><code class="language-X"> として描画する。
    const codeBlocks = [];
    function collect(n) {
      if (!n) return '';
      let s = '';
      if (n.nodeType === Node.TEXT_NODE) { return n.nodeValue || ''; }
      const tn = (n.localName || '').toLowerCase();
      // PL-078: AI 生成画像 (lh3 /gg/) は <generated-image>/<single-image> 配下の <button> 内
      // <img> に在り、 button が UI_SKIP で空化されるため通常再帰では拾えない。 UI_SKIP 判定より
      // 前にここで gg img を直接マーカー化する (DOM/キャプチャ経路。 network 経路は別途 placeholder
      // →マーカー)。 gg でない画像 (web 引用 tbn 等) は素通しし従来挙動を維持。
      if (tn === 'generated-image' || tn === 'single-image') {
        try {
          const _img = n.querySelector('img');
          const _gsrc = _img ? (_img.getAttribute('src') || _img.currentSrc || '') : '';
          // <generated-image>/<single-image> は content 画像専用 wrapper (avatar/logo は別構造)。
          // http(s) (gg 生成 / encrypted-tbn web 引用) と blob: (Gemini が表示用に blob 化した生成画像) を
          // マーカー化する。 http は background が取得、 blob はページローカルのため content.js が取得する
          // (取得経路は pl078gRequestImages が URL scheme で振り分け)。 data: は既に inline なので対象外。
          if (/^(https?|blob):/i.test(_gsrc)) {
            // PL-088/PL-090: web 引用画像は overlay (div.overlay-container.hero-overlay-container)
            // に説明 caption を持つ。 単一 marker で即 return すると subtree を再帰せず caption が
            // 落ちるため、 hero overlay の説明文を拾い marker の後ろに添える (本家も web 引用画像に
            // この説明 caption を出す)。
            // PL-090: 出典名 (ソース: <名>) は DOM に存在せず network turn[3] の構造データ側にのみ
            // 在ると probe で確定 (capture では取得不可)。 旧実装は 'ソース' を含む時だけ caption 化
            // していたため、 出典が DOM に無い web 引用画像で説明 caption ごと落ちていた → 'ソース'
            // 条件を撤去し overlay 説明文があれば添える。 hero overlay 限定で生成画像/商品カードは対象外。
            const _ov = n.querySelector('.overlay-container.hero-overlay-container');
            const _cap = _ov ? String(_ov.textContent || '').replace(/[*\[\]]/g, '').replace(/\s+/g, ' ').trim() : '';
            if (_cap) {
              return '[[ASA_GEMIMG:' + _gsrc + ']]\n\n[[ASA_IMGCAP:' + _cap + ']]';
            }
            return '[[ASA_GEMIMG:' + _gsrc + ']]';
          }
        } catch (_e) {}
      }
      // PL-123: 一部アカウントの共有ショッピングカードは新 UI `rich-list-card` で描画され、
      // 商品画像・商品名・価格・評価が <button class="card-data"> 内に在る。 button は UI_SKIP で
      // 空化されるため card 全体が落ちる (PL-078「画像がボタン内で落ちる」の新パターン)。
      // UI_SKIP 判定より手前で card 構造から直接組み立てる (旧 product-card 描画のアカウントは
      // rich-list-card 不在のため不発火・無影響)。 サムネは shopping gstatic のため既存 shopping
      // レール ([[ASA_GEMIMG:]] → pl078gNormalizeImages が inline src-ref 化) に乗せる。
      if (tn === 'rich-list-card') {
        try {
          const _thumb = n.querySelector('img.thumbnail');
          const _src = _thumb ? (_thumb.getAttribute('src') || _thumb.currentSrc || '') : '';
          const _titleEl = n.querySelector('.card-info .title') || n.querySelector('.title');
          const _title = _titleEl ? String(_titleEl.textContent || '').replace(/\s+/g, ' ').trim() : '';
          const _meta = Array.from(n.querySelectorAll('rich-list-row .row-text'))
            .map(e => String(e.textContent || '').trim()).filter(Boolean).join(' ')
            .replace(/\s+/g, ' ').trim();
          let _info = _title;
          if (_meta) { _info = (_info ? _info + ' — ' : '') + _meta; }
          _info = _info.replace(/[*\[\]]/g, '').trim();
          const _lines = [];
          if (_src) { _lines.push('[[ASA_GEMIMG:' + _src + ']]'); }
          if (_info) { if (_src) { _lines.push(''); } _lines.push('*' + _info + '*'); }
          if (_lines.length) { return '\n' + _lines.join('\n') + '\n'; }
        } catch (_e) {}
        return '';
      }
      if (UI_SKIP.has(tn)) return '';
      if (tn === 'br') return '\n';
      // a11y screen-reader-only ノード除去:
      //   `cdk-visually-hidden` (Angular Material),
      //   `screen-reader-user-query-label` (「あなたのプロンプト」/「Your prompt」 label),
      //   `screen-reader-only` 系 class を持つ要素は subtree ごと skip。
      // mixed turn (添付 + 本文) で `[添付ファイル]` 注入後の text 中に
      // role label が残り stripRoleLabel の startsWith 判定を抜ける問題への
      // 根本対策 (要素単位除去で行 filter 不要化)。
      const _classStr = (typeof n.className === 'string') ? n.className : '';
      if (_classStr && /\bcdk-visually-hidden\b|\bscreen-reader-/.test(_classStr)) {
        return '';
      }
      // PL-017: file-only upload (本文空 + 添付のみ) で user 発言が消失する問題対応。
      // file 名 / 拡張子 label が <button> 配下にあり UI_SKIP で空文字化されるため、
      // user-query-file-preview 到達時に aria-label からファイル名を抽出して
      // 本文 text に [添付ファイル: <name>] 形式で注入する。
      if (tn === 'user-query-file-preview') {
        const btn = n.querySelector('button[aria-label]');
        const name = btn ? (btn.getAttribute('aria-label') || '').trim() : '';
        if (name) return '\n[添付ファイル: ' + name + ']\n';
        const nameDiv = n.querySelector('.new-file-name');
        const typeDiv = n.querySelector('.new-file-type');
        const fname = nameDiv ? (nameDiv.textContent || '').trim() : '';
        const ftype = typeDiv ? (typeDiv.textContent || '').trim() : '';
        if (fname || ftype) {
          const combined = fname + (ftype ? '.' + ftype.toLowerCase() : '');
          return '\n[添付ファイル: ' + combined + ']\n';
        }
        return '';
      }
      if (tn === 'table') {
        const md = tableToMarkdown(n);
        if (md) return md;
      }
      if (n.shadowRoot) { for (const c of n.shadowRoot.childNodes) { s += collect(c); } }
      for (const c of n.childNodes) { s += collect(c); }
      const cls = (typeof n.className === 'string') ? n.className : '';
      const hasCodeBlockClass = cls && cls.split(/\s+/).indexOf('code-block') !== -1;
      // 既に内側で wrap 済みなら二重 wrap しない（最内 pre/code-block を優先）
      const alreadyWrapped = s.indexOf('[[ASA_CODE_BLOCK_') !== -1;
      // PL-016 Step 2: 外側 <code-block> 到達時に lang ラベルを抽出して内側 marker に
      // 紐付け、ラベルテキストを drop して markers のみ通す。
      if (tn === 'code-block' && alreadyWrapped) {
        const markers = s.match(/\[\[ASA_CODE_BLOCK_\d+\]\]/g) || [];
        if (markers.length) {
          const dec = n.querySelector('.code-block-decoration');
          const lang = dec ? String(dec.textContent || '').trim().toLowerCase() : '';
          if (lang) {
            const m = markers[0].match(/\[\[ASA_CODE_BLOCK_(\d+)\]\]/);
            if (m) {
              const idx = parseInt(m[1], 10);
              if (codeBlocks[idx]) codeBlocks[idx].lang = lang;
            }
          }
          return '\n' + markers.join('\n') + '\n';
        }
        return '';
      }
      if ((tn === 'pre' || tn === 'code-block' || hasCodeBlockClass) && !alreadyWrapped) {
        const idx = codeBlocks.length;
        codeBlocks.push({ body: s.replace(/^\n+|\n+$/g, ''), lang: '' });
        return '\n[[ASA_CODE_BLOCK_' + idx + ']]\n';
      }
      // PL-053 段 C (Codex Round 7 verdict): DOM-to-markdown 復元拡張
      // 新規会話の template 不在 state で `collectConversation` が DOM fallback に落ちた時の
      // 保存品質を上げるための practical fallback。 inline / block markdown 6 種を復元。
      // 既存の table / code block marker / attachment placeholder path は維持。
      if (tn === 'strong' || tn === 'b') {
        return s ? '**' + s + '**' : '';
      }
      if (tn === 'em' || tn === 'i') {
        return s ? '*' + s + '*' : '';
      }
      if (tn === 'code') {
        // pre / code-block / .code-block 配下の code は既存 marker path で処理済、 markdown 化 skip
        let _p = n.parentElement;
        let inCodeBlock = false;
        while (_p) {
          const _pt = (_p.localName || '').toLowerCase();
          const _pcls = (typeof _p.className === 'string') ? _p.className : '';
          if (_pt === 'pre' || _pt === 'code-block' || /\bcode-block\b/.test(_pcls)) {
            inCodeBlock = true;
            break;
          }
          _p = _p.parentElement;
        }
        if (!inCodeBlock) {
          const _code = s.trim();
          return _code ? '`' + _code + '`' : '';
        }
      }
      if (tn === 'a') {
        const href = n.getAttribute('href') || '';
        const txt = s.trim();
        const isJsHref = /^javascript:/i.test(href);
        // PL-087: <a> が画像や複数行を内包する場合 (= ショッピングカード全体を包む anchor 等)、
        // markdown の inline link `[txt](href)` はリンク文字に画像/改行を持てず不正で、 python-markdown が
        // 解析失敗して `[` と生 href を露出させる。 この場合はリンクで包まず内容のみ返す (href は破棄。
        // 商品への遷移はカード直前の商品名リンクが担う)。 単純な inline テキストのみ従来どおりリンク化。
        const isComplexContent = /\n|\[\[ASA_(?:GEMIMG|CODE_BLOCK)/.test(s);
        if (isComplexContent) {
          return s;
        }
        if (href && !isJsHref && txt && href !== txt) {
          // PL-123: href 内の生 ( ) は markdown リンク解析を壊し PDF/HTML で URL 末尾が
          // 本文流出する (PL-103 と同型。 当時は network 経路 pl086ResolveShopping のみ対処、
          // 共有=DOM 経路の product-link が未対処で再発)。 %28/%29 へエンコードして安全化
          // (リンク機能は維持・サーバはデコード)。
          const safeHref = href.replace(/\(/g, '%28').replace(/\)/g, '%29');
          return '[' + txt + '](' + safeHref + ')';
        }
        return s;
      }
      // PL-053 段 C 拡張: img tag を markdown image link 化 + placeholder text 復元で turn drop 防止。
      // 公開 URL (http/https、 data: / blob: 除外) = clickable な `![alt](src)` 化、 出力 file で画像表示可能。
      // internal placeholder URL (= `image_agent_tag_*` site 内部 ID etc) = text placeholder で復元
      // (= AI 画像生成 turn が `model-response` 子 img のみで text 0 件のとき turn drop する症状を防ぐ、
      // 既存 PL-051 network 経路 `[AI生成]` placeholder contract と整合)。
      if (tn === 'img') {
        const src = n.getAttribute('src') || '';
        const alt = (n.getAttribute('alt') || '').trim();
        // PL-078: AI 生成画像 (lh3 /gg/) は中間マーカー化 → buildSnapshot で image_blocks 化、
        // background が取得して埋め込む (短命 URL の死にリンク ![](lh3) を回避)。
        if (/lh3\.googleusercontent\.com\/gg(?:-dl)?\//i.test(src)) {
          return '[[ASA_GEMIMG:' + src + ']]';
        }
        const isDataBlob = /^data:/i.test(src) || /^blob:/i.test(src);
        const isPublicUrl = /^https?:\/\//i.test(src) && !isDataBlob;
        if (isPublicUrl) {
          return '![' + (alt || '画像') + '](' + src + ')';
        }
        const isAiGenerated = /^image_agent_tag_/i.test(src);
        const label = isAiGenerated ? 'AI生成' : '画像';
        return alt ? '[' + label + ': ' + alt + ']' : '[' + label + ']';
      }
      if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tn)) {
        const level = parseInt(tn.slice(1), 10);
        const prefix = '#'.repeat(level);
        const _h = s.trim();
        return _h ? '\n\n' + prefix + ' ' + _h + '\n\n' : '';
      }
      if (tn === 'blockquote') {
        // PL-079: Gemini は blockquote を引用バー無しで描画するため `>` を付けず
        // 普通の block として出す (network 経路 pl079DeBlockquote と対称)。 内部の
        // 段落↔list 間の空行は子 p/li の collect 出力が既に供給する。
        const _q = s.trim();
        return _q ? '\n' + _q + '\n' : '';
      }
      if (tn === 'li') {
        const parent = n.parentElement;
        const parentTag = parent ? (parent.localName || '').toLowerCase() : '';
        const _li = s.trim();
        if (parentTag === 'ol') {
          const siblings = Array.from(parent.children).filter(c => (c.localName || '').toLowerCase() === 'li');
          const idx = siblings.indexOf(n) + 1;
          return '\n' + idx + '. ' + _li;
        }
        return '\n- ' + _li;
      }
      if (['p', 'div', 'section', 'tr', 'td', 'th'].includes(tn)) {
        return '\n' + s + '\n';
      }
      return s;
    }
    const raw = collect(node);
    const text = String(raw).replace(/\r/g, '').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
    // PL-053 段 C 包括 fallback: text 0 件 + 画像系 element (= img / picture / canvas / video)
    // 検出時 `[AI生成]` placeholder で turn drop 防止 (= img handler 単独では site が img でない別
    // element で画像 render する case を救えず、 純画像生成 turn 完全消失する症状を防ぐ)。
    if (!text) {
      try {
        if (node && node.querySelector && node.querySelector('img, picture, canvas, video')) {
          return { text: '[AI生成]', code_blocks: codeBlocks };
        }
      } catch (_e) {}
    }
    return { text: text, code_blocks: codeBlocks };
  }

  // 取込用: 1 会話ぶんの記録 (user の送信文 + Gemini の返答 HTML) を発言列にする。
  // 入力は Python 側が組み立てた {title, records:[{prompt, html, ts}]}。
  function parseGeminiActivityConversation(jsonText) {
    let data;
    try { data = JSON.parse(String(jsonText || '')); } catch (_e) { return null; }
    const records = data && data.records;
    if (!Array.isArray(records) || !records.length) return null;
    const parser = new DOMParser();
    const messages = [];
    records.forEach(function (rec, i) {
      const prompt = String((rec && rec.prompt) || '').trim();
      if (prompt) {
        messages.push({ role: 'user', text: prompt, code_blocks: [], msg_id: 'g' + i + 'u' });
      }
      const html = String((rec && rec.html) || '');
      if (!html.trim()) return;
      const doc = parser.parseFromString('<div id="asa-root">' + html + '</div>', 'text/html');
      const rootEl = doc.getElementById('asa-root');
      if (!rootEl) return;
      const dt = deepText(rootEl);
      const text = String((dt && dt.text) || '').trim();
      if (!text) return;
      messages.push({
        role: 'assistant', text: text,
        code_blocks: (dt && dt.code_blocks) || [], msg_id: 'g' + i + 'a',
      });
    });
    return messages.length ? messages : null;
  }

  root.ASA_GEMINI_PARSE = {
    parseGeminiActivityConversation: parseGeminiActivityConversation,
    deepText: deepText,
  };
})(typeof globalThis !== 'undefined' ? globalThis : this);
