// 后台进程(bg proc,DESIGN §8.12)前端:工具卡活化 + 完成 toast + 停止按钮。
// 数据源 GET /v1/procs(用户级,纯文件系统读取)。轮询式:有 running proc 时每 5s
// 一拉,否则只靠触发点刷新(登录/选 task/run 收尾/[Background] 工具结果)——
// 不走 SSE:proc 完成时刻往往没有活跃 run,SSE 通道根本不在;轮询成本忽略不计。
//
// 展示沿用工具卡体验(用户反馈:别另起顶部条):[Background] 的工具结果卡本身
// 变活卡 —— .running spinner + summary 跳秒 + 停止按钮,终态定格 exit/耗时。
// 历史重渲(刷新页面)同样活化,updateCards 按最新拉取校正状态。
// toast 用户级:切到别的任务也能收到完成提示,点击跳转对应任务。
import { api } from "./api.js";
import { state } from "./state.js";
import { $ } from "./dom.js";
import { escapeHtml } from "./format.js";
import { selectTask, syncBgprocLock } from "./chat.js";
import { dialogConfirm } from "./dialog.js";

const POLL_MS = 5000;
let _pollTimer = null;
let _tickTimer = null;
let _known = new Map();   // proc_id -> 上次见到的 status(跨拉取比对出 running→终态)
let _byId = new Map();    // proc_id -> 最新 proc 对象(kill 需要 task_id / 卡片更新用)
let _fetchedAt = 0;       // 上次拉取时刻(秒数本地推进的基准)

export async function refreshProcs() {
  if (!state.token) return;
  let data;
  try { data = await api("GET", "/v1/procs"); } catch (e) { return; }  // 轮询失败静默
  const procs = data.procs || [];
  procs.forEach((p) => {
    const prev = _known.get(p.proc_id);
    if (prev === "running" && p.status !== "running") notifyDone(p);
    _known.set(p.proc_id, p.status);
    _byId.set(p.proc_id, p);
  });
  _fetchedAt = Date.now();
  updateCards();
  syncBgprocLock();  // 对话锁:本 task 有 running proc → composer 锁定;结束的那次拉取解锁
  if (procs.some((p) => p.status === "running")) startPolling(); else stopPolling();
}

// 本 task 是否有 running 后台进程(chat.js 对话锁 / Enter 拦截用)
export function hasRunningProc(taskId) {
  if (!taskId) return false;
  for (const p of _byId.values()) {
    if (p.task_id === taskId && p.status === "running") return true;
  }
  return false;
}

// 终止该 task 全部 running 后台进程(composer 主按钮"停止"入口;通常只有 1 个)
export async function killTaskProcs(taskId) {
  const running = [..._byId.values()].filter(
    (p) => p.task_id === taskId && p.status === "running"
  );
  if (!running.length) return;
  if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
  for (const p of running) {
    try {
      await api("POST", `/v1/tasks/${p.task_id}/procs/${p.proc_id}/kill`);
    } catch (e) { /* kill 幂等,失败下轮轮询自会校正 */ }
  }
  refreshProcs();
}

function startPolling() {
  if (!_pollTimer) _pollTimer = setInterval(refreshProcs, POLL_MS);
  if (!_tickTimer) _tickTimer = setInterval(tickElapsed, 1000);
}

function stopPolling() {
  if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
  if (_tickTimer) { clearInterval(_tickTimer); _tickTimer = null; }
}

// ───── 工具卡活化(live 与历史重渲两个渲染点都调)─────

// det = [Background] 工具结果的 <details class="tool-call">。从结果文本抽 proc_id,
// 打上 data-proc-id + 状态 span + 停止按钮;真实状态由 updateCards 按拉取数据校正
// (历史卡刚渲染时状态未知,先不转 spinner,refreshProcs 回来再定)。
export function decorateBgprocCard(det, txt) {
  if (!det || !txt || !txt.startsWith("[Background]")) return;
  const m = txt.match(/proc_id=(\w+)/);
  if (!m) return;
  det.classList.add("bgproc");
  det.dataset.procId = m[1];
  const sum = det.querySelector("summary");
  if (!sum) return;
  const st = document.createElement("span");
  st.className = "tool-elapsed bp-state";
  sum.appendChild(st);
  const btn = document.createElement("button");
  btn.type = "button";
  btn.className = "small bp-kill";
  btn.textContent = "停止";
  btn.style.display = "none";  // 状态未知先藏,updateCards 见 running 再显
  btn.onclick = (e) => {
    e.preventDefault();   // 别触发 details 开合
    e.stopPropagation();
    killProc(det.dataset.procId, btn);
  };
  sum.appendChild(btn);
  const p = _byId.get(m[1]);
  if (p) updateCards();  // 已有该 proc 的状态(live 场景轮询先到)→ 立即定态
}

