'use client';

import { useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { ToolStep } from './chat-blocks';
import { cn } from '@/lib/utils';

/**
 * 执行过程 —— 这一轮**到底调了哪些工具**，可核查的原始记录。
 *
 * ═══ 为什么要有 ═══════════════════════════════════════════════════
 * 🔴 「模型到底调没调工具」是这条生产线**最贵的一类问题**，而在此之前只能靠
 *   "界面上有没有出现卡片/按钮"去反推。2026-08-13 实测吃过一次:主管点了
 *   「只圈电商平台那 251 人」，助手回了一整份确认单式的汇报
 *   （251 人 / 98 条落到 33 位客服 / 17 回专属 / 81 补空 / 153 待分配），
 *   数字彼此自洽、读起来毫无破绽 —— 但 `propose_assignment` 一次都没跑，
 *   整段是照着提示词里那个三段模板**填出来的**。
 *   当时排查只能靠「卡片没出现 ⇒ 侧信道没推 ⇒ 工具没跑」这条推理链。
 * ⇒ 把记录摆出来，这类问题从"推理"变成"看一眼"。
 *
 * ═══ 三条边界 ═══════════════════════════════════════════════════
 * ⛔ **一步都没有时整块不渲染**（产品定）——「执行过程 · 0 步」对主管是噪音,
 *   他不需要每条闲聊下面都挂一个空壳。⚠️ 但这也意味着"没调工具"本身**不会**
 *   在这里显形，那件事另有防线（见 assistant-prompts 第 2 段与服务端收尾标记）。
 * ⛔ **默认收起**：这是脚注不是内容。主管要读的是结论,证据放在他伸手够得到的地方就行。
 * ⚠️ **摆在整条消息的最后**（正文 → 卡片 → 这里）。Claude Code 是按发生顺序摆在正文之前，
 *   我们不行：qwen 会把一轮的 tool-call 全提到文本之前，照发生顺序摆等于每次都先给主管
 *   看一屏工具记录再给结论。位置固定在末尾，"这轮跑了几步"也就永远在同一个地方。
 *
 * ⚠️ 这里**允许出现英文工具名**，与「⛔ 别把工具名说给主管」不冲突：
 *   那条约束管的是**助手嘴里说的话**；这里是一份可核查的记录，语境不同
 *   （同 Claude Code 的 "Ran a command"）。中文动作名在前、原始名在后且更淡。
 */

/**
 * 工具 → 中文说法。**凡是注册进 MCP / 助手的工具都要在这里有一条。**
 *
 * ⚠️ 未映射时**显示原始英文名**，⛔ 不显示空白 —— 漏配一个要能一眼看见。
 *   （曾经漏过四个，兜底文案直接把 `get_cohort_attributes` 摆给了主管看。）
 */
const TOOL_META: Record<string, { label: string; running: string }> = {
  find_patient: { label: '查找患者', running: '正在查找患者' },
  get_patient_overview: { label: '读取患者概览', running: '正在读取患者概览' },
  get_persona: { label: '读取患者画像', running: '正在读取患者画像' },
  get_facts: { label: '读取关键事实', running: '正在读取关键事实' },
  get_recall_plan: { label: '读取召回计划', running: '正在读取召回计划' },
  list_recall_queue: { label: '检索召回队列', running: '正在检索召回队列' },
  recall_queue_stats: { label: '统计召回池', running: '正在统计召回池' },
  get_current_user: { label: '确认身份', running: '正在确认你的身份与权限' },
  get_agents: { label: '查在岗客服', running: '正在查在岗客服与在手负载' },
  propose_assignment: { label: '出分配方案', running: '正在挑人、排客服，出分配方案' },
  list_assignment_batches: { label: '查历史批次', running: '正在查你分过的批次' },
  get_assignment_detail: { label: '查批次详情', running: '正在查这批的执行情况' },
  explain_assignment: { label: '解释为什么是他', running: '正在查这个人为什么被选进来' },
  get_cohort_attributes: { label: '看这批人构成', running: '正在看这批人里各类有多少' },
  edit_assignment_sheet: { label: '改确认单', running: '正在按你说的改确认单' },
  // ⚠️ 查的是**眼前那张还没确认的**单子(草稿态,由浏览器随请求捎给服务端)——
  //   与 `get_assignment_detail`(已经分下去的那批)⛔ 不是一回事,标签也别写像
  get_current_sheet: { label: '看当前确认单', running: '正在看这张单现在什么样' },
  revoke_assignment: { label: '撤销批次', running: '正在撤销这批' },
  render_artifact: { label: '出图表', running: '正在画图' },
  // 取一篇做法(pull 式加载,见服务端 assistant-playbooks):不查库不改数据
  open_playbook: { label: '取做法', running: '正在取这类活的做法' },
  // ── 纯位置标记，不查库不写库（见 assistant.service 的 place_*）──
  show_sheet: { label: '摆确认单卡片', running: '正在摆确认单' },
  show_guidance: { label: '摆选项按钮', running: '正在摆选项按钮' },
};

export function toolMeta(tool: string) {
  return TOOL_META[tool] ?? { label: tool, running: `正在调用 ${tool}` };
}

/** 展示用的长度上限 —— ⚠️ 与历史回传那份（`HISTORY_CAP`）**不是同一件事**，别去对齐：
 *  那边是省 token，这边是省版面，超出的部分靠滚动看，⛔ 不截断丢失。 */
// ⚠️ `max-h-56 overflow-auto` **不许动**:超出的部分靠滚动看,⛔ 不截断丢失。
//   2026-08-15 只改了外观:细边 + 更淡的底(对齐 Claude Code 那种"有框的浅盒")。
const BOX =
  'max-h-56 overflow-auto rounded-md border border-slate-200/80 bg-slate-50/60 px-2.5 py-2';
const MONO = 'whitespace-pre-wrap break-all font-mono text-[10.5px] leading-[1.6] text-slate-600';

/** 参数短就一行、长就换行铺开 —— ⛔ 别一律 pretty-print，`{"编号":3}` 占三行纯属浪费。 */
function fmt(v: unknown): string {
  if (v == null) return '';
  const one = typeof v === 'string' ? v : JSON.stringify(v);
  if (one.length <= 80) return one;
  return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
}

/**
 * ⭐ **一条可折叠的轨迹行** —— 思考和工具调用共用的唯一原语。
 *
 * 为什么合成一个：它们在消息里**交错出现**，视觉必须同源，否则同一串时序读起来
 * 像两套东西。⇒ 一行标签 + 可展开的正文；差别只在标签和展开的内容。
 * ⚠️ 一律**默认收起**：这是脚注不是内容，主管要读的是结论。
 * ⛔ 别给其中一种单独加边框/底色 —— 那会把"谁更重要"的暗示塞进纯记录里。
 */
function TraceRow({
  label,
  tag,
  status,
  children,
}: {
  label: string;
  /** 灰色小字：工具原始名 / 思考字数 —— 出问题时能拿去 grep 的那个词 */
  tag?: string;
  status?: 'error' | 'running' | 'aborted';
  children?: React.ReactNode;
}) {
  const [open, setOpen] = useState(false);
  return (
    // ⭐ **无分隔线**(2026-08-15):一串轨迹行本来就是一串,横线把它切成了好几块小卡片。
    //   ⚠️ 行与行靠**留白**分开,⛔ 别再加回 border-t。
    <div>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        /**
         * ⭐ **文字顶到最左，箭头跟在后面**（2026-08-15）。
         *
         * 箭头原来在最前面，把这一行整体往右推了 18px —— 于是每一行轨迹都比它下面的
         * 正文缩进一截，读起来像是正文的子项，而它们本来是同一级的。
         * ⚠️ `-mx-2 px-2`：视觉左沿落在 0（与正文对齐），而悬停底色仍向两侧各溢出 8px，
         *   点击热区不至于紧贴文字。⛔ 别改成 `px-0`：那样悬停块会贴着字边，很局促。
         */
        className="-mx-2 flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left transition-colors hover:bg-slate-50"
      >
        <span className="text-[12px] text-slate-500">{label}</span>
        {tag && <span className="truncate font-mono text-[10.5px] text-slate-400">{tag}</span>}
        {open ? (
          <ChevronDown className="size-3 shrink-0 text-slate-400" />
        ) : (
          <ChevronRight className="size-3 shrink-0 text-slate-400" />
        )}
        {status === 'error' && (
          <span className="ml-auto shrink-0 text-[10.5px] text-rose-600">失败</span>
        )}
        {status === 'running' && (
          <span className="ml-auto shrink-0 text-[10.5px] text-slate-400">进行中…</span>
        )}
        {/* ⚠️ 中性色,⛔ 不是红:这一步**没有失败**,只是这一轮不听了(见 ToolStep 上那段) */}
        {status === 'aborted' && (
          <span className="ml-auto shrink-0 text-[10.5px] text-slate-400">已中断</span>
        )}
      </button>
      {/* ⚠️ 箭头挪到文字后面之后，展开内容与 label 左沿**天然对齐**（都落在 0）——
          ⛔ 别再加 `pl-*` 去"缩进一下"：那正是刚拆掉的那种错位。 */}
      {open && children && <div className="space-y-1 pb-2">{children}</div>}
    </div>
  );
}

/** 一次工具调用。⚠️ 参数/返回里会出现取值码，所以**默认收起**（同 StepTrace 顶部那段边界）。 */
export function StepRow({ step }: { step: ToolStep }) {
  const meta = toolMeta(step.tool);
  const args = fmt(step.args);
  const result = step.status === 'error' ? fmt(step.error) : fmt(step.result);
  return (
    <TraceRow
      label={meta.label}
      tag={step.tool}
      {...(step.status === 'done' ? {} : { status: step.status })}
    >
      {args && (
        <div className={BOX}>
          <div className="mb-0.5 text-[10px] text-slate-400">参数</div>
          <div className={MONO}>{args}</div>
        </div>
      )}
      {result && (
        <div className={BOX}>
          <div className="mb-0.5 text-[10px] text-slate-400">
            {step.status === 'error' ? '错因' : '返回'}
          </div>
          <div className={cn(MONO, step.status === 'error' && 'text-rose-700')}>{result}</div>
        </div>
      )}
    </TraceRow>
  );
}

/**
 * 模型的思考。⚠️ 标签上带**字数**：流式期间它在长，字数是唯一能看出"还在想"的信号。
 * ⛔ 别自动展开 —— 实测单轮能到三千多字，摊开会把正文淹掉。
 */
export function ReasoningRow({ text }: { text: string }) {
  return (
    <TraceRow label="思考" tag={`${text.length} 字`}>
      <div className={BOX}>
        <div className={MONO}>{text}</div>
      </div>
    </TraceRow>
  );
}

/**
 * 执行过程 —— **只剩一行小结**（2026-08-14 改）。
 *
 * 🔴 明细现在**按时序穿插在正文里**（`StepRow` / `ReasoningRow` 由 BlockView 就地渲染），
 *   这里再展开一遍就是同一份东西的两个副本 —— 改一处漏一处。
 * ⭐ 但这一行要留：**「这轮跑了几步」永远在同一个地方**，扫一眼就知道走了什么路，
 *   ⛔ 不用顺着正文一路数绿框和灰行。
 * ⚠️ 计数**含**位置标记（`show_*`），而它们在正文里不出灰行（见 BlockView 那段），
 *   所以这个数会比正文里的行数多 —— 差几个就是摆了几次。
 */
export function StepTrace({ steps }: { steps: ToolStep[] }) {
  // ⛔ 一步都没有就整块不渲染（产品定）—— 空壳对主管是噪音
  if (steps.length === 0) return null;
  const failed = steps.filter((s) => s.status === 'error').length;
  return (
    <p className="mt-2 flex items-center gap-2 px-0.5 text-[11px] text-slate-400">
      {/* 🔴 `shrink-0 whitespace-nowrap` **不许去掉**（2026-08-15 对抗审查实测）：
          同排那条工具链带 `truncate`（overflow:hidden ⇒ 最小尺寸为 0），但 flex 收缩是
          **按比例分摊**的，不会优先榨可截断的那个 —— 首段跟着被压窄，压不下就逐字换行。
          480px 浮窗实测：6 步（正好是出一版方案的正常步数）就折成两行、12 步三行，
          而「这轮跑了几步」这个唯一固定锚点被劈开时，看起来像渲染坏了。 */}
      <span className="shrink-0 whitespace-nowrap">执行过程 · {steps.length} 步</span>
      <span className="truncate">{steps.map((s) => toolMeta(s.tool).label).join(' → ')}</span>
      {/* ⭐ 失败数一直摆在外面 —— 不展开也得看得见有东西挂了 */}
      {failed > 0 && <span className="ml-auto shrink-0 text-rose-600">{failed} 失败</span>}
    </p>
  );
}
