import type { AssignmentProposal, SheetEditOp } from '@pac/types';

/**
 * chat-blocks —— 助手消息流的**数据模型 + 纯函数操作**。
 *
 * 从 `use-assistant-chat` 抽出来的原因：草稿状态机（active / superseded / …）
 * 是这条链路上最容易出错、也最值得回归的一段逻辑，而它原来内联在 React hook 里，
 * 只能靠端到端点一遍来验。抽成纯函数之后可以直接测。
 *
 * ⛔ 这里只放**不碰 React、不碰网络**的东西。带副作用的仍留在 hook 里。
 */

/** 一步工具调用(Claude 式透明步骤:看到调了哪个工具、入参、返回数据)。 */
export interface ToolStep {
  id: string;
  /** 后端 toolCallId,用于把 tool_result/tool_error 精确匹配回对应步骤(支持并行/多调用)。 */
  callId?: string;
  tool: string;
  args: unknown;
  result?: unknown;
  status: 'running' | 'done' | 'error';
  error?: string;
}

/** 模型产出的 HTML 卡片(在输出区沙箱 iframe 渲染)。 */
export interface Artifact {
  id: string;
  title?: string;
  html: string;
  /** true = 还在流式生成中(只注入内容,图表脚本不执行);false/缺省 = 最终(执行图表脚本)。 */
  streaming?: boolean;
}

/**
 * 草稿状态机 —— **任一时刻最多只有一张 `active`**。
 *
 * ⭐ `superseded` = 主管让助手重出了一版，这张当场作废。
 *   🔴 作废发生在**新版生成时**，⛔ 不是在确认时（产品定，2026-08-12）：
 *   否则主管面前会同时摆着两张都能点确认的卡，点错就是分错一批人。
 * ⚠️ 作废的卡**继续留在对话里并显式标注**，⛔ 不许静默变灰、⛔ 不许消失 ——
 *   他要能看见自己出过几版、以及哪一版才是当前的。
 * ⚠️ `confirmed` / `cancelled` 是**终态**，⛔ 不许被后续的作废扫描回退。
 */
export type DraftState = 'active' | 'superseded' | 'confirmed' | 'cancelled';

export type Block =
  /**
   * 文本块。`modelText` = **只给模型看**的版本(缺省时模型看 `text`)。
   *
   * 🔴 为什么需要两份:确认后注入的那句话里要带**完整批次 uuid**,否则模型撤销时
   *   只能拿界面上的 8 位短号去调 `revoke_assignment` —— 实测必然报「批次不存在」。
   *   但 uuid 对主管毫无意义,摆在对话里纯属噪音。⇒ 显示一份、喂模型一份。
   * ⚠️ 用它承载的必须是**同一件事的两种措辞**,⛔ 不许塞模型专属的隐藏指令:
   *   主管看不见的话就无法纠正,那是"界面替主管说话"。
   */
  | { kind: 'text'; text: string; modelText?: string }
  | { kind: 'tool'; step: ToolStep }
  | { kind: 'artifact'; artifact: Artifact }
  /**
   * 全景确认单 —— **原生 React 组件**,不是 artifact。
   *
   * ⚠️ 为什么不能用 artifact:artifact 跑在 `sandbox="allow-scripts"` 的 iframe 里,
   * CSP 是 `connect-src 'none'` —— **卡片内不可能发出写请求**。确认单要能点确认落库,
   * 只能是原生组件。
   * 分岔记牢:**只读展示用 artifact,可交互用原生组件。**
   *
   * ⚠️ sheet 由**服务端经侧信道推来**,不是模型生成的 —— 它含几百个 planId,
   * 让模型吐等于让它编。
   */
  | {
      kind: 'assignment_sheet';
      requestId: string;
      sheet: AssignmentProposal;
      state: DraftState;
      /** 确认后的批次 id(终态展示用) */
      assignmentId?: string;
      /**
       * ⭐ 助手下发的**语义编辑指令队列**(edit_assignment_sheet 走侧信道推来)。
       * 卡片组件按 `seq` 只执行新增的那一批,执行完把结果回一句进对话。
       * ⚠️ 用队列而不是"最新一条":主管连着说两句时,两批指令都要生效。
       */
      edits?: { seq: number; ops: SheetEditOp[] }[];
    };