async function killProc(procId, btn) {
  const p = _byId.get(procId);
  if (!p) return;
  if (!await dialogConfirm({ title: "终止后台进程", message: "确定终止这个后台进程?已产生的输出会保留,进程本身无法恢复。", okText: "终止", danger: true })) return;
  btn.disabled = true;
  try {
    await api("POST", `/v1/tasks/${p.task_id}/procs/${procId}/kill`);
  } catch (e) { /* kill 幂等,失败下轮轮询自会校正 */ }
  refreshProcs();
}

// 页面上所有 bgproc 卡按 _byId 最新状态校正(spinner / 跳秒基准 / 终态定格)
function updateCards() {
  document.querySelectorAll(".tool-call.bgproc[data-proc-id]").forEach((det) => {
    const p = _byId.get(det.dataset.procId);
    const st = det.querySelector(".bp-state");
    const btn = det.querySelector(".bp-kill");
    if (!p || !st) return;
    if (p.status === "running") {
      det.classList.add("running");
      st.dataset.base = p.elapsed_s;
      st.textContent = ` · 后台运行中 ${fmtElapsed(p.elapsed_s)}`;
      if (btn) btn.style.display = "";
    } else {
      det.classList.remove("running");
      delete st.dataset.base;
      const label = p.killed ? "已终止"
        : p.status === "finished"
          ? (p.exit_code === 0 ? "已完成" : `失败(exit ${p.exit_code})`)
          : "已中断";
      st.textContent = ` · 后台${label} · 耗时 ${fmtElapsed(p.elapsed_s)}`;
      if (btn) btn.remove();
    }
  });
}

// 秒数本地推进(与工具卡跳秒同体验):elapsed = 拉取时基准 + 本地流逝
function tickElapsed() {
  const dt = Math.floor((Date.now() - _fetchedAt) / 1000);
  document.querySelectorAll(".tool-call.bgproc.running .bp-state").forEach((st) => {
    const base = parseInt(st.dataset.base, 10);
    if (!isNaN(base)) st.textContent = ` · 后台运行中 ${fmtElapsed(base + dt)}`;
  });
}

function fmtElapsed(secs) {
  secs = Math.max(0, secs | 0);
  if (secs < 60) return secs + "s";
  if (secs < 3600) return Math.floor(secs / 60) + "m" + String(secs % 60).padStart(2, "0") + "s";
  return Math.floor(secs / 3600) + "h" + String(Math.floor((secs % 3600) / 60)).padStart(2, "0") + "m";
}

// ───── 完成 toast(右下角,10s 自动消失;非当前 task 点击可跳转)─────

function notifyDone(p) {
  let wrap = $("proc-toasts");
  if (!wrap) {
    wrap = document.createElement("div");
    wrap.id = "proc-toasts";
    document.body.appendChild(wrap);
  }
  const ok = p.exit_code === 0;
  const label = p.killed ? "已终止" : (ok ? "已完成" : `失败(exit ${p.exit_code})`);
  const jump = p.task_id && p.task_id !== state.taskId;
  const t = document.createElement("div");
  t.className = "proc-toast" + (ok || p.killed ? "" : " fail");
  t.innerHTML = `
    <div>后台任务${label} · 耗时 ${fmtElapsed(p.elapsed_s || 0)}${jump ? " · 点击查看" : " · 可继续对话"}</div>
    <div class="pt-cmd">${escapeHtml(p.command)}</div>`;
  t.onclick = () => {
    t.remove();
    if (jump) selectTask(p.task_id);
  };
  wrap.appendChild(t);
  setTimeout(() => t.remove(), 10000);
}
