'use client';

import { create } from 'zustand';
import type { SheetSnapshot } from '@pac/types';
import { emitPetEvent } from '@/lib/pet-events';
import { STREAM_TOTAL_MS } from '@/components/assistant/cohort-stream';

/**
 * 助手窗的外部控制口 —— 让**业务代码**能打开助手并塞一句话进去。
 *
 * ── 为什么必须有这个 store ──────────────────────────────────
 * 在此之前助手窗是完全自闭的:
 *   · `AssistantWidget` 的 `open` 是组件内 `useState`,外面打不开;
 *   · `useAssistantChat()` 在 `AssistantChat` 内部实例化,`send` 不外露。
 * 于是「在召回池点一下,把这批人移交给助手」这件事**根本无处落地** ——
 * 不是难写,是没有接口。这 30 行就是那个接口。
 *
 * ── 为什么用 seq 而不是"消费完置空" ─────────────────────────
 * 与 `plan-sync-store` 同款:自增序号触发订阅,消费方按 seq 变化执行一次。
 * 用"置空"表达已消费需要消费方回写 store,那是双向数据流,
 * StrictMode 双执行下会变成"发一次跑两次 / 或一次都不跑"。
 *
 * ⚠️ `AssistantWidget` 收起时是 **CSS 隐藏而非卸载**(见那里的注释:对话/工具步骤/
 * 听写状态全靠这个保留)。store 化之后**必须保持这个特性** ——
 * 若顺手改成条件渲染,主管每收一次助手就丢一次对话,而这在代码上看不出任何异常。
 */
interface AssistantState {
  open: boolean;
  setOpen: (open: boolean) => void;
  /**
   * 最大化。⚠️ 原先是 `AssistantWidget` 的组件内 `useState` —— 与 `open` 当初同一个毛病:
   * 业务侧要"移交后直接开大窗"就够不着。提到 store 后两者一起由外部驱动。
   */
  maximized: boolean;
  setMaximized: (maximized: boolean) => void;
  /**
   * 待发送的一句话。`seq` 自增即为"有新请求";`text` 是要替主管说的话。
   * null = 没有待处理请求。
   */
  pending: { text: string; seq: number } | null;
  /** 打开助手并发一句话(移交入口用);maximize=true 时同时开大窗 */
  ask: (text: string, opts?: { maximize?: boolean }) => void;
  /**
   * 人群移交 —— 发数据流动画,**等它飞完**再开助手。
   *
   * ⚠️ 为什么不在业务侧自己 setTimeout:那样每个调用点都要知道动画多长,
   *   改一次时长要满仓找。时长的真理源在 cohort-stream(舞台层),这里引它。
   * ⚠️ 为什么不让舞台层自己开助手:那会让"怎么演"反过来决定"接下来做什么" ——
   *   业务语义(要说哪句话、开不开大窗)必须留在业务这边。
   *   舞台只负责演,时长是它对外的唯一契约。
   */
  handoff: (input: {
    text: string;
    from: { x: number; y: number; w: number; h: number };
    count: number;
    treatment?: string;
    temperature?: string;
  }) => void;
  /**
   * 主管当前正看着哪家诊所 —— **跟着每次 chat 请求一起发给服务端**。
   *
   * 🔴 为什么必须走 store 而不是让模型问出来:移交那句话只有
   *   「帮我给「拔牙 · 3 年以上」这批患者出一份分配方案」,**里面没有诊所**。
   *   模型没得传 → 服务端回落 `scope.clinicIds[0]` → 多诊所主管在第二家的矩阵上点一格,
   *   确认单按**第一家**出,人数跟他刚看到的格子对不上**而且不报错**。
   * ⚠️ `get_current_user` 救不了这个:它返回的是诊所**列表**,
   *   模型照样不知道主管刚才在看哪一家。⇒ 上下文走旁路,⛔ 别让模型转译。
   * ⚠️ 客服侧(患者详情里的助手)没有诊所选择器 → 一直是 null → 服务端行为与从前一致。
   */
  activeClinicId: string | null;
  setActiveClinicId: (clinicId: string | null) => void;
  /**
   * 🔴 **眼前那张还没确认的确认单,此刻的样子** —— 与 `activeClinicId` 同一条旁路,
   * 跟着每次 chat 请求发给服务端,模型用 `get_current_sheet` 取。
   *
   * ⚠️ 为什么非走这条路不可:确认之前,改派/移出/时限/福利**只存在于卡片组件里**,
   *   服务端没有可查的东西(见 `SheetSnapshotSchema` 上那段)。
   * ⚠️ 只有**现役**那张挂在这里 —— 卡片自己在 effect 里挂、卸载时只清自己那份
   *   (按 requestId 比对),否则重出一版后旧卡片卸载会把新的一起清掉。
   * ⚠️ 它是**照片不是账本**:主管下一秒拖一下就过期,所以每次请求现取现发,
   *   ⛔ 服务端不许缓存。
   */
  sheetSnapshot: SheetSnapshot | null;
  setSheetSnapshot: (snapshot: SheetSnapshot | null) => void;
  /** 消费方(AssistantChat)发完后调,避免重复发送 */
  consume: (seq: number) => void;
}

export const useAssistantStore = create<AssistantState>((set, get) => ({
  open: false,
  setOpen: (open) => set({ open }),
  maximized: false,
  setMaximized: (maximized) => set({ maximized }),
  pending: null,
  ask: (text, opts) => {
    const seq = (get().pending?.seq ?? 0) + 1;
    // 先开窗再挂待办:窗没开就发消息,主管会看到助手"自己动了"却不知道在哪
    set({ open: true, pending: { text, seq }, ...(opts?.maximize ? { maximized: true } : {}) });
  },
  handoff: ({ text, from, count, treatment, temperature }) => {
    // ① 先发语义事件:舞台层据此放数据流,大脑层据此进"吸收"姿态
    emitPetEvent({ type: 'cohort_handoff', payload: { count, treatment, temperature, from } });
    /**
     * ② 粒子飞完再开窗 —— 先开窗会把飞行路径整个盖住,演了等于没演。
     *
     * ⭐ **默认不最大化**(2026-08-11 产品定,原来是 `maximize: true`)。
     *   移交现在前面多了一张中间表(主管已经看过这批人是谁了),再一上来就铺满整屏,
     *   等于把他刚看完的工作台整个盖掉 —— 而他多半还要回去点下一格。
     * ⛔ 别改回 true:要看大图,助手窗口自己有最大化按钮,那是**他**的决定。
     */
    window.setTimeout(() => get().ask(text), STREAM_TOTAL_MS);
  },
  activeClinicId: null,
  setActiveClinicId: (activeClinicId) => set({ activeClinicId }),
  sheetSnapshot: null,
  setSheetSnapshot: (sheetSnapshot) => set({ sheetSnapshot }),
  consume: (seq) => {
    // 只清掉自己那一条 —— 期间若又来了新请求(seq 更大),不能连它一起清
    if (get().pending?.seq === seq) set({ pending: null });
  },
}));
