// 对话内的工具活动标签 + artifact(产物)抽取与渲染:
// toolActivityLabel(工具调用→中文活动名)、extractArtifactRels(从结果文本/working_dir 提产物相对路径)、
// extractMediaBanner(seedream/seedance 媒体横幅)、renderArtifactBarHtml(产物 chip 条,图/视频/HTML 内联占位)、
// upgradeMediaArtifacts(可视区内异步填 <img>/<video>/<iframe>,带缓存)、downloadFile(blob 下载)。
// 供 main 的 renderMessages 及 preview/files(downloadFile)使用。
import { state } from "./state.js";
import { escapeHtml } from "./format.js";
import { _categorize } from "./preview.js";
import { configureHtmlPreviewFrame } from "./preview_content.js";
import { logout } from "./auth.js";
import { message } from "./dialog.js";

// ───── artifact 抽取(对话内 chip → 复用文件预览 modal) ─────
// task.working_dir 在 DB 是 `workspace/users/<uuid>/<name>` 形态(to_db_path),
// 不是 user_root 相对。这里取最后一段作为 chip 抽取锚点 —— 等价于 user_root 下
// 一级子目录名(同 filesPath 的 wdName 语义)。外部 --working-dir 是绝对路径,
// 文件不在 user_root,backend files API 拒访问 → 不挂 chip。
// 把一次 tool_call 翻成一句中文活动描述(展示在折叠标题行,不展开就能看懂在干啥)。
// args 是后端 _execute_tool_call 解析后的参数 dict;取每个工具最能代表"在干啥"的字段。
export function toolActivityLabel(name, args) {
  const a = (args && typeof args === "object") ? args : {};
  const clip = (v, n) => {
    const s = String(v == null ? "" : v).replace(/\s+/g, " ").trim();
    return s.length > n ? s.slice(0, n) + "…" : s;
  };
  switch (name) {
    case "read":       return `读取文件: ${clip(a.path, 80)}`;
    case "write":      return `写入文件: ${clip(a.path, 80)}`;
    case "edit":       return `编辑文件: ${clip(a.path, 80)}`;
    case "glob":       return `查找文件: ${clip(a.pattern, 60)}`;
    case "grep":       return `搜索内容: ${clip(a.pattern, 60)}`;
    case "shell":      return `执行命令: ${clip(a.command, 80)}`;
    case "run_python": return `运行 Python: ${clip(a.code, 80)}`;
    case "web_fetch":  return `抓取网页: ${clip(a.url, 80)}`;
    case "web_search": return `联网搜索: ${clip(a.query, 60)}`;
    case "load_skill": return `加载技能: ${clip(a.name, 40)}`;
    case "rename_working_dir": return `重命名工作目录: ${clip(a.new_name, 60)}`;
    case "publish_artifacts": return `发布最终产物: ${clip(JSON.stringify(a.artifacts || []), 80)}`;
    case "publish_web_preview": return `发布网页项目预览: ${clip(a.directory, 80)}`;
    case "office_to_pdf": return `转换 PDF: ${clip(a.source, 80)}`;
    case "seedream":   return `生成图像: ${clip(a.prompt, 60)}`;
    case "gpt_image":  return `生成图像: ${clip(a.prompt, 60)}`;
    case "seedance":   return `生成视频: ${clip(a.prompt, 60)}`;
    default: {
      const p = clip(JSON.stringify(a), 80);
      return p && p !== "{}" ? `${name} ${p}` : `工具调用: ${name}`;
    }
  }
}

export function _workingDirName(workingDir) {
  if (!workingDir) return "";
  const wd = String(workingDir).replace(/\\+/g, "/");
  if (wd.startsWith("/") || /^[A-Za-z]:/.test(wd)) return "";  // 绝对 = 外部目录,跳过
  const segs = wd.split("/").filter(Boolean);
  return segs[segs.length - 1] || "";
}

// 产物工具白名单:**工具 I/O** 维度,只有这些工具的 tool_call args / tool_result
// 里 echo 的路径才挂 chip 条 + 图片/视频 inline 大图;通用工具(grep/read/glob/
// shell)echo 的路径是"引用"不是"产物",完全不挂(避免把 grep 命中的老 figures/
// foo.png 当新产物展示)。**assistant 正文不受此限** —— 助手回复里任何 echo 的
// 路径无条件挂 chip(`allowInlineMedia=false`,只 chip 不 inline,跟上面 tool 结果
// 可能已 inline 的同图不冲突);用户视角"助手提到的文件理应能点开",chip 是
// 可发现性入口,小图标无视觉污染。
// 注:与 extractMediaBanner 的"媒体 banner"白名单是不同维度 —— 将来若新增
// "生成 docx 的工具",入这里但不入 banner 白名单。
export const ARTIFACT_PRODUCING_TOOLS = new Set(["seedream", "gpt_image", "seedance"]);

