'use client';

import { useMemo, useState } from 'react';
import { Check, ChevronRight, GripVertical, Loader2, Users, X } from 'lucide-react';
import {
  ASSIGNMENT_EXPIRES_DAYS_PRESETS,
  AssignStrategy,
  Permission,
  type AgentOverride,
  type AssignmentProposal,
  type ProposedItem,
} from '@pac/types';
import { Can } from '@/components/can';
import { assignmentsApi } from '@/components/plans/assignments-api';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { cn } from '@/lib/utils';

/**
 * 全景确认单 —— 主管**一次确认**就落库的那张卡片。
 *
 * ⚠️ 必须是**原生 React 组件**,不能用 `render_artifact`:
 * artifact 跑在 `sandbox="allow-scripts"` 的 iframe 里、CSP `connect-src 'none'`,
 * **卡片内不可能发出写请求**。分岔记牢:只读展示用 artifact,可交互用原生组件。
 *
 * ── 卡片上能做什么(2026-08-03 产品定)──────────────────────────
 * 四件**局部**操作,都不需要重新圈人、也不需要重跑落人算法:
 *   ① 逐条改时效(1-7 天)   ② 删掉某一条
 *   ③ 把某一条拖给别的客服  ④ 移除某个客服(连同他名下的条目)
 * ⛔ 换人群、改批次人数、按条件重新收敛 —— 一律回对话让助手重出一版。
 *   判据很简单:**要不要重跑服务端算法**。要,就回对话;不要,才放到卡片上。
 *
 * ⚠️ 这不违 T13「微调只有两项」—— 那条防的是"卡片变成第二个筛选器"。
 *   上面四件事都是对**已经算好的结果**做局部修正,不产生新的人群。
 *
 * ── 布局 ────────────────────────────────────────────────
 * 助手窗只有 400px 宽(最大化后更宽),所以**不用 table**(横向撑爆),用卡片列表;
 * 患者明细按客服折叠,展开才看 —— 兼顾"尽明细"与"一眼可确认"。
 */

/** 卡片本地状态下,一条召回计划的**生效**形态(叠加了主管的改动) */
interface EffectiveItem extends ProposedItem {
  /** 生效的经办人(拖拽后可能不是提案里的那个) */
  assignee: string;
  /** 生效时效(天) */
  days: number;
  /** 是否被主管拖到了别人名下 —— 落库时策略要改成 manual */
  moved: boolean;
}

/**
 * 精调表落库形态 = 提案带来的名额上限部分(原样带走)。
 *
 * ⚠️ 时效**不再进这里** —— 它已经细到"每一条召回计划"一级,再往 agentOverrides
 *   里塞一个"按客服的时效"就是两套口径打架(同一个人两处不同的值,听谁的?)。
 * ⚠️ 名额上限必须原样带走:少带一次,主管上次设的「李莉这批最多 5 条」
 *   就在这一次确认里被悄悄清掉了,而界面上完全看不出来。
 */
function carryOverrides(fromSheet: Record<string, AgentOverride>): 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 };
  }
  return out;
}

/**
 * 时效选择 —— 1~7 天。用 shadcn `Select`(Radix),不用原生 `<select>`:
 * 原生下拉在深色/窄屏下样式不可控,而这个控件现在**每一条召回计划都有一个**,
 * 出现几百次,样式必须跟卡片其余部分一致。
 * ⚠️ 值域来自 `ASSIGNMENT_EXPIRES_DAYS_PRESETS`(共享常量),⛔ 别在这里内联 [1..7]。
 */
