'use client';

import { useState } from 'react';
import {
  TEMPERATURE_META,
  TEMPERATURE_ORDER,
  potentialTreatmentItemName,
  temperatureWindowHint,
  type TemperatureValue,
} from '@pac/types';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/card';
import type { PoolMatrix as PoolMatrixData, PoolMatrixRow } from './plans-api';

/**
 * PoolMatrix — 初选矩阵(8 潜在治疗 × 3 召回窗口)。排版按 claude design 的 **1b「连续色带」**。
 *
 * 生产线的**第一环**:主管在这里点一格,就完成了「初选」,人群随即交给助手出确认单。
 * ⚠️ 落点已经变过两次:最早是执行页左栏的「分配」浮层,后来是主管工作台的居中 modal,
 *   2026-08-11 起**常驻**在主管工作台「分一批新的」面板里(见 supervisor/new-batch.tsx)。
 *   T16 也已改写(主管工作台是独立路由)——⛔ 别再照旧注释说"不新开路由"。
 *
 * ═══ 1b 的核心:矩阵区是**一张连续的渐变面**,不是 24 个色块 ═══════════
 * 三列共用一条 `amber → emerald → sky` 的横向渐变,**只用色相区分窗口**;
 * 格子本身透明,浮在这张面上。列与列之间没有缝,整片读起来是一条"由烫到冷"的轴。
 *
 * ⛔ **数量绝不参与配色**(这条纪律一直没变,1b 也遵守):
 *   渐变只由**列的位置**决定 —— 否则「这格暖是因为在黄金期、还是因为人多」分不清。
 *   人数已经用数字表达了,不需要颜色再说一遍。
 * ⚠️ 色相承载的是**窗口档位**,与列头文字是同一个信息的两种编码 —— 这是刻意的冗余
 *   (色盲用户靠文字,扫视靠颜色),不是重复。
 *
 * ⚠️ 用 `/nn` 透明度而不是设计稿里的 `color-mix(in oklch, …, white)`:
 *   两者在白底卡片上视觉等价(Tailwind v4 的透明度修饰符本身就编译成 color-mix),
 *   而透明度写法**不写死任何十六进制**,符合 globals.css 那条「别在组件里写死色值」。
 */

/// 列序走共享的 TEMPERATURE_ORDER,中文走共享的 TEMPERATURE_META —— ⛔ 别在这里内联中文表
const COLUMNS = TEMPERATURE_ORDER;
type TempKey = TemperatureValue;

/**
 * 每档的**色相**。⚠️ 只用于「这一列是哪个窗口」的标识(列头圆点 + 汇总数字),
 * 格子的底色来自下方那张整片渐变,不是从这里取的 —— 两处若各上各的色就会对不齐。
 */
const HUE: Record<TempKey, { dot: string; text: string }> = {
  hot: { dot: 'bg-amber-600', text: 'text-amber-700' },
  warm: { dot: 'bg-emerald-600', text: 'text-emerald-700' },
  // ⭐ 冷端四档走**同一色相(sky)的深浅**,不换色相:
  //   换色相会读成"四种不同的东西",而它们是同一件事(已出临床周期)的**程度**差异。
  //   深→浅 = 近→远,与左边 amber→emerald→sky 的"由烫到冷"方向一致。
  cold_1y: { dot: 'bg-sky-600', text: 'text-sky-700' },
  cold_2y: { dot: 'bg-sky-500', text: 'text-sky-600' },
  cold_3y: { dot: 'bg-sky-400', text: 'text-sky-500' },
  cold_over: { dot: 'bg-slate-400', text: 'text-slate-500' },
};

