'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { Search, X } from 'lucide-react';
import {
  ANCHOR_MODE_META,
  NO_OWNER,
  TEMPERATURE_META,
  type AnchorModeValue,
  type TemperatureValue,
} from '@pac/types';
import { assignmentsApi, type CohortPatientRow } from '@/components/plans/assignments-api';
import { plansApi } from '@/components/plans/plans-api';
import { cn, formatGender } from '@/lib/utils';

/**
 * 「点了一格,先看看这批人是谁」—— 矩阵与助手之间的**中间一步**(2026-08-11 产品定)。
 *
 * ⭐ 存在的理由:主管点的是一个数字(「充填 · 半年到 1 年 420」),而他要为这批人负责。
 *   在交给助手之前先摊开"这些人是谁",比事后在确认单上一个个挑出来便宜得多。
 *
 * 🔴 **这张表与确认单必须是同一批人、同一个顺序**:数据走
 *   `GET /plans/assignments/cohort`,服务端用的是**出确认单那段同样的** SQL 与排序键。
 *   ⛔ 前端不许再排一次序 —— 那会让"我在这看到的前 20 个"跟"助手挑走的前 20 个"对不上,
 *   而且两边都不报错(T14)。
 *
 * 🔴 **搜索/筛选只影响"看",⛔ 不影响"分"**:点确定交出去的永远是**整格**的人。
 *   这是本组件最容易被误解的一点(筛到 12 人 → 点确定 → 分下去 420 人),
 *   所以按钮上**必须**始终写着整格的人数,筛选状态下还要显式说明。
 *   ⚠️ 要让筛选参与圈人,那是另一件事:得把条件也传给助手(医生/客服本来就是画像维度),
 *   ⛔ 别在这里偷偷把 filtered 当成人群 —— 那会分错人且无法追溯。
 */