export interface ChatMessage {
  id: string;
  role: 'user' | 'assistant';
  blocks: Block[];
}

export type ChatStatus = 'idle' | 'streaming' | 'error';

// ─────────────────────────────────────────────────────────
// 纯函数操作
// ─────────────────────────────────────────────────────────

/** 把 tool_result/tool_error 匹配回对应工具步骤:优先按 callId,退化到"最后一个 running"。 */
export function findToolIdx(blocks: Block[], callId: string | undefined): number {
  if (callId) {
    const i = blocks.findIndex((b) => b.kind === 'tool' && b.step.callId === callId);
    if (i !== -1) return i;
  }
  for (let i = blocks.length - 1; i >= 0; i--) {
    const b = blocks[i];
    if (b && b.kind === 'tool' && b.step.status === 'running') return i;
  }
  return -1;
}

/** 按 callId upsert artifact 块:流式 html 增量更新,最终 tool_call 覆盖(title+完整 html)。 */
export function upsertArtifact(
  blocks: Block[],
  id: string,
  patch: { title?: string; html?: string; streaming?: boolean },
): Block[] {
  const i = blocks.findIndex((b) => b.kind === 'artifact' && b.artifact.id === id);
  if (i === -1) {
    return [
      ...blocks,
      {
        kind: 'artifact',
        artifact: { id, title: patch.title, html: patch.html ?? '', streaming: patch.streaming },
      },
    ];
  }
  return blocks.map((b, j) => {
    if (j !== i || b.kind !== 'artifact') return b;
    return {
      kind: 'artifact',
      artifact: {
        id,
        title: patch.title ?? b.artifact.title,
        html: patch.html ?? b.artifact.html,
        streaming: patch.streaming ?? b.artifact.streaming,
      },
    };
  });
}

/**
 * 新草稿到达 —— upsert 它，并把**此前所有 `active` 的当场作废**。
 *
 * 🔴 必须**跨消息**扫：上一版几乎总在**上一轮**的消息里
 *   （主管看着它才说「重排一版」的）。只扫当前消息永远扫不到，而且不会报错。
 * ⚠️ 只作废 `active`：`confirmed` / `cancelled` 是终态，⛔ 不许回退。
 * ⚠️ 同一 `requestId` 重推时清空 `edits` —— 旧的编辑指令对新名单没有意义。
 *
 * @param hostMessageId 新卡挂到哪条消息下（通常是当前这条助手消息）
 */
export function applyIncomingSheet(
  messages: ChatMessage[],
  input: { requestId: string; sheet: AssignmentProposal; hostMessageId: string },
): ChatMessage[] {
  const { requestId, sheet, hostMessageId } = input;
  let placed = false;
  const swept = messages.map((m) => ({
    ...m,
    blocks: m.blocks.map((b): Block => {
      if (b.kind !== 'assignment_sheet') return b;
      if (b.requestId === requestId) {
        placed = true;
        return { ...b, sheet, state: 'active', edits: [] };
      }
      return b.state === 'active' ? { ...b, state: 'superseded' } : b;
    }),
  }));
  if (placed) return swept;
  return swept.map((m) =>
    m.id === hostMessageId
      ? { ...m, blocks: [...m.blocks, { kind: 'assignment_sheet', requestId, sheet, state: 'active' }] }
      : m,
  );
}

/**
 * 找**当前有效**的那张确认单（唯一的 `active`）。
 *
 * ⭐ 2026-08-12 由「最后一张」改成「唯一 active」：重出一版之后，最后一张可能
 *   正是刚作废的那张 —— 助手的修改指令打上去，主管一点反应都看不到。
 * ⚠️ 模型手里没有 requestId（服务端铸的，没进它上下文），它说「这批」指的就是当前有效那张。
 */
export function findActiveSheet(
  messages: ChatMessage[],
): { messageIndex: number; blockIndex: number } | null {
  for (let mi = messages.length - 1; mi >= 0; mi--) {
    const blocks = messages[mi]?.blocks ?? [];
    const bi = blocks.findIndex((b) => b.kind === 'assignment_sheet' && b.state === 'active');
    if (bi >= 0) return { messageIndex: mi, blockIndex: bi };
  }
  return null;
}
