'use client';

import { useState } from 'react';
import { ChevronRight, Check, Loader2, Users } from 'lucide-react';
import {
  ASSIGN_STRATEGY_META,
  ASSIGNMENT_EXPIRES_DAYS_PRESETS,
  Permission,
  type AgentOverride,
  type AssignmentProposal,
  type AssignStrategy,
} from '@pac/types';
import { Can } from '@/components/can';
import { assignmentsApi } from '@/components/plans/assignments-api';
import { cn } from '@/lib/utils';

/**
 * 全景确认单 —— 主管**一次确认**就落库的那张卡片。
 *
 * ⚠️ 必须是**原生 React 组件**,不能用 `render_artifact`:
 * artifact 跑在 `sandbox="allow-scripts"` 的 iframe 里、CSP `connect-src 'none'`,
 * **卡片内不可能发出写请求**。分岔记牢:只读展示用 artifact,可交互用原生组件。
 *
 * ── 微调**只有两类**(T13)──────────────────────────────────
 * 指定客服、时效(整批 + 按客服两级)。多加一类就直接违 T13 ——
 * 微调项越多,「一次确认」这个设计目标就越不可能实现。
 * 要换人群请回到对话里让助手重新圈,不要在卡片上做筛选。
 *
 * ── 布局 ────────────────────────────────────────────────
 * 助手窗只有 400px 宽,所以**不用 table**(横向撑爆),用卡片列表;
 * 患者明细按客服折叠,展开才看 —— 兼顾"尽明细"与"一眼可确认"。
 */
/**
 * 精调表落库形态 = 提案带来的(含名额上限部分) + 卡片上刚改的时效。
 *
 * ⚠️ 卡片只能改时效,所以**名额上限部分必须原样带走** —— 少带一次,主管上次设的
 * 「李莉这批最多 5 条」就在这一次确认里被悄悄清掉了,而界面上完全看不出来。
 */
function mergeOverrides(
  fromSheet: Record<string, AgentOverride>,
  expiryByAgent: Record<string, number>,
): Record<string, AgentOverride> {
  const out: Record<string, AgentOverride> = {};
  for (const [userId, o] of Object.entries(fromSheet)) {
    // 时效部分交给卡片当前值决定(下面统一写),这里只留名额上限
    if (o.maxThisBatch != null) out[userId] = { maxThisBatch: o.maxThisBatch };
  }
  for (const [userId, days] of Object.entries(expiryByAgent)) {
    out[userId] = { ...(out[userId] ?? {}), expiresInDays: days };
  }
  return out;
}

/**
 * 时效选择 —— 1~7 天下拉。
 *
 * ⭐ 用原生 `<select>`:卡片跑在 400px 的助手窗里,7 个档位铺成按钮会换行占两行,
 *   而这是个"设一次就不动"的控件,不值得占那么多地方。
 * ⚠️ 值域来自 `ASSIGNMENT_EXPIRES_DAYS_PRESETS`(共享常量),⛔ 别在这里内联 [1..7]。
 */
function DaySelect({
  value,
  disabled,
  onChange,
}: {
  value: number;
  disabled?: boolean;
  onChange: (days: number) => void;
}) {
  return (
    <select
      value={value}
      disabled={disabled}
      onChange={(e) => onChange(Number(e.target.value))}
      className={cn(
        'rounded border border-slate-200 bg-white px-1.5 py-0.5 text-[11px] text-slate-700 outline-none',
        'focus:border-brand-400 disabled:opacity-60',
      )}
    >
      {ASSIGNMENT_EXPIRES_DAYS_PRESETS.map((d) => (
        <option key={d} value={d}>
          {d} 天
        </option>
      ))}
    </select>
  );
}