// 从 tool args / result / assistant 正文里抓 working_dir 下的文件路径,归一为 user_root 相对。
// 启发式:把 \ 一律归 /,然后找以 `<wdName>/` 打头的串,要求最后一段含 . (像文件)。
// 从 seedream/seedance tool_result 第一行 banner 抽 model/size/cost/elapsed,
// 拼一行 .tool-banner HTML 挂在 details summary 旁。匹配失败返 ""(不渲染)。
// 协议:tool 返回串首行格式 `[<tool>] key=value · key=value · ...`
export function extractMediaBanner(toolName, resultText) {
  if (!resultText) return "";
  if (toolName !== "seedream" && toolName !== "gpt_image" && toolName !== "seedance") return "";
  const firstLine = String(resultText).split("\n", 1)[0] || "";
  // 抓 key=value(value 可含空格 / : / ., 用 · 或行尾结束)
  const re = /(\w+)=([^·\n]+?)(?=\s*·|\s*$)/g;
  const kvs = {};
  let m;
  while ((m = re.exec(firstLine)) !== null) {
    kvs[m[1]] = m[2].trim();
  }
  if (!kvs.model && !kvs.cost) return "";
  // model 文本太长(`doubao-seedream-5-0-260128`)→ 截短易读形式
  const model = (kvs.model || "").replace(/^doubao-/, "").replace(/-\d{6,}$/, "");
  const parts = [];
  if (model) parts.push(`<span class="kv model">${escapeHtml(model)}</span>`);
  if (kvs.size) parts.push(`<span class="kv">${escapeHtml(kvs.size)}</span>`);
  if (kvs.cost) parts.push(`<span class="kv cost">${escapeHtml(kvs.cost)}</span>`);
  if (kvs.elapsed) parts.push(`<span class="kv">${escapeHtml(kvs.elapsed)}</span>`);
  return parts.length ? `<span class="tool-banner">${parts.join("")}</span>` : "";
}

// 路径 tail 排除集:空白/引号/括号类 + 常见中文标点 —— 中文正文里路径后常紧贴
// "，请查看"这类标点(无空格),不排除会把标点连后文一起吞进文件名,生成点开 404
// 的坏 chip。文件名几乎不含这些中文标点,排除的误伤面≈0。
// 注:间隔号 `·`(U+00B7)**不**排除 —— 它是文件名内部的合法分隔符(产品名
// "AI赋能·xxx.pptx" 常见),几乎不当句尾标点用;排除它会在 `·` 处截断路径,把
// 真产物 chip 吞没(踩过:含 `·` 的 pptx 不挂 chip)。尾部标点另有 strip 兜底。
// `*` 排除:助手常把路径写成 markdown 粗体 `**<wd>/x.md**`,前导 `**` 已被 lead
// 边界吃掉,但尾部 `**` 会被贪婪吞进 rel(星号既不在本集也不在尾部 strip 集),
// 生成 `x.md**` 这种点开 404 的坏 chip(踩过:task d9c74a5a)。文件名含 `*` 在
// Windows 非法、实际几乎不存在,排除误伤≈0,同反引号一样在 emphasis 处干净截断。
const _TAIL_CLS = "[^\\s\"'`*<>(){}\\[\\]|，。、；：！？…“”‘’《》〈〉【】（）「」『』]+";

