'use client';

import { useState } from 'react';
import type { Signal, SignalOption } from '@pac/types';

/**
 * 引导节点的可选项 —— **插在模型讲这件事的那句话正下方**。
 *
 * ═══ 位置由**模型**决定，⛔ 不由前端猜 ═══════════════════════════
 * 一条引导节点 = 「一句解释 + 几个可选动作」。解释归模型讲（卡片不重复渲染），
 * 于是按钮离得越远越难连起来：主管读到「389 人卡在待分配，不处理 = 这批不发给他们」，
 * 要一路翻过画像亮点、翻过卡片汇总行，才看到那四个按钮。
 *
 * ⇒ 模型写完那一条 → 调 `show_guidance` → 消息流在**此刻这个位置**插一个
 *   `guidance` 块，本组件就渲染在那里。和 `render_artifact` 是同一个模式。
 * ⛔ **前端曾经用 `text.indexOf(标题)` 去猜位置，已删** —— 模型换个措辞就匹配不上，
 *   而匹配不上就渲染不出来（把选项静默藏起来，G9）。位置归知道自己写在哪的那一方。
 * ⚠️ 模型漏调也不丢：没插进正文的节点仍旧显示在确认单卡片上（卡片按 placedKeys 去重）。
 *
 * ═══ 样式：克制 ═══════════════════════════════════════════════
 * ⛔ 不要盒子、不要底色、不要重复标题 —— 标题就是它上面那句话，再写一遍等于读两遍。
 * 只需要「看得出可点」：小号描边按钮一行，加一行更弱的自由输入。
 *
 * ⚠️ 判定不在这里 —— 该不该亮、亮几条、什么顺序全在服务端 `computeSignals`。
 *   本组件是纯呈现 + 把 intent / 原话抛上去。
 * ⚠️ **按钮优先于打字**（G10）：点击是确定性的；打字要过「模型理解 → 翻成动作」，
 *   每一环都可能错（实测栽过 owner/balance 选反，18 人被散给 17 位别人）。
 *   ⇒ 兜底是**同一排里更次要的一个按钮**（虚线框、灰字），⛔ 不做成和选项平级的输入框。
 */
/**
 * 要填一个数的选项 —— 「改时效 [1] 天」「改每天几通 [15] 通」。
 *
 * ⭐ 为什么是输入框而不是几个固定档：天数和通数是**连续量**，列 3/5/7 天必然漏掉他要的那个，
 *   而漏掉的那次他只能去下面的自由输入打字 → 走模型理解 → 慢且可能错。填数是确定性的。
 * ⚠️ `min/max` 是护栏：越界直接不给提交，⛔ 别让它走到服务端才报错。
 * ⚠️ 值没变就不提交 —— 「改时效」填回原来的 1 天等于什么都没做，
 *   真发出去会白跑一次重排（`daily_rate.set` 那条要重跑算法）。
 */
function NumberOption({
  option,
  input,
  disabled,
  onIntent,
}: {
  option: SignalOption;
  input: NonNullable<SignalOption['input']>;
  disabled?: boolean;
  onIntent: (intent: string, args?: Record<string, unknown>) => void;
}) {
  const [v, setV] = useState(String(input.default));
  const n = Number(v);
  const ok = Number.isInteger(n) && n >= input.min && n <= input.max && n !== input.default;
  const submit = () => ok && onIntent(option.intent, { ...option.args, [input.argKey]: n });

  return (
    <span className="inline-flex items-center gap-1 rounded border border-slate-300 bg-white px-2 py-0.5 text-[11.5px] leading-5 text-slate-600">
      {option.label}
      <input
        value={v}
        onChange={(e) => setV(e.target.value.replace(/\D/g, '').slice(0, 3))}
        onKeyDown={(e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            submit();
          }
        }}
        disabled={disabled}
        aria-label={`${option.label}（${input.min}-${input.max}${input.unit}）`}
        className="w-8 rounded bg-slate-100 px-1 text-center tabular-nums text-slate-800 outline-none focus:bg-white focus:ring-1 focus:ring-brand-400"
      />
      {input.unit}
      {/* ⚠️ 只在**改过且合法**时才出现 —— 一直挂着会让人以为不点也生效 */}
      {ok && !disabled && (
        <button
          type="button"
          onClick={submit}
          className="ml-0.5 rounded px-1 font-medium text-brand-700 hover:bg-brand-50"
        >
          确定
        </button>
      )}
    </span>
  );
}