function DaySelect({
  value,
  disabled,
  onChange,
  compact,
}: {
  value: number;
  disabled?: boolean;
  onChange: (days: number) => void;
  /** 条目行用紧凑版(只显示数字,省掉"天"字省宽度) */
  compact?: boolean;
}) {
  return (
    <Select
      value={String(value)}
      disabled={disabled}
      onValueChange={(v) => onChange(Number(v))}
    >
      <SelectTrigger
        className={cn(
          'h-6 gap-1 px-1.5 text-[11px]',
          compact ? 'w-[54px]' : 'w-[76px]',
        )}
      >
        <SelectValue />
      </SelectTrigger>
      {/* z 层级已在 ui/select.tsx 统一抬到 70(助手窗是 z-60,见那里的层级约定) */}
      <SelectContent>
        {ASSIGNMENT_EXPIRES_DAYS_PRESETS.map((d) => (
          <SelectItem key={d} value={String(d)} className="text-[12px]">
            {d} 天
          </SelectItem>
        ))}
      </SelectContent>
    </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。
   * 改它 = 把所有**没有单独设过**的条目一起改;单独设过的不动(下面 dayByPlan 优先)。
   */
  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());

  /// 逐条时效覆盖(planId → 天)。⚠️ 时效**落到每一条召回计划**,不是按客服
  const [dayByPlan, setDayByPlan] = useState<Record<string, number>>({});
  /// 拖拽改派(planId → 新经办人)。落库时这些条的策略改成 manual(主管指定)
  const [moveByPlan, setMoveByPlan] = useState<Record<string, string>>({});
  /// 主管删掉的条目
  const [dropped, setDropped] = useState<Set<string>>(new Set());
  /// 待确认移除的客服(AlertDialog 用;移除 = 连同他名下条目一起移出本批)
  const [pendingRemove, setPendingRemove] = useState<string | null>(null);
  /// 拖拽悬停中的目标客服(高亮用)
  const [dragOver, setDragOver] = useState<string | null>(null);

  const done = state === 'confirmed';
  const readOnly = done || submitting;

  /**
   * 生效条目 = 提案条目 − 删掉的 + 拖拽改派 + 逐条时效。
   * ⭐ 所有展示都从这里派生(条数、按客服分组、确认按钮上的数字),
   *   ⛔ 别再直接用 `sheet.byAgent` —— 它是**提案时**的分组,拖过一条就对不上了。
   */
  const items = useMemo<EffectiveItem[]>(
    () =>
      sheet.items
        .filter((i) => !dropped.has(i.planId))
        .map((i) => ({
          ...i,
          assignee: moveByPlan[i.planId] ?? i.assigneeUserId,
          days: dayByPlan[i.planId] ?? expiresInDays,
          moved: moveByPlan[i.planId] != null && moveByPlan[i.planId] !== i.assigneeUserId,
        })),
    [sheet.items, dropped, moveByPlan, dayByPlan, expiresInDays],
  );

  /** 按客服分组(顺序沿用提案里的客服顺序;拖过去的新客服排在后面) */
  const groups = useMemo(() => {
    const byUser = new Map<string, EffectiveItem[]>();
    for (const it of items) {
      const arr = byUser.get(it.assignee) ?? [];
      arr.push(it);
      byUser.set(it.assignee, arr);
    }
    const order = [
      ...sheet.byAgent.map((a) => a.userId).filter((u) => byUser.has(u)),
      ...[...byUser.keys()].filter((u) => !sheet.byAgent.some((a) => a.userId === u)),
    ];
    const nameOf = new Map(sheet.byAgent.map((a) => [a.userId, a.name]));
    const inHandOf = new Map(sheet.byAgent.map((a) => [a.userId, a.inHandBefore]));
    return order.map((userId) => ({
      userId,
      name: nameOf.get(userId) ?? null,
      inHandBefore: inHandOf.get(userId) ?? 0,
      list: byUser.get(userId)!,
    }));
  }, [items, sheet.byAgent]);

  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,
          // ⚠️ 存的是**基数**不是 target —— 候选不够时 target 被压低,那是这一批的偶然事实。
          //   存 target 的话,主管点一次 44 人的小格子,以后所有批次就永远是 44 人。
          //   ⚠️ 同时 target 也在(上面)—— 沿用时两个键都认,改版不丢"沿用"。
          batchSize: sheet.batchSize,
          expiresInDays,
          agentOverrides: carryOverrides(sheet.agentOverrides),
          // 主管在卡片上做过的局部修正 —— 事后要能解释"这批为什么跟助手算的不一样"
          manualEdits: {
            dropped: [...dropped],
            moved: Object.keys(moveByPlan).length,
            perItemExpiry: Object.keys(dayByPlan).length,
          },
        },
        expiresInDays,
        // ⚠️ 落库的是**卡片当前值**,不是模型原提案。
        //   逐条 expiresInDays 只在**与批次不同**时才带:一样还带等于把批次时效冻进每条,
        //   将来批次层面改时效(撤销重发之类)就改不动这些单子了。
        items: items.map((it) => ({
          planId: it.planId,
          assigneeUserId: it.assignee,
          // ⭐ 拖过的标 manual(主管指定)—— 不标的话事后分析会把它算成算法的选择,
          //   而 T20 要反推的正是"算法选得准不准"
          assignStrategy: it.moved ? AssignStrategy.MANUAL : it.assignStrategy,
          selectionMode: it.selectionMode,
          ...(it.days !== expiresInDays ? { expiresInDays: it.days } : {}),
        })),
      });
      const summary =
        `已确认分配:批次 #${res.assignmentId.slice(0, 8)} · ` +
        (sheet.potentialTreatment ? `${sheet.potentialTreatment} · ` : '') +
        `${res.assigned} 条 · ${groups.length} 位客服 · ${expiresInDays} 天有效` +
        (dropped.size ? ` · 主管移除 ${dropped.size} 条` : '') +
        (res.skipped.length ? ` · ${res.skipped.length} 条未落(确认期间被他人认领)` : '');
      onConfirmed(res.assignmentId, summary);
    } catch (e) {
      setError(e instanceof Error ? e.message : '确认失败');
    } finally {
      setSubmitting(false);
    }
  };

  /** 移除某个客服 = 他名下的条目**一并移出本批** */
  const removeAgent = (userId: string) => {
    setDropped((s) => {
      const n = new Set(s);
      for (const it of items) if (it.assignee === userId) n.add(it.planId);
      return n;
    });
    setPendingRemove(null);
  };

  const removingGroup = groups.find((g) => g.userId === pendingRemove);

  return (
    <Card className="overflow-hidden border-brand-100 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">
          拟分 {items.length} 人 · {groups.length} 位客服
        </span>
        {dropped.size > 0 && (
          <Badge variant="secondary" className="text-[10px] font-normal">
            已移除 {dropped.size}
          </Badge>
        )}
        {sheet.unplaced > 0 && (
          <Badge variant="secondary" className="bg-amber-100 text-[10px] font-normal text-amber-700">
            {sheet.unplaced} 人分不下去
          </Badge>
        )}
        {/* ⭐ 批次时效放**右上角**:它是"这一整批"的设置,和左边"这一整批多少人"同一层级,
            放在底部微调区会让人以为它跟下面的逐条时效是一回事。
            ⚠️ 文案是「N 天**后自动退回**」而不是光一个「时效」——
            主管要知道到期会发生什么(单子回池),不然这个数对他没有意义。 */}
        <div className="ml-auto flex flex-none items-center gap-1 text-[10.5px] text-slate-500">
          <DaySelect value={expiresInDays} disabled={readOnly} onChange={setExpiresInDays} />
          <span>后自动退回</span>
        </div>
      </div>

      {/* ② 按客服(产能视图)—— 这一层是主管真正在看的 */}
      <CardContent className="p-0">
        <div className="divide-y divide-slate-50">
          {groups.map((g) => {
            const isOpen = open.has(g.userId);
            return (
              <div
                key={g.userId}
                // ⭐ 整个客服块都是投放区(不只是标题行):展开后列表很长,
                //   拖到中间松手是最自然的动作,只认标题行会让人反复试
                onDragOver={(e) => {
                  if (readOnly) return;
                  e.preventDefault();
                  setDragOver(g.userId);
                }}
                onDragLeave={() => setDragOver((u) => (u === g.userId ? null : u))}
                onDrop={(e) => {
                  e.preventDefault();
                  setDragOver(null);
                  const planId = e.dataTransfer.getData('text/plan-id');
                  if (planId) setMoveByPlan((m) => ({ ...m, [planId]: g.userId }));
                }}
                className={cn(dragOver === g.userId && 'bg-brand-50/60 ring-1 ring-inset ring-brand-300')}
              >
                <div className="flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-50">
                  <button
                    type="button"
                    onClick={() =>
                      setOpen((s) => {
                        const n = new Set(s);
                        if (n.has(g.userId)) n.delete(g.userId);
                        else n.add(g.userId);
                        return n;
                      })
                    }
                    className="flex min-w-0 flex-1 items-center gap-2 text-left"
                  >
                    <ChevronRight
                      className={cn(
                        'h-3 w-3 flex-none text-slate-400 transition-transform',
                        isOpen && 'rotate-90',
                      )}
                    />
                    {/* 姓名解析不出来时退回 id 前缀 —— ⛔ 不显示完整 uuid,卡片会撑爆 */}
                    <span className="truncate font-medium text-slate-700">
                      {g.name ?? `#${g.userId.slice(0, 8)}`}
                    </span>
                    <span className="flex-none tabular-nums text-slate-500">
                      {g.list.length} 条
                      <span className="ml-1 text-[10.5px] text-slate-400">
                        (在手 {g.inHandBefore} → {g.inHandBefore + g.list.length})
                      </span>
                    </span>
                  </button>
                  {!readOnly && (
                    <Button
                      variant="ghost"
                      size="icon"
                      title="把这位客服移出本批(他名下的条目一并移除)"
                      onClick={() => setPendingRemove(g.userId)}
                      className="h-6 w-6 flex-none text-slate-400 hover:text-rose-600"
                    >
                      <X className="h-3.5 w-3.5" />
                    </Button>
                  )}
                </div>

                {isOpen && (
                  <ul className="space-y-px bg-slate-50/60 px-2 pb-2">
                    {g.list.map((i) => (
                      <li
                        key={i.planId}
                        draggable={!readOnly}
                        onDragStart={(e) => {
                          e.dataTransfer.setData('text/plan-id', i.planId);
                          e.dataTransfer.effectAllowed = 'move';
                        }}
                        className={cn(
                          'flex items-center gap-1.5 rounded px-1.5 py-1 hover:bg-white',
                          !readOnly && 'cursor-grab active:cursor-grabbing',
                        )}
                      >
                        {!readOnly && (
                          <GripVertical className="h-3 w-3 flex-none text-slate-300" />
                        )}
                        {/* ⭐ 姓名优先;宿主没给姓名才退回 id 前缀 */}
                        <span className="min-w-0 flex-1 truncate text-[11.5px] text-slate-700">
                          {i.patientName ?? `#${i.patientId.slice(0, 8)}`}
                          {i.medicalRecordNumber && (
                            <span className="ml-1 font-mono text-[10px] text-slate-400">
                              {i.medicalRecordNumber}
                            </span>
                          )}
                        </span>
                        {/* ⛔ 只标「专属」,不标「铺平」—— 后者是算法词,主管看不懂。
                            没标的就是"不是他的老客户",反而更好读。 */}
                        {i.assignStrategy === AssignStrategy.DEDICATED && !i.moved && (
                          <Badge
                            variant="secondary"
                            className="flex-none bg-brand-50 px-1 text-[10px] font-normal text-brand-700"
                          >
                            专属
                          </Badge>
                        )}
                        {i.moved && (
                          <Badge
                            variant="secondary"
                            className="flex-none bg-violet-50 px-1 text-[10px] font-normal text-violet-700"
                          >
                            指定
                          </Badge>
                        )}
                        {i.selectionMode === 'explore' && (
                          <Badge
                            variant="secondary"
                            className="flex-none bg-violet-100 px-1 text-[10px] font-normal text-violet-600"
                          >
                            探索
                          </Badge>
                        )}
                        {/* ⭐ 时效**逐条**:落库时写进这条 plan 的 assignment_expires_at */}
                        <DaySelect
                          compact
                          value={i.days}
                          disabled={readOnly}
                          onChange={(d) => setDayByPlan((m) => ({ ...m, [i.planId]: d }))}
                        />
                        {!readOnly && (
                          <Button
                            variant="ghost"
                            size="icon"
                            title="本批不分这个人"
                            onClick={() => setDropped((s) => new Set(s).add(i.planId))}
                            className="h-5 w-5 flex-none text-slate-300 hover:text-rose-600"
                          >
                            <X className="h-3 w-3" />
                          </Button>
                        )}
                      </li>
                    ))}
                  </ul>
                )}
              </div>
            );
          })}
        </div>
      </CardContent>

      {/* 本批没分到的人 —— 仍然列出来:主管要看见"他不是被漏了"。
          ⚠️ 措辞不能写「已满」:没有容量上限这回事了,他没分到是因为**手上本来就最多**
          (水位法没轮到)或被精调成 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>
      )}

      {/* ③ 批次级设置 */}
      <div className="space-y-1.5 border-t border-slate-100 px-3 py-2">
        {/* 本批人数:**只读**。改它就是换一批人 —— 卡片不做这种事(要回对话) */}
        <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>
        {/* ⭐ T14:时效/人数这两个数**哪来的**必须当场说清 —— 沿用上次 / 首次默认,信任度不同 */}
        <p className="text-[10.5px] leading-relaxed text-slate-400">
          时效{' '}
          {sheet.basis === 'inherited' && sheet.basisFrom
            ? `沿用 ${sheet.basisFrom.slice(5, 10).replace('-', '/')} 那次`
            : sheet.basis === 'explicit'
              ? '为本次指定'
              : '为首次默认'}
          {Object.keys(dayByPlan).length > 0 && `,其中 ${Object.keys(dayByPlan).length} 条单独设了`};
          条目可<span className="font-medium text-slate-500">拖到别的客服</span>名下;
          换人群 / 改人数回对话让助手重新圈。
        </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
              disabled={submitting || items.length === 0}
              onClick={() => void confirm()}
              className="w-full gap-1.5"
            >
              {submitting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />}
              确认分配 {items.length} 条
            </Button>
          </Can>
        )}
      </div>

      {/* 移除客服前的二次确认 —— ⚠️ 必须说清"他名下的条目会一起移出",
          否则主管以为只是把人从名单上划掉,回头发现少了 20 个患者 */}
      <AlertDialog open={pendingRemove != null} onOpenChange={(o) => !o && setPendingRemove(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>
              把「{removingGroup?.name ?? removingGroup?.userId.slice(0, 8)}」移出本批?
            </AlertDialogTitle>
            <AlertDialogDescription>
              他名下的 {removingGroup?.list.length ?? 0} 条会
              <span className="font-medium text-slate-700">一并移出本批</span>
              (患者留在池子里,下批再说)。想把这些人交给别人,先把条目拖过去,
              或者回对话说一句让助手重出一版。
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>取消</AlertDialogCancel>
            <AlertDialogAction onClick={() => pendingRemove && removeAgent(pendingRemove)}>
              移出本批
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </Card>
  );
}