export function extractArtifactRels(text, workingDir) {
  if (!text || !workingDir) return [];
  const wd = String(workingDir).replace(/\\+/g, "/").replace(/^\/+|\/+$/g, "");
  if (!wd) return [];
  const norm = String(text).replace(/\\+/g, "/");
  const wdEsc = wd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const seen = new Set();
  const out = [];

  // 规范形式:<wdName>/<...>/<file>.<ext> —— 当前协议(system prompt 强约束助手照抄 tool `saved:` 行)
  // lead 边界:行首或任意非 [A-Za-z0-9._-] 字符 —— 汉字也算边界(中文正文"已保存
  // 到gpt生图/xxx.png"路径前紧贴汉字是常态),同时仍防 "mygpt生图/..." 前缀误挂;
  // tail 截到空白/引号/括号/中文标点(见 _TAIL_CLS)
  {
    const re = new RegExp(
      "(?:^|[^A-Za-z0-9._-])(" + wdEsc + "/" + _TAIL_CLS + ")",
      "g"
    );
    let m;
    while ((m = re.exec(norm)) !== null) {
      let rel = m[1];
      rel = rel.replace(/[.,;:!?)\]}>。,;:!?)]+$/, "");  // 剥尾标点(中英)
      const tail = rel.slice(wd.length + 1);
      if (!tail) continue;
      const last = tail.split("/").pop() || "";
      if (!last.includes(".")) continue;  // 看着像目录的不挂 chip
      if (seen.has(rel)) continue;
      seen.add(rel);
      out.push(rel);
    }
  }

  // ───── 一次性兼容:协议刚性化前的历史简写消息 ─────
  // system prompt 改硬约束之前,助手按 SKILL 旧文案 echo 过 `videos/xxx.mp4` /
  // `figures/xxx.png` 这种裸形式 —— 那些消息已存 DB 改不动,前端这里 prepend
  // <wdName> 把它们拼成 user_root rel 才能挂 chip。**白名单显式枚举不扩展**:
  // 新产物 skill 走 system 协议必出全形式,这一层只服务**历史消息**渲染。
  // 长期(老消息归档/不再回看)整段可删。
  const LEGACY_PRODUCT_DIRS = ["videos", "figures"];
  for (const dir of LEGACY_PRODUCT_DIRS) {
    const dirEsc = dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    // lead 边界跟主规则一致但额外排 `/` —— 否则 <wd>/videos/xxx 里的 videos/xxx 会被重复
    // 匹配(虽然 seen 去重,但浪费 cycles)
    const re = new RegExp(
      "(?:^|[^A-Za-z0-9._/-])(" + dirEsc + "/" + _TAIL_CLS + ")",
      "g"
    );
    let m;
    while ((m = re.exec(norm)) !== null) {
      let tail = m[1];
      tail = tail.replace(/[.,;:!?)\]}>。,;:!?)]+$/, "");
      const last = tail.split("/").pop() || "";
      if (!last.includes(".")) continue;
      const rel = wd + "/" + tail;
      if (seen.has(rel)) continue;
      seen.add(rel);
      out.push(rel);
    }
  }
  return out;
}

// 文件 chip 的内容结构保持纯前端生成：扩展名充当轻量类型图标，不额外请求
// 文件元数据；长文件名仍由 CSS 单行省略，完整路径保留在 button title 中。
export function renderArtifactChipContent(name, rel, action = "预览") {
  const rawExt = String(rel || "").split("/").pop().split(".").pop();
  const hasExt = rawExt && rawExt !== String(rel || "").split("/").pop();
  const ext = hasExt ? rawExt.toUpperCase().slice(0, 4) : "FILE";
  return `<span class="art-chip-icon" aria-hidden="true">${escapeHtml(ext)}</span>`
    + `<span class="art-chip-name">${escapeHtml(name)}</span>`
    + `<span class="art-chip-open">${escapeHtml(action)}</span>`;
}