export function SignalPrompt({
  signal: s,
  busyIntent,
  onIntent,
  onCompose,
  withTitle,
}: {
  signal: Signal;
  /** 正在执行的 intent（按钮转圈用）；null = 空闲 */
  busyIntent?: string | null;
  onIntent: (intent: string, args?: Record<string, unknown>) => void;
  /**
   * 兜底 —— 把一句**开头**填进底部输入框并聚焦，⛔ 不直接发送。
   *
   * 🔴 前缀必须带上是哪一条：主管只打「给张悦」，模型不知道说的是待分配那批人。
   * ⭐ 填进输入框而不是偷偷拼在发送时 —— 他**看得见**这句会带什么上下文，
   *   不想要可以自己删。⛔ 界面不替主管说他看不见的话。
   * ⚠️ 前缀用节点标题（他眼前就是这句），⛔ 不用 key / intent id：
   *   那会原样出现在对话里，`pending.to_owner` 就是内部码泄漏。
   */
  onCompose?: (draft: string) => void;
  /**
   * 自带标题 —— **上面没有解释它的那句话**时必须为 true。
   *
   * 🔴 正常情况下标题是多余的：模型刚讲完这条，按钮紧跟其后（`show_guidance`）。
   *   但**模型不交错时**（实测 qwen 会把这一轮的 tool-call 全提到文本之前），
   *   这些块被退化保护挪到正文之后堆成一摞 —— 那时一排光秃秃的按钮
   *   （「只圈这 2324 人」）根本看不出在说哪件事。
   */
  withTitle?: boolean;
}) {
  /**
   * 一个动作都没有 = 没什么可点的,别摆一排空按钮。
   * ⚠️ 但**"我自己说"也算动作** —— 有些节点唯一的处置就是他开口说一句
   *   (「这批带上…」那种,福利原文只能是他的话,⛔ 界面不给备选项:
   *    列出来的几条会被当成"系统推荐的福利")。⇒ 有兜底就照样渲染。
   */
  if (s.options.length === 0 && !onCompose) return null;

  return (
    // ⚠️ `not-prose`-ish：这块插在 markdown 里，字号/行高要自己钉死，
    //   ⛔ 别继承正文的 13.5px —— 继承了它就跟正文一样重，抢戏。
    <div className="my-1.5 space-y-1">
      {withTitle && <p className="text-[12px] font-medium text-slate-700">{s.title}</p>}
      <div className="flex flex-wrap items-center gap-1.5">
        {s.options.map((o) =>
          o.input ? (
            <NumberOption
              key={`${o.intent}:${o.label}`}
              option={o}
              input={o.input}
              disabled={busyIntent != null}
              onIntent={onIntent}
            />
          ) : (
            <button
              key={`${o.intent}:${o.label}`}
              type="button"
              disabled={busyIntent != null}
              onClick={() => onIntent(o.intent, o.args)}
              className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[11.5px] leading-5 text-slate-600 transition-colors hover:border-brand-400 hover:text-brand-700 disabled:opacity-50"
            >
              {busyIntent === o.intent ? '处理中…' : o.label}
            </button>
          ),
        )}
        {/* 兜底:同一排,但**再弱一档** —— 虚线框 + 更浅的字,一眼看得出它不是正经选项 */}
        {onCompose && (
          <button
            type="button"
            onClick={() => onCompose(`关于「${s.title}」—— `)}
            className="rounded border border-dashed border-slate-300 px-2 py-0.5 text-[11.5px] leading-5 text-slate-400 transition-colors hover:border-slate-400 hover:text-slate-600"
          >
            我自己说
          </button>
        )}
      </div>
    </div>
  );
}