/**
 * 整片矩阵那条渐变 —— 一处定义,列头刻度与格子面共用;写两遍必然漂。
 *
 * 🔴 2026-08-07 **收尾从 slate 改回 sky**(产品对着稿子指出来的)。
 *   在此之前:注释和上面的 HUE 都说冷端是 **sky**,而这条渐变实际收在 `slate-400/25` ——
 *   **面和点对不上**:列头三个圆点是蓝的,它们底下那片是灰的。
 *   整条轴于是读成"由烫到**灰**",而设计意图是"由烫到**冷**"——冷是蓝,灰是没有信息。
 * ⚠️ 冷端占了 4/6 的宽度,`via-45%` 把 emerald 的停靠点往左压,
 *   让 emerald→sky 的过渡铺满右半边,否则右侧一大片糊成一个颜色、分不出四档。
 * ⚠️ 最后一档(3 年以上)的**灰**只留在列头圆点上(HUE.cold_over):
 *   那是"这一列是谁"的标识,而这张面是一条连续的轴,不需要把最后一格单独编码。
 * 🔴 2026-08-07 **整体调浅**(同一次走查):原来走 `-600` 档 + 高透明度(`amber-600/55`),
 *   出来是"实色蒙了一层白" —— 面很浓,格子里的数字要跟它抢对比度。
 *   改走 **`-300` 档 + 三成左右**:面退成奶油/薄荷/淡天蓝的底,数字才是主角。
 *   ⚠️ 这三档的取值是按产品认可的稿子反算的(#F3C969 / #7FD1B9 / #8FC5E8 各 28% 叠白),
 *   ⛔ 别再往回调深 —— 面是背景不是内容,它只负责"由烫到冷"这一个信息。
 * ⚠️ 列头圆点仍走 `-600/-500/-400`(见上面 HUE):**小圆点需要饱和度才看得见**,
 *   与这张大面积的底是两种用途,⛔ 不要为了"一致"把两边调成同一档。
 * ⚠️ 仍用 `/nn` 透明度而不是十六进制 —— 见文件头注释那条「别在组件里写死色值」。
 */
const PLANE =
  'bg-linear-to-r from-amber-300/35 via-emerald-300/30 via-45% to-sky-300/32';

/// 行标签列宽 + 行高:列头、标签列、格子面三者靠它们对齐,别在某一处手改
const LABEL_W = 'w-[46px]';
const ROW_H = 'h-[30px]';