// inlineMode 控制升级范围:true=图片/视频/HTML，"html"=仅 HTML，false=全走 chip。
// 产物工具传 true；assistant 正文传 "html"，避免重复内联图片/视频但让通用工具生成的
// HTML 有展示位；普通工具结果传 false，引用到的文件仍走 chip，避免把旧产物铺满消息。
export function renderArtifactBarHtml(rels, inlineMode = true, taskId = "", legacy = false) {
  if (!rels || !rels.length) return "";
  const taskAttr = taskId ? ` data-task-id="${escapeHtml(taskId)}"` : "";
  const legacyAttr = legacy ? ` data-legacy-path="1"` : "";
  const items = rels.map((item) => {
    const ref = (item && typeof item === "object") ? item : { path: item };
    const rel = String(ref.path || "");
    const artifactAttr = ref.artifact_id
      ? ` data-artifact-id="${escapeHtml(String(ref.artifact_id))}"`
      : "";
    const previewAttr = ref.preview_url
      ? ` data-preview-url="${escapeHtml(String(ref.preview_url))}"`
      : "";
    if (!rel) return "";
    const name = String(ref.label || rel.split("/").pop() || rel);
    const cat = _categorize(rel);
    if ((inlineMode === true || inlineMode === "html") && cat === "html") {
      return `<section class="art-html" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr}${artifactAttr} title="${escapeHtml(rel)}">
        <div class="art-html-head"><span>${escapeHtml(name)}</span><button type="button" class="art-html-open" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr}${artifactAttr}>放大</button></div>
        <div class="art-html-viewport"><span class="art-media-loading">进入可视区域后加载…</span></div>
      </section>`;
    }
    if (inlineMode === true && (cat === "image" || cat === "video")) {
      // 占位元素;插入 DOM 后 upgradeMediaArtifacts 异步 fetch blob → 填 <img>/<video>。
      // 不在这里发请求避免 string-build 阶段失控的并发;upgrade 走 DOM walk 一次。
      const media = `<span class="art-media art-media-${cat}" data-rel="${escapeHtml(rel)}" data-cat="${cat}"${taskAttr}${legacyAttr}${artifactAttr}${previewAttr} title="${escapeHtml(rel)}"><span class="art-media-loading">${escapeHtml(name)} 加载中…</span></span>`;
      // 新 workspace 预览已经回传到 task 文件夹：图片负责快速查看，弱化 chip
      // 明确文件确实存在且可打开。历史预览只有裸文件名、仍在隐藏缓存中，不生成
      // 一个无法通过 task files API 打开的 chip。
      if (ref.kind === "software_preview" && rel.includes("/")) {
        const chip = `<button type="button" class="art-chip software-preview-chip" data-rel="${escapeHtml(rel)}"${taskAttr} title="${escapeHtml(rel)} · 预览文件(可下载)">${renderArtifactChipContent(name, rel, "预览文件")}</button>`;
        return `<span class="software-preview-file">${media}${chip}</span>`;
      }
      return media;
    }
    return `<button type="button" class="art-chip" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr}${artifactAttr} title="${escapeHtml(rel)} · 点击预览(可下载)">${renderArtifactChipContent(name, rel)}</button>`;
  }).join("");
  return `<div class="artifact-bar">${items}</div>`;
}

// rel → Promise<blob-url>。auth 是 Bearer header,不能直接 <img src=>,只能 fetch
// 拿 blob 再转 URL。同 rel 在同会话内复用,免重复拉。task 切换 / logout 时
// _flushMediaArtifactCache 清掉旧 URL 防泄漏。
const _mediaArtifactCache = new Map();
const _htmlArtifactCache = new Map();
const INLINE_HTML_MAX = 2 * 1024 * 1024;

function _artifactDownloadUrl(rel, taskId = "", legacy = false, artifactId = "") {
  const base = taskId
    ? `/v1/tasks/${encodeURIComponent(taskId)}/files/download`
    : "/v1/files/download";
  return base + "?path=" + encodeURIComponent(rel) + (legacy ? "&legacy=true" : "")
    + (artifactId ? "&artifact_id=" + encodeURIComponent(artifactId) : "");
}

function _fetchMediaBlobUrl(rel, taskId = "", legacy = false, artifactId = "") {
  const key = `${taskId}:${artifactId}:${legacy ? "legacy:" : ""}${rel}`;
  if (_mediaArtifactCache.has(key)) return _mediaArtifactCache.get(key);
  const p = fetch(_artifactDownloadUrl(rel, taskId, legacy, artifactId), {
    headers: { "Authorization": "Bearer " + state.token },
  }).then(async (r) => {
    if (!r.ok) throw new Error("HTTP " + r.status);
    const blob = await r.blob();
    return URL.createObjectURL(blob);
  });
  _mediaArtifactCache.set(key, p);
  return p;
}

function _fetchPreviewBlobUrl(url) {
  const key = `preview:${url}`;
  if (_mediaArtifactCache.has(key)) return _mediaArtifactCache.get(key);
  const p = fetch(url, {
    headers: { "Authorization": "Bearer " + state.token },
  }).then(async (r) => {
    if (!r.ok) throw new Error("HTTP " + r.status);
    return URL.createObjectURL(await r.blob());
  });
  _mediaArtifactCache.set(key, p);
  return p;
}

function _fetchHtmlArtifact(rel, taskId = "", legacy = false, artifactId = "") {
  const key = `${taskId}:${artifactId}:${legacy ? "legacy:" : ""}${rel}`;
  if (_htmlArtifactCache.has(key)) return _htmlArtifactCache.get(key);
  const p = fetch(_artifactDownloadUrl(rel, taskId, legacy, artifactId), {
    headers: { "Authorization": "Bearer " + state.token },
  }).then(async (r) => {
    if (!r.ok) throw new Error("HTTP " + r.status);
    const blob = await r.blob();
    if (blob.size > INLINE_HTML_MAX) throw new Error("文件过大，请点击放大预览");
    return blob.text();
  });
  _htmlArtifactCache.set(key, p);
  return p;
}

