'use client';

import { useState, useMemo } from 'react';
import {
  Stethoscope,
  Pill,
  ClipboardList,
  UserRound,
  FileText,
  Image as ImageIcon,
  Calendar,
  CircleDollarSign,
  Undo2,
  MessageCircle,
  Receipt,
  Wallet,
  AlertTriangle,
  Users,
  AlertOctagon,
} from 'lucide-react';
import { diagnosisCodeNameZh, treatmentCategoryNameZh } from '@pac/types';
import { cn } from '@/lib/utils';
import type { AdaptedFact } from './adapt-data';

/**
 * FactsTimeline — 患者事实时间轴(drawer 用)
 *
 * 新版改进(W3 末):
 *   ① 顶部加 fact_type 多选筛选(chip 切换,显示该 type 计数)
 *   ② EMR / 影像节点 value 重写(原"病历 ad575..."无意义,改"主诉" / "modality + 牙位")
 *   ③ 图标走 lucide-react(替代 inline SVG path,更易维护)
 *   ④ 按 occurredAt ?? plannedFor 倒序;planned 加"约"前缀
 */
export function FactsTimeline({
  facts,
  initialTypes,
}: {
  facts: AdaptedFact[];
  /// 初始勾选的 fact_type(缺省全选);如治疗历史卡「详情」传 ['treatment_record'] → 默认只看治疗时间轴
  initialTypes?: string[];
}) {
  if (facts.length === 0) {
    return <div className="text-center py-12 text-sm text-slate-400">无 fact</div>;
  }

  // ─ 按筛选键统计 + 默认全选 ─
  //   计划治疗(treatment_record kind=planned)拆成独立筛选键 treatment_plan,跟已做的治疗分开筛
  const typeCounts = useMemo(() => {
    const c = new Map<string, number>();
    for (const f of facts) {
      const k = factFilterKey(f);
      c.set(k, (c.get(k) ?? 0) + 1);
    }
    return c;
  }, [facts]);
  const allTypes = useMemo(() => [...typeCounts.keys()].sort(), [typeCounts]);

  const [selected, setSelected] = useState<Set<string>>(() =>
    initialTypes && initialTypes.length ? new Set(initialTypes) : new Set(allTypes),
  );
  const allOn = selected.size === allTypes.length;
  const toggleAll = () => setSelected(allOn ? new Set() : new Set(allTypes));
  const toggleType = (t: string) =>
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(t)) next.delete(t);
      else next.add(t);
      return next;
    });

  // ─ 牙位筛选(单选)─ 把某颗牙的所有事实(诊断/治疗/影像等带牙位的)挑出来
  const allTeeth = useMemo(() => {
    const s = new Set<string>();
    for (const f of facts) for (const t of factTeeth(f)) s.add(t);
    return [...s].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
  }, [facts]);
  const [toothFilter, setToothFilter] = useState<string | null>(null);

  // 时间轴排序键:actual 用 occurredAt;planned 用 plannedFor 兜底
  const tkey = (f: AdaptedFact) => {
    const t = f.occurredAt ?? f.plannedFor;
    return t ? new Date(t).getTime() : -Infinity;
  };
  // 临床顺序 rank:诊断 1 → 影像 2 → 建议 3 → 计划 4 → 治疗 5。
  // 整体时间倒序(新→旧),同一时刻也跟着倒:治疗 → 计划 → 建议 → 影像 → 诊断。
  const typeRank = (f: AdaptedFact) => {
    if (f.type === 'diagnosis_record') return 1;
    if (f.type === 'image_record') return 2;
    if (f.type === 'recommendation_record') return 3;
    if (f.type === 'treatment_record') return f.kind === 'planned' ? 4 : 5;
    return 6;
  };
  const sorted = useMemo(
    () =>
      [...facts]
        .filter((f) => selected.has(factFilterKey(f)))
        .filter((f) => !toothFilter || factTeeth(f).includes(toothFilter))
        .sort((a, b) => tkey(b) - tkey(a) || typeRank(b) - typeRank(a)),
    [facts, selected, toothFilter],
  );

  // W4 末:sticky 顶部 — stats + filter 滚动时固定,时间轴在下方滚过
  //   父容器(drawer body)已 overflow-y-auto,sticky 在该 scroll container 内生效
  //   -mx-5 让 sticky 区域延伸到 drawer padding 边缘,bg-white 遮住下方时间轴
  return (
    <div className="space-y-3">
      <div className="sticky top-0 z-10 -mx-5 px-5 pt-1 pb-2 bg-white space-y-3 border-b border-slate-100">
        <StatsSummary facts={facts} />
        <div className="flex flex-wrap items-center gap-1.5">
          <FilterChip label="全部" count={facts.length} active={allOn} onClick={toggleAll} />
          <span className="text-slate-200 mx-0.5">|</span>
          {allTypes.map((t) => {
            const meta = FACT_META[t] ?? FACT_META_FALLBACK;
            return (
              <FilterChip
                key={t}
                label={meta.label}
                count={typeCounts.get(t) ?? 0}
                active={selected.has(t)}
                tone={meta.tone}
                onClick={() => toggleType(t)}
              />
            );
          })}
        </div>
        {allTeeth.length > 0 && (
          <div className="flex items-center gap-1.5 text-[11px]">
            <span className="text-slate-500">牙位</span>
            <select
              value={toothFilter ?? ''}
              onChange={(e) => setToothFilter(e.target.value || null)}
              className={cn(
                'rounded-md border px-2 py-0.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-sky-300',
                toothFilter
                  ? 'border-sky-200 bg-sky-50 text-sky-700 font-medium'
                  : 'border-slate-100 bg-white text-slate-600',
              )}
            >
              <option value="">全部牙位</option>
              {allTeeth.map((t) => (
                <option key={t} value={t}>
                  {t}
                </option>
              ))}
            </select>
            {toothFilter && (
              <span className="text-slate-400 tabular-nums">{sorted.length} 条</span>
            )}
          </div>
        )}
      </div>
      {sorted.length === 0 ? (
        <div className="text-center py-12 text-sm text-slate-400">已勾选类型无事实</div>
      ) : (
        <div className="relative pl-6">
          <div className="absolute left-[10px] top-1 bottom-1 w-px bg-slate-200" />
          {sorted.map((f) => (
            <TimelineRow key={f.id} fact={f} />
          ))}
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────
// StatsSummary — 4 块结构化聚合(资金 / 临床 / 行为 / 时间窗)
// ─────────────────────────────────────────────

function StatsSummary({ facts }: { facts: AdaptedFact[] }) {
  // ─ 资金 ─
  let paid = 0;
  let refunded = 0;
  for (const f of facts) {
    const c = (f.content ?? {}) as Record<string, unknown>;
    const cents = Number(c.amount_cents ?? 0);
    if (!Number.isFinite(cents)) continue;
    if (f.type === 'payment_record') paid += cents;
    else if (f.type === 'refund_record') refunded += Math.abs(cents);
  }
  const netCents = paid - refunded;

  // ─ 临床 ─ 诊断 top codes / 治疗 top categories
  const dxCodes = new Map<string, number>();
  const txCats = new Map<string, number>();
  for (const f of facts) {
    const c = (f.content ?? {}) as Record<string, unknown>;
    if (f.type === 'diagnosis_record') {
      const code = String(c.code ?? '');
      if (code) dxCodes.set(code, (dxCodes.get(code) ?? 0) + 1);
    } else if (f.type === 'treatment_record') {
      const cat = String(c.category ?? '');
      if (cat && cat !== 'review') txCats.set(cat, (txCats.get(cat) ?? 0) + 1);
    }
  }
  const dxTop = [...dxCodes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
  const txTop = [...txCats.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);

  // ─ 行为 ─ 接诊 / 预约履约率
  const encounterCount = facts.filter((f) => f.type === 'encounter_record').length;
  const apptAll = facts.filter((f) => f.type === 'appointment_record');
  const apptArrived = apptAll.filter((f) => {
    const c = (f.content ?? {}) as Record<string, unknown>;
    const st = String(c.status ?? '');
    return st === 'arrived' || st === 'completed' || st === 'in_treatment';
  }).length;
  const apptShowRate = apptAll.length > 0 ? Math.round((apptArrived / apptAll.length) * 100) : null;

  // ─ 时间窗 ─ 首次 / 最近 / 跨度天数
  const allTimes = facts
    .map((f) => f.occurredAt ?? f.plannedFor)
    .filter((t): t is string => !!t)
    .map((t) => new Date(t).getTime())
    .filter((t) => !Number.isNaN(t));
  const firstT = allTimes.length ? Math.min(...allTimes) : null;
  const lastT = allTimes.length ? Math.max(...allTimes) : null;
  const spanDays = firstT && lastT ? Math.round((lastT - firstT) / 86400_000) : null;
  const fmtDate = (t: number | null) =>
    t ? new Date(t).toLocaleDateString('zh-CN').replace(/\//g, '.') : '—';

  return (
    <div className="rounded-lg border border-slate-100 bg-slate-50/60 p-2.5">
      <div className="grid grid-cols-4 gap-3">
        <StatBlock label="累计净消费">
          <div className="text-[13px] font-semibold text-slate-900 tabular-nums">
            ¥{(netCents / 100).toLocaleString()}
          </div>
          <div className="text-[10px] text-slate-500 tabular-nums">
            收款 ¥{(paid / 100).toLocaleString()}
            {refunded > 0 && (
              <span className="text-rose-600 ml-1">- ¥{(refunded / 100).toLocaleString()}</span>
            )}
          </div>
        </StatBlock>
        <StatBlock label={`诊断 ${dxCodes.size} 种`}>
          <div className="text-[11px] text-slate-700 leading-tight">
            {dxTop.length > 0 ? (
              dxTop.map(([code, n]) => (
                <span key={code} className="inline-block mr-1.5" title={code}>
                  <span className="font-medium">{diagnosisCodeNameZh(code)}</span>
                  <span className="text-slate-400 ml-0.5">×{n}</span>
                </span>
              ))
            ) : (
              <span className="text-slate-400">—</span>
            )}
          </div>
        </StatBlock>
        <StatBlock label={`治疗 ${txCats.size} 类`}>
          <div className="text-[11px] text-slate-700 leading-tight">
            {txTop.length > 0 ? (
              txTop.map(([cat, n]) => (
                <span key={cat} className="inline-block mr-1.5" title={cat}>
                  <span className="font-medium">{treatmentCategoryNameZh(cat)}</span>
                  <span className="text-slate-400 ml-0.5">×{n}</span>
                </span>
              ))
            ) : (
              <span className="text-slate-400">—</span>
            )}
          </div>
        </StatBlock>
        <StatBlock label={`接诊 ${encounterCount}`}>
          <div className="text-[11px] text-slate-700 leading-tight">
            预约 {apptAll.length}{' '}
            {apptShowRate != null && (
              <span className={cn('tabular-nums', apptShowRate >= 80 ? 'text-emerald-700' : 'text-amber-700')}>
                履约 {apptShowRate}%
              </span>
            )}
          </div>
          <div className="text-[10px] text-slate-500 tabular-nums">
            {fmtDate(firstT)} → {fmtDate(lastT)}
            {spanDays != null && spanDays > 0 && <span className="ml-1">· 跨 {spanDays} 天</span>}
          </div>
        </StatBlock>
      </div>
    </div>
  );
}

function StatBlock({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex flex-col gap-0.5 min-w-0">
      <div className="text-[10px] text-slate-500 truncate">{label}</div>
      {children}
    </div>
  );
}

// ─────────────────────────────────────────────
// 单个 fact 时间轴行
// ─────────────────────────────────────────────

function TimelineRow({ fact }: { fact: AdaptedFact }) {
  const meta = FACT_META[fact.type] ?? FACT_META_FALLBACK;
  const Icon = meta.Icon;

  // W4 末视觉降噪 — 3 种语义色:rose(异常)/ emerald(钱进来)/ amber(爽约) — 其它 slate 灰
  //   appointment 特例:status=no_show/cancelled 才转 amber,其它走 slate
  const effectiveTone = computeEffectiveTone(fact, meta);
  const T = toneOf(effectiveTone);

  // planned 事实(预约/计划治疗)显示 plannedFor;加"约"前缀以区分已发生
  const tIso = fact.occurredAt ?? fact.plannedFor;
  const dateStr = tIso
    ? (fact.occurredAt ? '' : '约 ') + new Date(tIso).toLocaleDateString('zh-CN').replace(/\//g, '.')
    : '—';

  // 计划治疗(treatment_record kind=planned)≠ 已做的治疗:单独标"治疗计划" + 虚线空心点 + "计划"徽标,
  //   避免跟已完成的治疗只靠"约"前缀区分(客服反馈分不清)。
  const isPlannedTx = isPlannedTreatment(fact);
  const typeLabel = isPlannedTx ? '治疗计划' : meta.label;

  const { title, note } = factSummary(fact);
  const rightLines = rightColumn(fact);

  return (
    <div className="relative pb-3">
      {/* icon dot — 默认 slate 灰,异常类型(rose/emerald/amber)着色;计划治疗用虚线空心点(未发生);ring-white 保持时间轴脉络感 */}
      <span
        className={cn(
          'absolute -left-[22px] top-0.5 w-5 h-5 rounded-full flex items-center justify-center ring-2 ring-white',
          isPlannedTx ? 'border border-dashed border-sky-300 bg-white text-sky-500' : cn(T.bg, T.text),
        )}
      >
        <Icon className="w-3 h-3" />
      </span>
      {/* 2 列网格:左 1fr(时间/类型/标题/副字段),右 auto(牙位/金额/HH:MM/状态)*/}
      <div className="grid grid-cols-[1fr_auto] gap-3 items-start">
        <div className="min-w-0">
          <div className="text-[10.5px] text-slate-500 tabular-nums">
            <span className="font-mono">{dateStr}</span>
            <span className="mx-1 text-slate-300">·</span>
            <span className={isPlannedTx ? 'font-medium text-sky-600' : undefined}>{typeLabel}</span>
          </div>
          <div className="mt-0.5 text-[13px] font-medium text-slate-900 leading-tight">{title}</div>
          {note && (
            <div className="text-[11px] text-slate-500 leading-snug mt-0.5 line-clamp-2">{note}</div>
          )}
        </div>
        {rightLines.length > 0 && (
          <div className="flex flex-col items-end gap-0.5 text-[11.5px] text-slate-600 tabular-nums whitespace-nowrap">
            {rightLines.map((line, i) => (
              <span key={i} className={line.className}>{line.text}</span>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// 计算 row 实际显示的 tone — appointment status 异常时升 amber
function computeEffectiveTone(fact: AdaptedFact, meta: FactMeta): string {
  if (fact.type === 'appointment_record') {
    const st = String((fact.content as Record<string, unknown> | null)?.status ?? '');
    if (st === 'no_show' || st === 'cancelled') return 'amber';
    return 'slate'; // 普通预约不着色
  }
  return meta.tone;
}

// 状态 badge 统一样式(slate)— W4 末降噪:语义色集中在时间轴 icon dot,badge 不重复表达
const STATUS_BADGE = 'px-1.5 py-px rounded border text-[10.5px] bg-slate-50 text-slate-600 border-slate-100';
// 计划治疗专用徽标(sky)— 跟"已完成"(slate)区分:这是还没做的计划,不是已发生的事实
const PLAN_BADGE = 'px-1.5 py-px rounded border text-[10.5px] bg-sky-50 text-sky-700 border-sky-200';

// 右列数据 — 按 fact 类型挑最关键的 1-2 行
interface RightLine { text: string; className?: string }
function rightColumn(fact: AdaptedFact): RightLine[] {
  const c = (fact.content ?? {}) as Record<string, unknown>;
  const out: RightLine[] = [];
  switch (fact.type) {
    case 'diagnosis_record':
    case 'treatment_record':
    case 'recommendation_record': {
      const tooth = String(c.tooth_position ?? c.tooth_positions ?? '').trim();
      if (tooth) {
        const teeth = tooth.split(';').map((t) => t.trim()).filter(Boolean);
        out.push({ text: teeth.length > 4 ? `${teeth.slice(0, 3).join(';')} 等 ${teeth.length} 颗` : teeth.join(';'), className: 'font-medium text-slate-700' });
      }
      if (fact.type === 'treatment_record') {
        if (isPlannedTreatment(fact)) {
          // 待做的计划治疗:醒目的"计划"徽标(sky)。已做掉的不进这里(归下方已完成)
          out.push({ text: '计划', className: PLAN_BADGE });
        } else {
          const st = String(c.status ?? '');
          if (st === 'completed' || st === 'fulfilled') out.push({ text: '已完成', className: STATUS_BADGE });
          else if (st === 'cancelled') out.push({ text: '已取消', className: STATUS_BADGE });
        }
      }
      break;
    }
    case 'payment_record':
    case 'order_record':
    case 'recharge_record':
    case 'refund_record': {
      const cents = Number(c.amount_cents ?? 0);
      const yuan = `¥${(Math.abs(cents) / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
      const isNegative = fact.type === 'refund_record';
      // 金额本身用 emerald/rose 色(语义信号,跟时间轴 icon dot 同色系)
      out.push({ text: isNegative ? `- ${yuan}` : yuan, className: cn('font-semibold', isNegative ? 'text-rose-700' : 'text-emerald-700') });
      break;
    }
    case 'appointment_record': {
      const at = String(c.scheduled_at ?? '');
      if (at) out.push({ text: hhmm(at), className: 'font-mono text-slate-700' });
      const st = String(c.status ?? '');
      const stZh = APPT_STATUS_ZH[st];
      if (stZh) {
        // W4 末:状态 badge 样式统一(slate),不同 status 不再有不同色
        //   语义信号(爽约 amber / 异常 rose)走时间轴 icon dot,不重复在 badge 上表达
        out.push({ text: stZh, className: STATUS_BADGE });
      }
      break;
    }
    case 'encounter_record': {
      const etype = String(c.encounter_type ?? '');
      const zh = ENCOUNTER_TYPE_ZH[etype];
      if (zh) out.push({ text: zh, className: 'text-slate-500' });
      break;
    }
    case 'image_record': {
      const mod = String(c.modality ?? '');
      const modZh = MODALITY_ZH[mod] ?? mod;
      if (modZh) out.push({ text: modZh, className: 'text-slate-500' });
      const tooths = c.tooth_positions;
      const tooth = Array.isArray(tooths) ? tooths.join(';') : String(tooths ?? '');
      if (tooth) {
        const teeth = tooth.split(';').map((t) => t.trim()).filter(Boolean);
        out.push({ text: teeth.length > 4 ? `${teeth.slice(0, 3).join(';')} 等 ${teeth.length}` : teeth.join(';'), className: 'text-slate-600' });
      }
      break;
    }
    default:
      break;
  }
  return out;
}

// ─────────────────────────────────────────────
// 顶部筛选 chip
// ─────────────────────────────────────────────

// W4 末:filter chip 配色统一 — 不再 per-meta-tone 着色(全 sky 主色作"激活"信号)
//   语义色保留给时间轴 icon dot(rose/emerald/amber)— 顶部 chip 表"筛选状态"不表"语义"
function FilterChip({
  label,
  count,
  active,
  onClick,
  tone = 'sky',
}: {
  label: string;
  count: number;
  active: boolean;
  onClick: () => void;
  /// 选中色 = 该类型对应区块/行的颜色(诊断 rose / 治疗计划 sky / …),让 chip 跟内容一眼对得上
  tone?: string;
}) {
  const T = CHIP_TONE[tone] ?? CHIP_TONE_FALLBACK;
  return (
    <button
      type="button"
      onClick={onClick}
      className={cn(
        'inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[11px] border transition-colors',
        active ? cn(T.bg, T.text, T.border, 'font-medium') : 'bg-white text-slate-500 border-slate-100 hover:bg-slate-50',
      )}
    >
      <span>{label}</span>
      <span
        className={cn('tabular-nums text-[10px] px-1 rounded', active ? cn('bg-white/70', T.text) : 'bg-slate-100 text-slate-500')}
      >
        {count}
      </span>
    </button>
  );
}

// chip 选中态配色(跟时间轴行/区块同色系)。slate 用更可读的浅灰选中样式。
const CHIP_TONE_FALLBACK = { bg: 'bg-sky-50', text: 'text-sky-700', border: 'border-sky-200' };
const CHIP_TONE: Record<string, { bg: string; text: string; border: string }> = {
  rose: { bg: 'bg-rose-50', text: 'text-rose-700', border: 'border-rose-200' },
  emerald: { bg: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200' },
  amber: { bg: 'bg-amber-50', text: 'text-amber-800', border: 'border-amber-200' },
  sky: { bg: 'bg-sky-50', text: 'text-sky-700', border: 'border-sky-200' },
  indigo: { bg: 'bg-indigo-50', text: 'text-indigo-700', border: 'border-indigo-200' },
  slate: { bg: 'bg-slate-100', text: 'text-slate-700', border: 'border-slate-300' },
};

// ─────────────────────────────────────────────
// fact.type → 元信息(图标 + 中文 + 色)
// ─────────────────────────────────────────────

interface FactMeta {
  label: string;
  tone: string;
  Icon: React.ComponentType<{ className?: string }>;
}

// W4 末降噪 — 全局只 3 语义色:rose(异常/警示)/emerald(钱进来)/amber(警告) — 其它 slate 灰
//   icon 已表达类型,不再用颜色重复表达 fact 大类
//   appointment.tone='slate' 默认,status=no_show/cancelled 时 TimelineRow.computeEffectiveTone 升 amber
const FACT_META: Record<string, FactMeta> = {
  diagnosis_record: { label: '诊断', tone: 'rose', Icon: AlertTriangle },
  complaint_record: { label: '投诉', tone: 'rose', Icon: AlertOctagon },
  refund_record: { label: '退费', tone: 'rose', Icon: Undo2 },
  payment_record: { label: '收款', tone: 'emerald', Icon: CircleDollarSign },
  recharge_record: { label: '充值', tone: 'emerald', Icon: Wallet },
  // 其它一律 slate(默认),仅 icon 区分
  treatment_record: { label: '治疗', tone: 'slate', Icon: Stethoscope },
  // 计划治疗(虚拟筛选键,见 factFilterKey)—— sky,跟时间轴里计划治疗行的 sky 一致
  treatment_plan: { label: '治疗计划', tone: 'sky', Icon: Stethoscope },
  recommendation_record: { label: '医嘱', tone: 'slate', Icon: Pill },
  encounter_record: { label: '接诊', tone: 'slate', Icon: UserRound },
  emr_record: { label: '病历', tone: 'slate', Icon: FileText },
  image_record: { label: '影像', tone: 'slate', Icon: ImageIcon },
  appointment_record: { label: '预约', tone: 'slate', Icon: Calendar },
  consultation_record: { label: '咨询', tone: 'slate', Icon: MessageCircle },
  visit_registration_record: { label: '挂号', tone: 'slate', Icon: ClipboardList },
  order_record: { label: '医嘱单', tone: 'slate', Icon: Receipt },
  referral_record: { label: '转介', tone: 'slate', Icon: Users },
};

const FACT_META_FALLBACK: FactMeta = { label: '事实', tone: 'slate', Icon: FileText };

interface ToneCls {
  bg: string;
  text: string;
  border: string;
}

const TONE_FALLBACK: ToneCls = { bg: 'bg-slate-100', text: 'text-slate-700', border: 'border-slate-300' };

const TONE = {
  rose: { bg: 'bg-rose-100', text: 'text-rose-700', border: 'border-rose-200' },
  teal: { bg: 'bg-teal-100', text: 'text-teal-700', border: 'border-teal-200' },
  indigo: { bg: 'bg-indigo-100', text: 'text-indigo-700', border: 'border-indigo-200' },
  slate: TONE_FALLBACK,
  sky: { bg: 'bg-sky-100', text: 'text-sky-700', border: 'border-sky-200' },
  violet: { bg: 'bg-violet-100', text: 'text-violet-700', border: 'border-violet-200' },
  amber: { bg: 'bg-amber-100', text: 'text-amber-800', border: 'border-amber-200' },
  emerald: { bg: 'bg-emerald-100', text: 'text-emerald-700', border: 'border-emerald-200' },
} as const satisfies Record<string, ToneCls>;

const toneOf = (tone: string): ToneCls => (TONE as Record<string, ToneCls>)[tone] ?? TONE_FALLBACK;


const APPT_STATUS_ZH: Record<string, string> = {
  scheduled: '待就诊',
  rescheduled: '已改约',
  cancelled: '已取消',
  arrived: '已到诊',
  in_treatment: '就诊中',
  completed: '已完成',
  no_show: '爽约',
  walk_in: '现场加号',
};

const MODALITY_ZH: Record<string, string> = {
  pa: '根尖片',
  bw: '咬翼片',
  pano: '曲面断层',
  cbct: 'CBCT',
  intraoral_photo: '口内照片',
};

const CHANNEL_ZH: Record<string, string> = {
  cash: '现金',
  wechat: '微信',
  alipay: '支付宝',
  card: '银行卡',
  pos: 'POS',
  store: '储值',
  insurance: '保险',
  advance: '预付',
  membership_card: '会员卡',
  medical_insurance: '医保',
  apple_pay: 'Apple Pay',
  third_party: '第三方',
  check: '支票',
  debt: '挂账',
  mini_program: '小程序',
  other: '其他',
};

const APPT_TYPE_ZH: Record<string, string> = {
  first_visit: '初诊',
  follow_up: '复诊',
  emergency: '急诊',
  consultation: '咨询',
};

const ENCOUNTER_TYPE_ZH: Record<string, string> = {
  first_visit: '初诊',
  follow_up: '复诊',
  emergency: '急诊',
};

function hhmm(iso: string): string {
  const d = new Date(iso);
  return Number.isNaN(d.getTime())
    ? ''
    : d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
}

function truncate(s: string, n: number): string {
  if (!s) return '';
  return s.length > n ? s.slice(0, n) + '…' : s;
}

// ─────────────────────────────────────────────
// fact 摘要文本(每个 type 一个 case)
// ─────────────────────────────────────────────

/**
 * fact 摘要文本(每个 type 自定义有意义字段;严格去重)
 *
 * 去重纪律(W3 末调):
 *   - **title 不重复 type 词**(左侧已有 type chip "诊断"/"治疗"/"预约"/"接诊"…)
 *     → title 直接显示具体内容(如 "慢性牙周炎"/"全口洁治"/"初诊·张三主诉"),省"诊断 / 治疗" 前缀
 *   - **note 不重复 fact.status**(右侧已有 "已替代/已取消/已完成/已过期/已失效" status chip 兜底显示)
 *     → 治疗 status 不再显示;预约 status 仅在非默认态(no_show/cancelled 等)时显示
 *
 * 各 type 显示选型:
 *   diagnosis : 中文病名 / 牙位 + 医生
 *   treatment : subtype / 牙位 + quantity unit + 医生
 *   encounter : encounter_type (初/复诊) + chief_complaint + 医生
 *   emr       : illness_desc 主诉 / 治疗计划 + 医生
 *   image     : modality 中文 / 牙位 + finding + 医生
 *   appointment: type 翻译 + 时间 + 主诉(+ 非默认 status / cancellation_reason if any)
 *   payment   : 金额 + channel 中文
 *   refund    : 金额 + reason
 *
 * 移除:doctor_id / *_external_id / source_encounter_id / code_source 等 raw / 技术字段
 */
function factSummary(f: AdaptedFact): { title: string; note: string } {
  const c = (f.content ?? {}) as Record<string, unknown>;
  const doctor = (c.doctor_name as string | undefined) ?? '';
  const dr = doctor ? `${doctor}医生` : '';
  switch (f.type) {
    case 'diagnosis_record': {
      const code = (c.code as string) ?? '?';
      const name = (c.name_zh as string) ?? (c.name as string) ?? '';
      return { title: name || code, note: dr };
    }
    case 'treatment_record': {
      const cat = (c.category as string) ?? '';
      const sub = (c.subtype as string) ?? '';
      const qty = c.quantity != null ? Number(c.quantity) : null;
      const unit = (c.unit_name as string) ?? '';
      const qtyStr = qty != null && qty > 0 ? `${qty}${unit ? ' ' + unit : ''}` : '';
      return {
        title: sub || treatmentCategoryNameZh(cat) || '治疗',
        note: [qtyStr, dr].filter(Boolean).join(' · '),
      };
    }
    case 'recommendation_record': {
      const code = (c.code as string) ?? '';
      // 优先 host 原文中文(name_zh/name,如"建议种植修复"),再字典翻译(IMPLANT_RECOMMENDED→建议种植),
      // 最后才裸 code 兜底 —— 不再直接展示英文枚举码。
      const name = (c.name_zh as string) || (c.name as string) || '';
      const zh = code ? diagnosisCodeNameZh(code) : '';
      return { title: name || zh || (code ? `建议 ${code}` : '医生建议'), note: dr };
    }
    case 'encounter_record': {
      const etype = (c.encounter_type as string) ?? '';
      const chief = (c.chief_complaint as string) ?? '';
      const etypeZh = etype ? ENCOUNTER_TYPE_ZH[etype] ?? etype : '';
      const title = chief ? truncate(chief, 32) : etypeZh || '到诊';
      return { title, note: dr };
    }
    case 'emr_record': {
      const ill = (c.illness_desc as string) ?? '';
      const plan = (c.treatment_plan as string) ?? '';
      const disp = parseFirstMessage(c.disposal);
      const planText = plan || disp;
      return {
        title: ill ? truncate(ill, 36) : '病历记录',
        note: [planText && `计划:${truncate(planText, 36)}`, dr].filter(Boolean).join(' · '),
      };
    }
    case 'image_record': {
      const mod = (c.modality as string) ?? '';
      const finding = (c.finding as string) ?? '';
      const modZh = MODALITY_ZH[mod] ?? mod ?? '影像';
      // title 优先 finding(有内容时更直观),fallback modality
      return {
        title: finding ? truncate(finding, 32) : modZh,
        note: [finding && modZh, dr].filter(Boolean).join(' · '),
      };
    }
    case 'appointment_record': {
      const type = (c.appointment_type as string) ?? '';
      const cancel = (c.cancellation_reason as string) ?? '';
      const complaint = (c.complaint_text as string) ?? (c.complaint_category as string) ?? '';
      const typeZh = type ? APPT_TYPE_ZH[type] ?? type : '';
      // title:type + 主诉(时间/状态在右列展示)
      return {
        title: [typeZh, complaint && truncate(complaint, 18)].filter(Boolean).join(' · ') || '预约',
        note: [cancel && `取消原因:${truncate(cancel, 16)}`, dr].filter(Boolean).join(' · '),
      };
    }
    case 'payment_record': {
      // 金额在右列;title 用渠道翻译
      const ch = (c.channel as string) ?? '';
      const chZh = CHANNEL_ZH[ch] ?? ch;
      return { title: chZh || '收款', note: dr };
    }
    case 'recharge_record': {
      const ch = (c.channel as string) ?? '';
      const chZh = CHANNEL_ZH[ch] ?? ch;
      return { title: chZh || '充值', note: dr };
    }
    case 'order_record': {
      const items = (c.items as Array<Record<string, unknown>> | undefined) ?? [];
      const itemNames = items.map((i) => String(i?.name ?? '')).filter(Boolean);
      return { title: itemNames.length > 0 ? truncate(itemNames.join(';'), 28) : '医嘱单', note: dr };
    }
    case 'refund_record': {
      const reason = (c.reason as string) ?? '';
      return { title: reason || '退费', note: dr };
    }
    default: {
      return { title: f.title ?? f.type, note: f.summary ?? '' };
    }
  }
}

/// 牙位归一:剥面/方位后缀保留牙位 base("36 OB"→"36";"1D OD"→"1D";"11"→"11")
function toothBase(t: string): string {
  const m = /^(\d{1,2}[A-E]?)/i.exec(t.trim());
  return m ? m[1]!.toUpperCase() : t.trim().toUpperCase();
}

/// 是否"待做的"计划治疗 = planned 且还没做(状态非完成/履约)。
/// 已经做掉的计划(status=completed/fulfilled)不算治疗计划 —— 归到"治疗 / 已完成"。
function isPlannedTreatment(f: AdaptedFact): boolean {
  if (f.type !== 'treatment_record' || f.kind !== 'planned') return false;
  const st = String((f.content as Record<string, unknown> | null)?.status ?? '');
  return st !== 'completed' && st !== 'fulfilled';
}

/// 筛选键:待做的计划治疗拆成独立的 treatment_plan,跟已做治疗分开筛
function factFilterKey(f: AdaptedFact): string {
  return isPlannedTreatment(f) ? 'treatment_plan' : f.type;
}

/// 取一条 fact 的牙位集合(content.tooth_position 字符串 或 tooth_positions 数组)
function factTeeth(f: AdaptedFact): string[] {
  const c = (f.content ?? {}) as Record<string, unknown>;
  const tp = c.tooth_positions;
  const raw = Array.isArray(tp) ? tp.join(';') : String(c.tooth_position ?? tp ?? '');
  return raw
    .split(/[;,]+/)
    .map((s) => s.trim())
    .filter(Boolean)
    .map(toothBase);
}

/// 从 JSON 数组字段(EMR.disposal / treatment_plan 等)取第一条 message
function parseFirstMessage(raw: unknown): string {
  if (typeof raw !== 'string' || !raw || raw === 'null') return '';
  try {
    const arr = JSON.parse(raw);
    if (Array.isArray(arr) && arr[0] && typeof arr[0] === 'object') {
      return String((arr[0] as Record<string, unknown>).message ?? '');
    }
  } catch {
    /* swallow */
  }
  return '';
}