export function CohortPreview({
  clinicId,
  cell,
  anchorMode,
  onCancel,
  onConfirm,
}: {
  clinicId: string;
  cell: { treatment: string; treatmentZh: string; temperature: TemperatureValue; count: number };
  anchorMode: AnchorModeValue;
  onCancel: () => void;
  /// 确定 —— 带上「确定」按钮的视口矩形,数据流粒子从那里起飞(⛔ 不能事后再查,窗已经关了)
  onConfirm: (from: { x: number; y: number; w: number; h: number }) => void;
}) {
  const PAGE_SIZE = 50;

  const [items, setItems] = useState<CohortPatientRow[]>([]);
  const [total, setTotal] = useState<number | null>(null);
  const [filtered, setFiltered] = useState<number | null>(null);
  const [page, setPage] = useState(1);
  const [done, setDone] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const okRef = useRef<HTMLButtonElement>(null);
  const scroller = useRef<HTMLDivElement>(null);

  // ── 三个「看」的条件 ──────────────────────────────────
  /// 输入框里的原文 vs 真正发出去的词 —— 分开是为了防抖(每敲一个字发一次请求会打爆后端)
  const [kwInput, setKwInput] = useState('');
  const [keyword, setKeyword] = useState('');
  const [doctor, setDoctor] = useState('');
  const [agent, setAgent] = useState('');
  useEffect(() => {
    const t = window.setTimeout(() => setKeyword(kwInput.trim()), 300);
    return () => window.clearTimeout(t);
  }, [kwInput]);

  /// 候选名单:医生走 `/plans/doctors`(服务端缓存 6h),客服走本诊所名册 —— 与筛选面板同源
  const [doctors, setDoctors] = useState<string[]>([]);
  const [agents, setAgents] = useState<Array<{ userId: string; name: string | null }>>([]);
  useEffect(() => {
    let alive = true;
    void plansApi.doctors().then((r) => alive && setDoctors(r.doctors)).catch(() => undefined);
    void assignmentsApi
      .agents(clinicId)
      .then((r) => alive && setAgents(r.agents.map((a) => ({ userId: a.userId, name: a.name }))))
      .catch(() => undefined);
    return () => {
      alive = false;
    };
  }, [clinicId]);

  /**
   * 🔴 **在途守卫必须是 ref,⛔ 不能靠 `loading` 这个 state**(与批次列表同一个坑):
   *   scroll 事件连发时 `loading` 还是上一帧的快照,挡不住第二、第三次触发 ——
   *   结果是同一页被追加两遍,React 报重复 key,界面上真的画两行。
   */
  const inFlight = useRef(false);

  const load = useCallback(
    async (p: number, replace: boolean) => {
      if (inFlight.current) return;
      inFlight.current = true;
      setLoading(true);
      try {
        const r = await assignmentsApi.cohort({
          clinicId,
          potentialTreatment: cell.treatment,
          temperature: cell.temperature,
          anchor: anchorMode,
          ...(keyword ? { keyword } : {}),
          ...(doctor ? { doctor } : {}),
          ...(agent ? { agentUserId: agent } : {}),
          page: p,
          pageSize: PAGE_SIZE,
        });
        setTotal(r.total);
        setFiltered(r.filtered);
        setError(null);
        setDone(r.items.length < PAGE_SIZE);
        setItems((prev) => {
          if (replace) return r.items;
          // ⚠️ 按 planId 去重:offset 分页在边界上仍可能重(池子被别人动过一条就会错位)
          const seen = new Set(prev.map((x) => x.planId));
          return [...prev, ...r.items.filter((x) => !seen.has(x.planId))];
        });
      } catch (e) {
        setError(e instanceof Error ? e.message : '取患者失败');
      } finally {
        inFlight.current = false;
        setLoading(false);
      }
    },
    [clinicId, cell.treatment, cell.temperature, anchorMode, keyword, doctor, agent],
  );

  /// 条件一变就回到第一页并清空 —— ⛔ 不能只追加:那会把新条件的结果接在旧结果后面
  useEffect(() => {
    setItems([]);
    setPage(1);
    setDone(false);
    scroller.current?.scrollTo({ top: 0 });
    void load(1, true);
  }, [load]);

  /**
   * 滚到底继续加载 —— **监听滚动容器的 scroll**,⛔ 不用 IntersectionObserver。
   * 理由与批次列表那处相同:零高度哨兵 IO 不回调,而且在内置浏览器面板里 IO 整个不工作,
   * 那条路**没法在我们的验证环境里证伪**。
   */
  useEffect(() => {
    const el = scroller.current;
    if (!el || done) return;
    const onScroll = () => {
      if (inFlight.current) return;
      if (el.scrollTop + el.clientHeight >= el.scrollHeight - 160) {
        setPage((p) => {
          const next = p + 1;
          void load(next, false);
          return next;
        });
      }
    };
    el.addEventListener('scroll', onScroll, { passive: true });
    onScroll(); // 首屏没撑满时也要能继续拉
    return () => el.removeEventListener('scroll', onScroll);
  }, [done, load, items.length]);

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onCancel();
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onCancel]);

  const cellTotal = total ?? cell.count;
  const hasFilter = Boolean(keyword || doctor || agent);
  const shown = filtered ?? cellTotal;
  const tag = ANCHOR_MODE_META[anchorMode].batchTag;

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/30 p-4"
      onClick={onCancel}
    >
      <div
        className="flex max-h-[86vh] w-full max-w-[64rem] flex-col overflow-hidden rounded-xl bg-white shadow-2xl"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex flex-none items-baseline gap-2 border-b px-4 py-3">
          <span className="text-[13.5px] font-semibold text-slate-900">
            {cell.treatmentZh} · {TEMPERATURE_META[cell.temperature].zh}
            {tag ? `（${tag}）` : ''}
          </span>
          {/* ⚠️ 总数取**服务端**的 total,⛔ 不用格子上那个数:格子可能是几秒前的快照 */}
          <span className="nums text-[12px] text-slate-500">共 {cellTotal} 人</span>
          {hasFilter && (
            <span className="nums text-[12px] text-brand-700">· 筛选后 {shown} 人</span>
          )}
          {total != null && total !== cell.count && (
            <span className="text-[11px] text-amber-700">（格子上显示 {cell.count}，池子刚变过）</span>
          )}
          <span className="ml-auto text-[11px] text-slate-400">按分配顺序排列，助手从上往下取</span>
          <button
            type="button"
            onClick={onCancel}
            aria-label="关闭"
            className="rounded-md p-1 text-slate-500 hover:bg-slate-100"
          >
            <X className="h-4 w-4" />
          </button>
        </div>

        {/* 筛选条 —— ⚠️ 全部走**服务端**过滤:滚动加载只加载了前几页,
            放前端过滤就成了"在已加载的那几页里搜",第 7 页的张三搜不到还不报错。 */}
        <div className="flex flex-none flex-wrap items-center gap-2 border-b bg-slate-50/60 px-4 py-2">
          <span className="relative">
            <Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
            <input
              value={kwInput}
              onChange={(e) => setKwInput(e.target.value)}
              placeholder="姓名 / 病历号"
              className="h-7 w-48 rounded-md border bg-white pl-7 pr-2 text-[12px] outline-none placeholder:text-slate-300 focus:border-brand-300"
            />
          </span>
          <select
            value={doctor}
            onChange={(e) => setDoctor(e.target.value)}
            className="h-7 max-w-[11rem] rounded-md border bg-white px-1.5 text-[12px] text-slate-700 outline-none focus:border-brand-300"
          >
            {/* ⚠️ 医生只有一个下拉:主治与上次接诊**任一命中**即可 ——
                主管说「薛玫的患者」时不分这两者,拆成两个筛选反而逼他猜。 */}
            <option value="">全部医生</option>
            {doctors.map((d) => (
              <option key={d} value={d}>
                {d}
              </option>
            ))}
          </select>
          <select
            value={agent}
            onChange={(e) => setAgent(e.target.value)}
            className="h-7 max-w-[11rem] rounded-md border bg-white px-1.5 text-[12px] text-slate-700 outline-none focus:border-brand-300"
          >
            <option value="">全部客服</option>
            {/* ⭐「无专属」是主管真会用的一档:他常想先看"没主的还剩多少" */}
            <option value={NO_OWNER}>无专属客服</option>
            {agents.map((a) => (
              <option key={a.userId} value={a.userId}>
                {a.name ?? a.userId}
              </option>
            ))}
          </select>
          {hasFilter && (
            <button
              type="button"
              onClick={() => {
                setKwInput('');
                setKeyword('');
                setDoctor('');
                setAgent('');
              }}
              className="text-[11.5px] text-brand-700 hover:underline"
            >
              清空筛选
            </button>
          )}
          {/* 🔴 这句必须在,而且必须在筛选条旁边 —— 见组件头注释 */}
          <span className="ml-auto text-[11px] text-slate-400">
            筛选只影响这张表，交给助手的仍是整格 {cellTotal} 人
          </span>
        </div>

        <div ref={scroller} className="min-h-0 flex-1 overflow-auto">
          {error ? (
            <p className="py-10 text-center text-[12px] text-rose-600">{error}</p>
          ) : items.length === 0 && loading ? (
            <div className="space-y-2 p-4">
              {Array.from({ length: 10 }).map((_, i) => (
                <div key={i} className="h-6 animate-pulse rounded bg-slate-100" />
              ))}
            </div>
          ) : items.length === 0 ? (
            <p className="py-10 text-center text-[12px] text-slate-400">
              {hasFilter ? '这批人里没有符合筛选条件的' : '这一格没有人'}
            </p>
          ) : (
            <table className="w-full border-collapse text-[12.5px] text-slate-700">
              <thead>
                <tr className="sticky top-0 z-10 bg-slate-50 shadow-[inset_0_-1px_0_#E2E8F0]">
                  {['#', '患者', '病历号', '性别', '年龄', '主治医生', '上次就诊医生', '专属客服'].map(
                    (h, i) => (
                      <th
                        key={h}
                        className={cn(
                          'whitespace-nowrap px-2.5 py-1.5 text-[11px] font-medium text-slate-500',
                          i === 0 ? 'w-12 text-right' : 'text-left',
                        )}
                      >
                        {h}
                      </th>
                    ),
                  )}
                </tr>
              </thead>
              <tbody>
                {items.map((r, i) => (
                  <tr key={r.planId} className="border-t hover:bg-slate-50">
                    <td className="nums px-2.5 py-1.5 text-right text-[11px] text-slate-400">{i + 1}</td>
                    <td className="px-2.5 py-1.5">
                      <span className="font-medium text-slate-900">
                        {r.patientName ?? `#${r.patientId.slice(0, 8)}`}
                      </span>
                      {/* ⭐ 之前被分过的人又出现了 —— 主管有权先知道,⛔ 别等他事后发现 */}
                      {r.repeated && (
                        <span className="ml-1.5 rounded bg-amber-50 px-1 py-px text-[10px] text-amber-700">
                          分过
                        </span>
                      )}
                    </td>
                    <td className="nums px-2.5 py-1.5 text-slate-500">{r.medicalRecordNumber ?? '—'}</td>
                    <td className="px-2.5 py-1.5 text-slate-500">{formatGender(r.gender) || '—'}</td>
                    <td className="nums px-2.5 py-1.5 text-slate-500">{r.age ?? '—'}</td>
                    <td className="px-2.5 py-1.5 text-slate-600">{r.attendingDoctor ?? '—'}</td>
                    <td className="px-2.5 py-1.5 text-slate-600">{r.lastVisitDoctor ?? '—'}</td>
                    <td className="px-2.5 py-1.5 text-slate-600">{r.dedicatedToName ?? '—'}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
          {/* 滚动加载的进度条 —— ⛔ 不要静默:主管要知道"还没到底"而不是"就这些人" */}
          {items.length > 0 && (
            <div className="flex items-center gap-2 px-4 pb-2 pt-2.5">
              <span className="h-px flex-1 bg-slate-100" />
              <span className="nums text-[10.5px] text-slate-400">
                {loading
                  ? '正在加载…'
                  : done
                    ? `已显示全部 ${items.length} 人`
                    : `已显示 ${items.length} / ${shown} 人 · 往下滚动继续加载`}
              </span>
              <span className="h-px flex-1 bg-slate-100" />
            </div>
          )}
        </div>

        <div className="flex flex-none items-center gap-2 border-t px-4 py-2.5">
          <span className="text-[11px] text-slate-400">
            这一步只是过目，点确定后助手才会出确认单（那时还能再改）
          </span>
          <span className="ml-auto flex items-center gap-2">
            <button
              type="button"
              onClick={onCancel}
              className="rounded-lg px-3 py-1.5 text-[12.5px] text-slate-600 hover:bg-slate-100"
            >
              取消
            </button>
            <button
              ref={okRef}
              type="button"
              onClick={() => {
                const b = okRef.current?.getBoundingClientRect();
                onConfirm(
                  b
                    ? { x: b.left, y: b.top, w: b.width, h: b.height }
                    : { x: window.innerWidth / 2, y: window.innerHeight / 2, w: 0, h: 0 },
                );
              }}
              className="nums rounded-lg bg-brand-600 px-3 py-1.5 text-[12.5px] font-medium text-white hover:bg-brand-700"
            >
              {/* 🔴 人数**永远是整格的** —— 筛选状态下尤其:这是"筛到 12 人却分出去 420"
                  这个误解唯一的拦截点 */}
              确定，交给助手（{cellTotal} 人）
            </button>
          </span>
        </div>
      </div>
    </div>
  );
}