let _htmlArtifactObserver = null;
function _loadInlineHtml(node) {
  if (!node || node.dataset.loaded) return;
  node.dataset.loaded = "1";
  const rel = node.dataset.rel;
  const taskId = node.dataset.taskId || "";
  const legacy = node.dataset.legacyPath === "1";
  const viewport = node.querySelector(".art-html-viewport");
  _fetchHtmlArtifact(rel, taskId, legacy, node.dataset.artifactId || "").then((source) => {
    if (!node.isConnected || !viewport) return;
    viewport.innerHTML = "";
    const frame = document.createElement("iframe");
    frame.className = "art-html-frame";
    configureHtmlPreviewFrame(frame, source, (rel.split("/").pop() || rel) + " 内嵌预览");
    viewport.appendChild(frame);
  }).catch((e) => {
    if (!viewport) return;
    viewport.innerHTML = `<span class="art-media-error">${escapeHtml(e.message || String(e))}</span>`;
  });
}

function _observeInlineHtml(node) {
  if (typeof IntersectionObserver !== "function") {
    _loadInlineHtml(node);
    return;
  }
  if (!_htmlArtifactObserver) {
    _htmlArtifactObserver = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (!entry.isIntersecting) continue;
        _htmlArtifactObserver.unobserve(entry.target);
        _loadInlineHtml(entry.target);
      }
    }, { rootMargin: "240px 0px" });
  }
  _htmlArtifactObserver.observe(node);
}

export function _flushMediaArtifactCache() {
  for (const p of _mediaArtifactCache.values()) {
    p.then((u) => URL.revokeObjectURL(u)).catch(() => {});
  }
  _mediaArtifactCache.clear();
  _htmlArtifactCache.clear();
  if (_htmlArtifactObserver) _htmlArtifactObserver.disconnect();
  _htmlArtifactObserver = null;
}

// DOM walk:把所有 .art-media[data-rel] 占位换成 <img> / <video>。
// renderMessages / SSE 插入完后调一次;重复调用幂等(已 upgrade 过的 set data-upgraded 跳过)。
export function upgradeMediaArtifacts(root) {
  const nodes = (root || document).querySelectorAll(".art-media[data-rel]:not([data-upgraded])");
  nodes.forEach((node) => {
    node.dataset.upgraded = "1";
    const rel = node.dataset.rel;
    const taskId = node.dataset.taskId || "";
    const legacy = node.dataset.legacyPath === "1";
    const cat = node.dataset.cat;
    const load = node.dataset.previewUrl
      ? _fetchPreviewBlobUrl(node.dataset.previewUrl)
      : _fetchMediaBlobUrl(rel, taskId, legacy, node.dataset.artifactId || "");
    load.then((url) => {
      node.innerHTML = "";
      if (cat === "image") {
        const img = document.createElement("img");
        img.src = url;
        img.alt = rel.split("/").pop() || rel;
        img.loading = "lazy";  // 浏览器懒解码(已在 viewport 内立即可见,远处暂不解)
        node.appendChild(img);
      } else if (cat === "video") {
        const v = document.createElement("video");
        v.src = url;
        v.controls = true;
        v.preload = "metadata";
        node.appendChild(v);
      }
    }).catch((e) => {
      node.innerHTML = `<span class="art-media-error">${escapeHtml(rel.split("/").pop() || rel)} 加载失败:${escapeHtml(e.message || String(e))}</span>`;
    });
  });
  const htmlNodes = (root || document).querySelectorAll(".art-html[data-rel]:not([data-upgraded])");
  htmlNodes.forEach((node) => {
    node.dataset.upgraded = "1";
    _observeInlineHtml(node);
  });
}

export function downloadFile(rel, taskId = "", legacy = false, artifactId = "") {
  const base = taskId
    ? `/v1/tasks/${encodeURIComponent(taskId)}/files/download`
    : "/v1/files/download";
  fetch(base + "?path=" + encodeURIComponent(rel) + (legacy ? "&legacy=true" : "")
    + (artifactId ? "&artifact_id=" + encodeURIComponent(artifactId) : ""), {
    headers: { "Authorization": "Bearer " + state.token },
  }).then(async (r) => {
    if (!r.ok) { message("下载失败:" + r.status, "error"); return; }
    const blob = await r.blob();
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = rel.split("/").pop() || "file";
    document.body.appendChild(a); a.click();
    setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
  });
}