export function PoolMatrix({
  data,
  selected,
  onPick,
  /**
   * 外层容器样式覆盖。
   * ⚠️ 默认 `w-[560px]` 是给**左栏那个 popover** 的（浮层要贴着按钮，不能撑满屏）；
   *   主管工作台的 modal 要它填满，传 `w-full` 覆盖。
   * ⛔ 别把默认宽度删掉改成永远 w-full —— popover 会跟着变成整屏宽。
   */
  className,
}: {
  /**
   * 矩阵数据 —— **由调用方取好再传进来**,本组件不自己发请求。
   *
   * ⭐ 这样「加载中」才能落在**触发按钮**上(shadcn 常规做法:钮里转圈 + 禁用),
   *   而不是先弹一个空浮层再在里面转圈 —— 后者浮层会先以骨架尺寸出现、
   *   数据到了再重排,浮层的抖动比列表刺眼得多。
   */
  data: PoolMatrixData;
  /** 当前选中的格子(回显用) */
  selected?: { treatment: string; temperature: TempKey } | null;
  className?: string;
  /**
   * 点格子 → 初选完成,把这一格的人群交出去。
   * ⚠️ 带 `rect`(该格子的视口矩形):数据流动画的**起点**。
   *   必须在点击那一刻取 —— 浮层紧接着就关了,回头再查只能拿到 null。
   */
  onPick: (cell: {
    treatment: string;
    treatmentZh: string;
    temperature: TempKey;
    count: number;
    rect: { x: number; y: number; w: number; h: number };
  }) => void;
}) {
  const showUnknown = data.unknownTotal > 0;
  /**
   * ⭐ **正指着哪一格** —— 悬停的行 × 列坐标（2026-08-19 产品要「瞄靶」时一起加的）。
   *
   * 🔴 为什么要提到这一层、⛔ 不留在 CSS 的 `:hover` 里:
   *   `:hover` 只够点亮**格子自己**,而这张表 8 行 × 6 列、格子离行头和列头都很远 ——
   *   人指着一格时真正要确认的是「这是**哪个治疗** × **哪个时间档**」,
   *   那两个字在表的最左和最上。⇒ 坐标必须能传给行标签和列头,那就得是 state。
   * ⚠️ 只在**有人可分**的格子上设(`n > 0`):0 格点不动,给它十字等于指了个空。
   * ⚠️ 整片 `onMouseLeave` 兜底清空 —— ⛔ 别只靠每个格子的 leave:
   *   鼠标从格子直接飞出容器时,leave 有概率被下一个 enter 抢在前面,十字就留在原地不走了。
   */
  const [aim, setAim] = useState<{ treatment: string; temperature: TempKey } | null>(null);
  return (
    <Card className={cn('w-[560px] border-0 shadow-none', className)}>
      {/*
        ⛔ **不放列合计**。列合计 = 该列 8 行相加,而**一个人可以同时出现在多行**
        (有几个潜在治疗就占几行,实测人均 1.37 个)。于是六列一加会**大于**池子总人数
        —— 主管一对数就觉得系统在骗他(实测 2,526 vs 池子 2,129)。
        他真正要的是「点哪一格拿多少人」,那是格子本身的数,不是列合计。
      */}
      {/* ⚠️ 整个 CardHeader 2026-08-11 撤掉(产品指着截图说这行去掉)：
          原来左边是「潜在治疗」(行轴标题)、右边是「点一格 = 把这批人交给助手」。
          后者已经写在「分一批新的」面板标题旁边；前者在常驻面板里是**纯占高**——
          行头本来就是治疗项名(种植/正畸/…)，再加一行标题等于把矩阵往下推 30px。
          ⛔ 别为了"结构完整"加回来：这块现在唯一的消费方是那个面板，标题它出。 */}
      <CardContent className="p-3">
        {/* 列头:圆点 + 档位名,颜色与下方渐变面的对应段同族 */}
        <div className="flex items-center pb-1.5">
          <div className={cn(LABEL_W, 'shrink-0')} />
          <div className="flex flex-1">
            {COLUMNS.map((c) => {
              const onAxis = aim?.temperature === c;
              return (
                <div
                  key={c}
                  title={TEMPERATURE_META[c].hint}
                  className={cn(
                    // ⚠️ whitespace-nowrap:「3 年以上」会折成两行,把整排列头撑高、基线也歪
                    'flex flex-1 items-center justify-center gap-1 whitespace-nowrap text-[11px] font-semibold',
                    'transition-opacity duration-150',
                    HUE[c].text,
                    // ⭐ 命中那一列**保持原样**,其余**退让** —— ⛔ 不是给命中的那列加亮:
                    //   加亮会让六个列头在悬停时颜色各不相同(色相本身是信息,见 HUE 那段);
                    //   压暗其余的既不动色相,又让"我在这一列"一眼看出来。
                    aim && !onAxis && 'opacity-35',
                  )}
                >
                  <span
                    className={cn(
                      'rounded-sm transition-all duration-150',
                      HUE[c].dot,
                      // 命中列的圆点长大一圈 —— 它是这条轴的"刻度指针"
                      onAxis ? 'h-2 w-2' : 'h-1.5 w-1.5',
                    )}
                  />
                  {TEMPERATURE_META[c].zh}
                </div>
              );
            })}
          </div>
          {showUnknown && (
            <div className="w-12 shrink-0 text-center text-[11px] whitespace-nowrap text-muted-foreground">算不出</div>
          )}
        </div>

        <div className="flex items-start">
          {/* 行标签独立成列 —— 它不该压在渐变面上(压上去白底文字会跟着色相变脏) */}
          <div className={cn(LABEL_W, 'shrink-0')}>
            {data.rows.map((row) => {
              const onAxis = aim?.treatment === row.key;
              return (
                <div
                  key={row.key}
                  className={cn(
                    ROW_H,
                    'flex items-center text-xs font-medium transition-all duration-150',
                    // ⚠️ 命中那一行**往右挪 2px + 上主色**,其余压暗 —— 位移比变色更早被眼睛捕捉到,
                    //   而这一列字很短(种植/早矫/拔牙),只变色容易被数字那片抢掉。
                    onAxis && 'translate-x-0.5 text-brand-700',
                    aim && !onAxis && 'opacity-35',
                  )}
                >
                  {potentialTreatmentItemName(row.key)}
                </div>
              );
            })}
          </div>

          {/* ⭐ 1b 的主体:**一整张**渐变面,8×3 个格子透明浮在上面 */}
          <div
            className={cn('flex-1 overflow-hidden rounded', PLANE)}
            onMouseLeave={() => setAim(null)}
          >
            {data.rows.map((row) => (
              <MatrixRow
                key={row.key}
                row={row}
                selected={selected}
                onPick={onPick}
                aim={aim}
                onAim={setAim}
              />
            ))}
          </div>

          {showUnknown && (
            <div className="w-12 shrink-0">
              {data.rows.map((row) => (
                <div
                  key={row.key}
                  title="算不出档位 —— 这些人没有末诊记录"
                  className={cn(
                    ROW_H,
                    'flex items-center justify-center text-[11px] text-muted-foreground nums',
                  )}
                >
                  {row.unknown || '—'}
                </div>
              ))}
            </div>
          )}
        </div>

        {/*
          🔴 「每一列 = 患者最后一次到诊距今多久」**搬到面板抬头去了**(2026-08-19 产品:
            「不用分那么散」)—— 见 `new-batch.tsx` 的抬头行,那儿已经有一句「点一格 = …」,
            两句同为"这一屏怎么读"的说明,分在表格上下两头看着散。
          ⛔ 别在这里再写回一句:同一件事写第二份,两边会各自漂。
          ⚠️ 这句话本身**不许消失**(沿革与理由见 `TEMPERATURE_AXIS_ZH` 上那段):
            列头写的是「三个月内」,而"什么的三个月"决定了它是哪批人;
            「患者」两个字尤其不能丢 —— 末诊是**患者级**的,同一个人的几个潜在治疗
            必然落同一列,不说清楚主管会以为"这人三个机会都刚诊断"。
        */}

        {data.note && (
          <p className="mt-1.5 rounded bg-amber-50 px-2 py-1.5 text-[10.5px] leading-relaxed text-amber-800">
            {data.note}
          </p>
        )}
      </CardContent>
    </Card>
  );
}

