import { AssignStrategy, DAILY_CALLS_PER_AGENT, type AssignmentProposal } from '@pac/types';
// ⚠️ 只借它的三个纯标签函数（取值码 → 中文），⛔ 不引 mcp 的任何服务/状态。
import { zhAnchorMode, zhTemperature, zhTreatment } from '../mcp/model-facing';

/**
 * assignment-facts —— 把一份确认单投影成**给模型的结构化事实**。
 *
 * ═══ 为什么不再给成品句子 ═══════════════════════════════════════
 * 原来 `propose_assignment` 回给模型的是 `selectionNote` / `basisNote` / `pendingNote`
 * 三段服务端拼好的话，外加十几行「原样输出、不要改写、不要合并」。两个问题：
 *
 *   ① **成品句子有固定形状，模型学会形状就能凭空背出来。** 实测背出过
 *      「已撤销批次：收回 9 条。」—— 而撤销工具一次都没被调用。
 *      结构化事实编不出来：JSON 里的具体值瞎写一眼就能对出来。
 *   ② **「照抄」浪费了模型唯一不可替代的能力。** 它最擅长的就是把一堆事实
 *      组织成人话；让它当复读机，等于花钱买了个模板引擎。
 *
 * ⇒ 服务端给**事实**，模型负责**措辞**，写作规范集中写在系统提示词里一处。
 *
 * ═══ 边界 ═══════════════════════════════════════════════════════
 * ⛔ **这里一个字的指令都不许有**（「请照抄」「不要说成…」之类）。
 *   2026-08-08 栽过：`get_assignment_detail` 的 note 字段混着给模型的指令，
 *   模型照抄就把内部指令原样贴进了主管的对话框。⇒ 事实归事实，规范归提示词。
 * ⛔ 不含任何 planId / 患者姓名 —— 那些走侧信道给界面，模型手里不该有。
 * ⚠️ 键名用中文：模型照着键名说就是对的，省掉一轮"把 inHandPending 翻译成人话"。
 */

/** 给模型的事实投影。⚠️ 纯函数，输入一份提案、输出一个可序列化对象。 */
export function modelFacts(p: AssignmentProposal): Record<string, unknown> {
  const agents = p.byAgent.length;
  const dedicated = p.byAgent.reduce((s, a) => s + a.dedicated, 0);
  const spread = p.byAgent.reduce((s, a) => s + a.spread, 0);
  const loads = p.byAgent.map((a) => a.loadAfter);
  const perBatch = p.byAgent.map((a) => a.count);

  return {
    '怎么选的': {
      '治疗项': zhTreatment(p.potentialTreatment),
      '时间档': zhTemperature(p.temperature),
      '口径': zhAnchorMode(p.anchorMode ?? null),
      '符合条件的总人数': p.candidateTotal,
      '本批人数': p.target,
      // ⚠️ 候选不够时 target 被压低，但**基数没变** —— 两个数要分别给，
      //   不然主管会以为系统偷偷把他的设置改小了。
      ...(p.target < p.batchSize ? { '您设的基数': p.batchSize } : {}),
      '本批人数怎么来的':
        p.basis === 'explicit'
          ? '主管这次指定的'
          : p.basis === 'inherited'
            ? `沿用 ${p.basisFrom ?? '上一次'}`
            : `系统默认：在岗 ${agents} 人 × 每天 ${DAILY_CALLS_PER_AGENT} 通 × ${p.expiresInDays} 天`,
      '排序': '没被分过的排在前面，其余按优先级从高到低',
      ...(p.candidateTotal > p.target
        ? { '这轮还没轮到': p.candidateTotal - p.target }
        : {}),
    },
    '怎么派的': {
      '已排好': p.placed,
      '涉及客服数': agents,
      '分给自己专属客服的': dedicated,
      '给手上最空的人的': spread,
      // ⚠️ 「一条都没分到」的人也要给 —— 主管要看见"他不是被漏了"
      '本批没分到的客服': p.skippedAgents.length,
      '时效天数': p.expiresInDays,
      '本批每人拿到': range(perBatch),
      '分完之后每人手上': range(loads),
      '按每天几通算': DAILY_CALLS_PER_AGENT,
      '落人规则': '有专属的先回自己人手上；无主的给当前手上最少的那个',
      ...(p.pending.length > 0 ? { '专属客服排满、这批没发的': p.pending.length } : {}),
    },
    // ⚠️ 只给 action 层：info 层是折叠在卡片里的说明，模型复述它纯属噪音。
    // ⛔ 不给 options 的技术细节 —— 那是按钮的事，模型说出 `pending.to_owner` 只会吓到主管。
    '要主管定的': p.signals
      .filter((s) => s.tier === 'action')
      .map((s) => ({ '是什么': s.title, '为什么': s.why, '不处理会怎样': s.defaultLabel })),
  };
}

/** `[3, 5, 4]` → `'3~5'`；全相等时给单值。空数组给 0。 */
function range(xs: number[]): string {
  if (xs.length === 0) return '0';
  const lo = Math.min(...xs);
  const hi = Math.max(...xs);
  return lo === hi ? `${lo}` : `${lo}~${hi}`;
}

/** 落人策略的取值码 → 中文（给明细用；目前 facts 里只用聚合数，留着备用）。 */
export function zhAssignStrategy(s: string): string {
  return s === AssignStrategy.DEDICATED ? '专属客服' : '手上最空的人';
}
