'use client';

import {
  TEMPERATURE_AXIS_ZH,
  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;
  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) => (
              <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',
                  HUE[c].text,
                )}
              >
                <span className={cn('h-1.5 w-1.5 rounded-sm', HUE[c].dot)} />
                {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) => (
              <div key={row.key} className={cn(ROW_H, 'flex items-center text-xs font-medium')}>
                {potentialTreatmentItemName(row.key)}
              </div>
            ))}
          </div>

          {/* ⭐ 1b 的主体:**一整张**渐变面,8×3 个格子透明浮在上面 */}
          <div className={cn('flex-1 overflow-hidden rounded', PLANE)}>
            {data.rows.map((row) => (
              <MatrixRow key={row.key} row={row} selected={selected} onPick={onPick} />
            ))}
          </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-11 大幅缩短:原来这里要解释"前两档按该治疗自己的周期、后四档按年数"——
            六档改成同一把尺子之后,那半句话没有了,⛔ 别再写回去。
          ⭐ 剩下那半句是**必须留的**:末诊是**患者级**的,同一个人的几个潜在治疗必然落同一档。
            不说清楚,主管会以为"这人三个机会都刚诊断"。⛔ 别省。
        */}
        <p className="mt-2 text-[10.5px] leading-relaxed text-muted-foreground">
          按<span className="font-medium">{TEMPERATURE_AXIS_ZH}距今多久</span>分档。
          末诊按人算，同一个人的几个治疗项都在同一档。
        </p>

        {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,
}: {
  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;
}) {
  // ⛔ 中文一律查 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;
        return (
          <button
            key={c}
            type="button"
            disabled={n === 0}
            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 让描边画在格子内侧,与相邻格子不互相挤压(设计稿也是这么写的)。
              n === 0
                ? 'cursor-default text-slate-900/35'
              //   ⚠️ 必须带 `outline-solid`:基态的 `outline-none` 把 outline-style 钉成 none,
              //     只加 `outline-2` 只改宽度 —— 实测算出来是 width:2px 但 style:none,**一条线都不画**。
                : 'cursor-pointer font-medium outline-none hover:outline-solid hover:outline-2 hover:-outline-offset-2 hover:outline-brand-600 focus-visible:outline-solid focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-600',
              isSelected && 'outline-solid outline-2 -outline-offset-2 outline-brand-600',
            )}
          >
            {n ? n.toLocaleString() : '—'}
          </button>
        );
      })}
    </div>
  );
}