export function AssignmentConfirmSheet({
  requestId,
  sheet,
  state,
  assignmentId,
  onConfirmed,
}: {
  requestId: string;
  sheet: AssignmentProposal;
  state: 'pending' | 'confirmed' | 'cancelled';
  assignmentId?: string;
  /** 落库成功 → 交给外层把卡片切终态 + 往消息流注入一条文本(补模型记忆) */
  onConfirmed: (assignmentId: string, summary: string) => void;
}) {
  /**
   * 时效初值来自**提案**(沿用主管上一次的值),⛔ 不是写死的 3 ——
   * 写死的话"记住上次时效"这件事在界面上就永远看不见,主管每次还得重调一遍。
   * ⚠️ **本批人数**不做成卡片控件:改人数会换一批人,而卡片的微调项只允许
   *   "不改人群"的那些(T13)。改人数回对话说一句(「这批 200 人」),助手重出单。
   */
  const [expiresInDays, setExpiresInDays] = useState(sheet.expiresInDays);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [open, setOpen] = useState<Set<string>>(new Set());
  /**
   * 按客服的**时效精调**(userId → 天数)。初值来自提案(沿用上次的精调)。
   *
   * ⚠️ 为什么时效能在卡片上精调、人数/名额不能:时效**不改人群**(只是这几条单子多久回池),
   *   另两个会 —— 改了就是换一批人,那必须重出确认单。
   * ⚠️ 落库时写到该客服名下**每一条**任务的 `assignment_expires_at`(逐条覆盖批次时效)。
   */
  const [expiryByAgent, setExpiryByAgent] = useState<Record<string, number>>(() =>
    Object.fromEntries(
      sheet.byAgent
        .filter((a) => a.expiresInDays !== sheet.expiresInDays)
        .map((a) => [a.userId, a.expiresInDays]),
    ),
  );

  const done = state === 'confirmed';

  const confirm = async () => {
    setSubmitting(true);
    setError(null);
    try {
      const res = await assignmentsApi.create({
        // ⭐ requestId **原样回传**服务端铸造的那个 —— 幂等键。
        //   前端自己生成挡不住 SSE 重连与重试。
        requestId,
        clinicId: sheet.clinicId,
        // 初筛条件快照:批次要能解释"当时按什么圈的"
        criteria: {
          potentialTreatment: sheet.potentialTreatment,
          candidateTotal: sheet.candidateTotal,
          target: sheet.target,
          selectionNote: sheet.selectionNote,
          // ⭐ 两个基数落进快照 —— **下一次分配就是从这里读出来沿用的**(零新列)。
          //   ⚠️ 时效存**卡片当前值**不是提案值:主管刚在上面改成 5 天,记住的就得是 5。
          // ⚠️ 存的是**基数**不是 target —— 候选不够时 target 被压低,那是这一批的偶然事实。
          //   存 target 的话,主管点一次 44 人的小格子,以后所有批次就永远是 44 人。
          //   ⚠️ 同时 target 也在(上面)—— 沿用时两个键都认,改版不丢"沿用"。
          batchSize: sheet.batchSize,
          expiresInDays,
          // ⭐ 按客服的精调也进快照 —— 下次一并沿用(容量部分原样带走,时效部分用卡片当前值)
          agentOverrides: mergeOverrides(sheet.agentOverrides, expiryByAgent),
        },
        expiresInDays,
        // ⚠️ 落库的是**卡片当前值**,不是模型原提案 —— 主管改了时效就得按改后的走。
        //   逐条 expiresInDays 只在**与批次不同**时才带:一样还带等于把批次时效冻进每条,
        //   将来批次层面改时效(撤销重发之类)就改不动这些单子了。
        items: sheet.items.map((it) => ({
          planId: it.planId,
          assigneeUserId: it.assigneeUserId,
          assignStrategy: it.assignStrategy,
          selectionMode: it.selectionMode,
          ...(expiryByAgent[it.assigneeUserId] != null &&
          expiryByAgent[it.assigneeUserId] !== expiresInDays
            ? { expiresInDays: expiryByAgent[it.assigneeUserId] }
            : {}),
        })),
      });
      const summary =
        `已确认分配:批次 #${res.assignmentId.slice(0, 8)} · ` +
        (sheet.potentialTreatment ? `${sheet.potentialTreatment} · ` : '') +
        `${res.assigned} 条 · ${sheet.byAgent.length} 位客服 · ${expiresInDays} 天有效` +
        (res.skipped.length ? ` · ${res.skipped.length} 条未落(确认期间被他人认领)` : '');
      onConfirmed(res.assignmentId, summary);
    } catch (e) {
      setError(e instanceof Error ? e.message : '确认失败');
    } finally {
      setSubmitting(false);
    }
  };

  const byPlan = new Map(sheet.items.map((i) => [i.planId, i]));

  return (
    <div className="rounded-lg border border-brand-100 bg-white text-[12px] shadow-sm">
      {/* ① 汇总 */}
      <div className="flex items-center gap-2 border-b border-slate-100 bg-brand-50/50 px-3 py-2">
        <Users className="h-3.5 w-3.5 flex-none text-brand-600" />
        <span className="font-semibold text-slate-800">
          拟分 {sheet.placed} 人 · {sheet.byAgent.length} 位客服
        </span>
        {sheet.unplaced > 0 && (
          <span className="rounded bg-amber-100 px-1.5 py-0.5 text-[10.5px] text-amber-700">
            {sheet.unplaced} 人分不下去
          </span>
        )}
      </div>

      {/* ② 按客服(产能视图)—— 这一层是主管真正在看的 */}
      <div className="divide-y divide-slate-50">
        {sheet.byAgent.map((a) => {
          const isOpen = open.has(a.userId);
          const mine = sheet.items.filter((i) => i.assigneeUserId === a.userId);
          return (
            <div key={a.userId}>
              <button
                type="button"
                onClick={() =>
                  setOpen((s) => {
                    const n = new Set(s);
                    if (n.has(a.userId)) n.delete(a.userId);
                    else n.add(a.userId);
                    return n;
                  })
                }
                className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-slate-50"
              >
                <ChevronRight
                  className={cn('h-3 w-3 flex-none text-slate-400 transition-transform', isOpen && 'rotate-90')}
                />
                {/* 姓名解析不出来时退回 id 前缀 —— ⛔ 不显示完整 uuid,400px 卡片会撑爆 */}
                <span className="flex-1 truncate font-medium text-slate-700">
                  {a.name ?? `#${a.userId.slice(0, 8)}`}
                </span>
                <span className="flex-none tabular-nums text-slate-500">
                  {a.count} 条
                  {a.inHandBefore > 0 && (
                    <span className="ml-1 text-[10.5px] text-slate-400">(在手 {a.inHandBefore})</span>
                  )}
                </span>
                {/* ⛔ 这里原来有个「铺平 N」标 —— 撤掉(2026-08-03 走查:主管看不懂)。
                    "铺平"是内部算法词,不是业务语言;而且它要表达的事(这几条不是他的老客户)
                    在展开的患者列表里用「专属」标已经说清楚了 —— 有标的是他的人,没标的不是。
                    ⚠️ 数据没丢:assignStrategy 仍逐条落库,T20 反推照常(T15 要的是"可见",
                    不是"必须以这个词出现在汇总行")。 */}
                {/* ⭐ 精调过的人必须一眼看得出 —— 一条上个月的临时精调如果静默沿用,没人会发现 */}
                {(a.overridden || expiryByAgent[a.userId] != null) && (
                  <span className="flex-none rounded bg-violet-50 px-1.5 py-0.5 text-[10px] text-violet-700 ring-1 ring-inset ring-violet-200">
                    精调
                  </span>
                )}
              </button>
              {isOpen && (
                <div className="bg-slate-50/60 px-3 pb-2 pl-8">
                  {/* 按客服的时效精调 —— 落到他名下**每一条**任务上。
                      容量只读:改它会换人群,得回对话让助手重出单(见组件顶部注释)。 */}
                  <div className="mb-1.5 flex flex-wrap items-center gap-1.5 text-[10.5px] text-slate-500">
                    <span>
                      在手 {a.inHandBefore} → <span className="text-slate-700">{a.loadAfter}</span>
                    </span>
                    <span className="text-slate-300">·</span>
                    <span>时效</span>
                    <DaySelect
                      value={expiryByAgent[a.userId] ?? expiresInDays}
                      disabled={done}
                      onChange={(d) =>
                        setExpiryByAgent((s) => {
                          const n = { ...s };
                          // 选回批次时效 = 取消精调(⛔ 不要留一条"恰好等于批次"的精调,
                          // 否则主管改批次时效时这个人不跟着动,而卡片上看不出为什么)
                          if (d === expiresInDays) delete n[a.userId];
                          else n[a.userId] = d;
                          return n;
                        })
                      }
                    />
                    {expiryByAgent[a.userId] != null && (
                      <span className="text-violet-600">单独设定</span>
                    )}
                  </div>
                <ul className="space-y-0.5 text-[11px] text-slate-500">
                  {mine.slice(0, 30).map((i) => (
                    <li key={i.planId} className="flex items-center gap-1.5">
                      {/* ⭐ 姓名优先;宿主没给姓名才退回 id 前缀(⛔ 不显示完整 uuid,卡片只有 400px) */}
                      <span className="truncate text-slate-700">
                        {i.patientName ?? `#${i.patientId.slice(0, 8)}`}
                      </span>
                      {i.medicalRecordNumber && (
                        <span className="flex-none font-mono text-[10px] text-slate-400">
                          {i.medicalRecordNumber}
                        </span>
                      )}
                      {/* ⛔ 只标「专属」,不标「铺平」—— 后者是算法词,主管看不懂(2026-08-03 走查)。
                          没有标的就是"不是他的老客户",反而更好读。 */}
                      {ASSIGN_STRATEGY_META[i.assignStrategy as AssignStrategy]?.isSpread ===
                        false && (
                        <span className="flex-none rounded bg-brand-50 px-1 text-[10px] text-brand-700">
                          专属
                        </span>
                      )}
                      {i.selectionMode === 'explore' && (
                        <span className="flex-none rounded bg-violet-100 px-1 text-[10px] text-violet-600">
                          探索
                        </span>
                      )}
                    </li>
                  ))}
                  {mine.length > 30 && (
                    <li className="text-[10px] text-slate-400">…另有 {mine.length - 30} 条</li>
                  )}
                </ul>
                </div>
              )}
            </div>
          );
        })}
      </div>

      {/* 本批没分到的人 —— 仍然列出来:主管要看见"他不是被漏了"。
          ⚠️ 措辞不能再写「已满」:没有容量上限这回事了,他没分到是因为**手上本来就最多**
          (水位法没轮到)或被精调成 0 名额。写「已满(0)」会自相矛盾。 */}
      {sheet.skippedAgents.length > 0 && (
        <div className="border-t border-slate-100 px-3 py-1.5 text-[10.5px] text-slate-400">
          本批未分到:{sheet.skippedAgents.map((a) => `${a.name ?? a.userId}(在手 ${a.inHand})`).join('、')}
        </div>
      )}

      {/* ③ 微调 —— 仍然**只有两类**(T13):指定客服、时效。
          时效有两级:这里是整批默认,展开某个客服可以单独给他一个(⚠️ 仍是"时效"这一项,
          不是第三项);容量只读,改它会换人群 → 回对话让助手重出单。 */}
      <div className="space-y-1.5 border-t border-slate-100 px-3 py-2">
        <div className="flex items-center gap-2">
          <span className="flex-none text-slate-500">时效</span>
          {/* ⭐ 1-7 天下拉,不是 3/5/7 三档 —— 档位是我们拍的,主管对自己团队的节奏有判断 */}
          <DaySelect value={expiresInDays} disabled={done} onChange={setExpiresInDays} />
          <span className="text-[10px] text-slate-400">每条召回计划各自记这个到期</span>
          {/* ⭐ T14:这个初值哪来的必须当场说清 —— 沿用上次 / 首次默认,信任度不同 */}
          <span className="text-[10px] text-slate-400">
            {sheet.basis === 'inherited' && sheet.basisFrom
              ? `沿用 ${sheet.basisFrom.slice(5, 10).replace('-', '/')} 那次`
              : sheet.basis === 'explicit'
                ? '本次指定'
                : '首次默认(暂无历史结案数据)'}
          </span>
        </div>
        {/* 本批人数:**只读**。改它就是换一批人 —— 卡片不做这种事(T13) */}
        <div className="flex items-center gap-2 text-[11px] text-slate-500">
          <span className="flex-none">人数</span>
          <span className="rounded border border-slate-200 px-2 py-0.5 tabular-nums text-slate-600">
            本批 {sheet.target} 人
          </span>
          <span className="text-[10px] text-slate-400">
            {sheet.target < sheet.batchSize
              ? `候选只有这么多(基数 ${sheet.batchSize} 不变)`
              : `要改说「这批 ${sheet.batchSize * 2} 人」· 池子里还有人随时能再分一批`}
          </span>
        </div>
        <p className="text-[10.5px] leading-relaxed text-slate-400">
          指定客服请在对话里说(如「王强这批给李莉」);换人群也回对话让助手重新圈。
        </p>
      </div>

      {/* 依据 —— 成品句子原样展示,不在前端改写 */}
      <div className="space-y-0.5 border-t border-slate-100 bg-slate-50/50 px-3 py-2 text-[10.5px] leading-relaxed text-slate-500">
        <p>{sheet.selectionNote}</p>
        <p>{sheet.basisNote}</p>
        <p>{sheet.rosterNote}</p>
      </div>

      {/* ④ 确认 */}
      <div className="border-t border-slate-100 px-3 py-2">
        {error && <p className="mb-1.5 text-[11px] text-rose-600">{error}</p>}
        {done ? (
          <div className="flex items-center gap-1.5 text-[11.5px] font-medium text-emerald-700">
            <Check className="h-3.5 w-3.5" />
            已分配 · 批次 #{assignmentId?.slice(0, 8)}
          </div>
        ) : (
          <Can
            perm={Permission.PLAN_DISPATCH}
            fallback={<p className="text-[11px] text-slate-400">你没有分配权限,请让主管确认</p>}
          >
            <button
              type="button"
              disabled={submitting || sheet.placed === 0}
              onClick={() => void confirm()}
              className="flex w-full items-center justify-center gap-1.5 rounded-md bg-brand-600 px-3 py-1.5 text-[12px] font-medium text-white transition-colors hover:bg-brand-700 disabled:opacity-50"
            >
              {submitting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
              确认分配 {sheet.placed} 条
            </button>
          </Can>
        )}
      </div>
    </div>
  );
}