function MatrixRow({
  row,
  selected,
  onPick,
  aim,
  onAim,
}: {
  row: PoolMatrixRow;
  selected?: { treatment: string; temperature: TempKey } | null;
  onPick: (cell: {
    treatment: string;
    treatmentZh: string;
    temperature: TempKey;
    count: number;
    rect: { x: number; y: number; w: number; h: number };
  }) => void;
  /** 正指着哪一格(见 PoolMatrix 里那段) */
  aim: { treatment: string; temperature: TempKey } | null;
  onAim: (a: { treatment: string; temperature: TempKey } | null) => void;
}) {
  // ⛔ 中文一律查 labels.ts,别在组件里另写一张表 —— 改措辞要即时生效、且全站一处。
  //   用**项目名**(种植 / 早矫 / 拔牙)而不是卡片措辞(种植治疗):
  //   8×3 的表里每行都拖着「治疗」两个字纯属噪声,还挤掉列宽。
  const zh = potentialTreatmentItemName(row.key);
  return (
    <div className="flex">
      {COLUMNS.map((c) => {
        const n = row.counts[c] ?? 0;
        const isSelected = selected?.treatment === row.key && selected.temperature === c;
        // 十字:同一行 或 同一列(交点就是准星那一格)
        const onCross = aim != null && (aim.treatment === row.key || aim.temperature === c);
        const isAimed = aim?.treatment === row.key && aim.temperature === c;
        return (
          <button
            key={c}
            type="button"
            disabled={n === 0}
            // ⚠️ 只有**能点**的格子才设瞄点:0 格点不动,给它十字等于指了个空
            onMouseEnter={() => n > 0 && onAim({ treatment: row.key, temperature: c })}
            onClick={(e) => {
              const r = e.currentTarget.getBoundingClientRect();
              onPick({
                treatment: row.key,
                treatmentZh: zh,
                temperature: c,
                count: n,
                rect: { x: r.left, y: r.top, w: r.width, h: r.height },
              });
            }}
            // ⭐ hover 文案保持原样:只给**天数区间**,不给解释。
            //   格子里已经有数字、列头已经写了档位名,再叠一段说明就是噪声;
            //   区间是主管唯一看不出来的那个信息。
            title={temperatureWindowHint(row.key, c)}
            className={cn(
              ROW_H,
              'flex-1 text-[13px] text-slate-900 nums',
              // ⭐ hover/选中用**内描边**而不是换底色 —— 换底色会在渐变面上戳一个洞,
              //   整片"连续"的观感当场破掉。描边只加一圈框,底下的渐变原样透出来。
              // ⚠️ 描边用**品牌主色**不是中性黑:这一圈框表达的是"你正指着谁 / 选中了谁",
              //   属于交互态,交互态在全站都该是主色(黑框会读成边界线,像是数据的一部分)。
              // ⚠️ 用 outline 而不是 shadow-[inset_…_var(--color-…)]:后者 Tailwind v4
              //   会把带 var() 的任意值解析成"只有颜色的阴影",偏移和扩散全丢,
              //   实测算出来是 `oklab(0 0 0 / 0) 0 0 0 0 inset` —— 完全透明,**看不出坏了**。
              //   负 offset 让描边画在格子内侧,与相邻格子不互相挤压(设计稿也是这么写的)。
              'transition-[background-color,opacity,transform] duration-150',
              n === 0
                ? 'cursor-default text-slate-900/35'
              //   ⚠️ 必须带 `outline-solid`:基态的 `outline-none` 把 outline-style 钉成 none,
              //     只加 `outline-2` 只改宽度 —— 实测算出来是 width:2px 但 style:none,**一条线都不画**。
              //   🔴 hover 那一圈**换成角括号准星**(`.pac-reticle`,globals.css)——
              //     实线框留给「已经选中」,两个状态长得一样就分不出"正指着"和"已选了"。
              //     focus-visible **仍走实线 outline**:键盘可达性⛔ 不能只靠一段动画。
                : 'cursor-pointer font-medium outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-600',
              /**
               * ⭐ 十字那层**薄纱**。
               * ⚠️ 与文件头「hover ⛔ 别换底色,会在渐变面上戳一个洞」⛔ 不冲突:
               *   那条管的是**一个格子**单独换底 —— 一个亮块浮在连续面上确实是个洞。
               *   而这里是**一整行 + 一整列**,读出来是一个十字,不是一个洞;
               *   且它是**瞬时交互态**,鼠标一走就没,⛔ 从不由数量驱动(那条纪律没动)。
               * ⚠️ 白纱 8% 是上限:再浓就把底下"由烫到冷"那条轴洗白了。
               */
              onCross && !isAimed && 'bg-white/8',
              /**
               * 准星那一格。
               * ⚠️ `pac-reticle` **由 state 挂,⛔ 不写成 CSS 的 `:hover::after`**:
               *   ① 0 格是 `disabled`,而 disabled 元素的 hover 行为跨浏览器不一致 ——
               *      走 state 才能保证"点不动的格子不给准星";
               *   ② 准星和十字必须**同生同灭**,同一个来源才不会一个亮一个不亮。
               * ⚠️ 数字**微微抬起** —— `nums` 是等宽数字,放大不会把邻居推歪。
               *   🔴 放大挂在**里面那个 span** 上,⛔ 不挂在 button 上(2026-08-20 修):
               *     渐变面为了圆角带 `overflow-hidden`,button 一 `scale(1.06)`,
               *     **最边上那一列/那一行**的盒子就顶出容器约 3px —— 准星的竖臂
               *     (`inset:2px` 处、宽 1.5px)整条落在容器外被裁光,横臂只剩内侧一截。
               *     现场是「3 年以上」那列:左两角完好,右两角只剩两个短横杠。
               *   ⛔ 别改成去掉 `overflow-hidden`:它是用来把**行/列高亮的白纱**裁进圆角的,
               *     去掉之后首行末行的纱会把四个圆角戳成方的。
               */
              isAimed && 'pac-reticle bg-white/12',
              // 其余整片退让 —— ⚠️ 只压**不在十字上**的,压全部等于什么都没压
              aim && !onCross && 'opacity-45',
              isSelected && 'outline-solid outline-2 -outline-offset-2 outline-brand-600',
            )}
          >
            <span
              className={cn(
                'inline-block transition-transform duration-150',
                isAimed && 'scale-[1.06]',
              )}
            >
              {n ? n.toLocaleString() : '—'}
            </span>
          </button>
        );
      })}
    </div>
  );
}
