import { Prisma } from '@prisma/client';
import {
  AnchorMode,
  COLD_BUCKET_YEARS,
  COLD_TEMPERATURES,
  HOT_BUCKET_DAYS,
  POTENTIAL_LABEL_RULES,
  Temperature,
  WARM_BUCKET_DAYS,
  type AnchorModeValue,
  type TemperatureValue,
} from '@pac/types';

/**
 * 初选两根轴的**取数口径** —— 从召回单自己的证据算,不经过画像。
 *
 * ═══ 为什么换源(2026-08)═════════════════════════════════════════════
 * 原来 X 轴读画像 `potential_treatment.types`、Y 轴读画像里存好的 `hotUntil/warmUntil`。
 * 问题:画像回答的是「**这个人有哪些潜在治疗**」,而矩阵是**召回池的视图**,
 * 该回答「**引擎为什么要召回他**」。两者不是一回事 ——
 *   实测:161 个「患者×标签」格位在画像里有、但召回单根本没为这个标签立 reason。
 *   主管点「补牤」那一格捞到这批人,客服打开一看 plan 讲的是缺牙 —— 格子和人对不上。
 *
 * ✅ 换成:两根轴都从 `plan_reasons.evidence.factIds → patient_facts` 取。
 *   实测 100% 的在跑单都能追到带锚点和诊断码的事实,不存在取不到的情况。
 *
 * ═══ 顺带解决的 ═══════════════════════════════════════════════════
 * ⭐ **改窗口配置不再需要重算画像**。边界从锚点**读时**算,`DiagnosisTreatmentMap`
 *   一改立刻生效。原来边界烤在 44 万行 JSON 里,调一次窗口要跑 4.9 小时全量重算,
 *   而且不重算**不会报错**,只是全按旧窗口判。
 *
 * ⚠️ 本文件的 SQL 全部由 `@pac/types` 的常量**生成**,⛔ 一个数字都不许手抄:
 *   `POTENTIAL_LABEL_RULES`(码→标签,含年龄闸/关键词)、`DiagnosisTreatmentMap`(窗口天数)、
 *   `COLD_BUCKET_YEARS`(冷端年数)。手抄一份 = 两份真理源,分叉了也不报错。
 */

/** 患者周岁 —— 与 `ageYearsAt` 同语义(Postgres 的 age() 是生日感知的完整年数) */
export const AGE_YEARS_SQL = Prisma.sql`date_part('year', age(NOW(), p.birth_date))::int`;
/** 同上,给**外层别名是 `p`** 的关联片段用(两处别名一致,留两个常量只为可读) */
export const AGE_YEARS_SQL_P = AGE_YEARS_SQL;

/**
 * 事实 → 业务标签的 CASE,**从 POTENTIAL_LABEL_RULES 生成**。
 *
 * @param factAlias 事实表别名(需有 content jsonb)
 * @param ageSql    患者周岁表达式
 *
 * ⚠️ 规则表是**顺序敏感**的(K03 含关键词→拔牙 必须在 K03→修复 之前),
 *   CASE 的 WHEN 天然按顺序求值,与 TS 侧 `classifyCodeToLabel` 的 for-return 一致。
 * ⚠️ 年龄为 NULL 时,带年龄条件的 WHEN 求值为 NULL(非真)→ 不命中 → 落 NULL。
 *   这与 TS 侧「无生日一律不命中」一致,⛔ 别加 COALESCE 兜底。
 */
export function labelCaseSql(factAlias: string, ageSql: Prisma.Sql): Prisma.Sql {
  const code = Prisma.raw(`${factAlias}.content->>'code'`);
  const name = Prisma.raw(`COALESCE(${factAlias}.content->>'name_zh', '')`);
  const whens = POTENTIAL_LABEL_RULES.map((r) => {
    const conds: Prisma.Sql[] = [Prisma.sql`${code} = ${r.code}`];
    if (r.ageGt !== undefined) conds.push(Prisma.sql`${ageSql} > ${r.ageGt}`);
    if (r.ageMin !== undefined) conds.push(Prisma.sql`${ageSql} >= ${r.ageMin}`);
    if (r.ageMax !== undefined) conds.push(Prisma.sql`${ageSql} <= ${r.ageMax}`);
    if (r.nameAny?.length) {
      // 用 position() 逐词判包含 —— ⛔ 别用正则:关键词是业务配置,可能含正则元字符
      const anyOf = r.nameAny.map((k) => Prisma.sql`position(${k} in ${name}) > 0`);
      conds.push(Prisma.sql`(${Prisma.join(anyOf, ' OR ')})`);
    }
    return Prisma.sql`WHEN ${Prisma.join(conds, ' AND ')} THEN ${r.label}`;
  });
  return Prisma.sql`CASE ${Prisma.join(whens, ' ')} ELSE NULL END`;
}

/**
 * 前两档的边界表达式(锚点 + 固定天数)。
 *
 * 🔴 2026-08-11:天数从「该码自己的 `urgencyDayThreshold` / `windowDays`」改成**固定 90/180**
 *   (口径与理由见 `@pac/types` 的 temperature.ts 文件头)。原来这里有一张
 *   `windowValuesSql()` 生成的 `(code, urg, wnd)` VALUES 表并 JOIN 到事实的诊断码上,
 *   现在整张表连同那次 JOIN 一起删了 ——
 *   ⚠️ 删 JOIN **不改变行集**:它连的是 `POTENTIAL_LABEL_RULES` 那 8 个码,
 *     而这两个函数的 WHERE 里 `labelCaseSql(...)` 只认同一批码(K00/K09 落 NULL 已被排掉)。
 *     (`tests/potential-label-rules.spec.ts` 锁着这层等价。)
 * ⚠️ 仍然是**参数注入**,⛔ 不许把 90/180 拼进 SQL 文本 —— 常量在 `@pac/types`,
 *   这里只引用;`make_interval(days => $n)` 是为了避开 `int || text` 的隐式转换。
 * 🔴 `::int` **不能省**:Prisma 把 JS number 绑成 `double precision`,而
 *   `make_interval(days => double precision)` **这个函数不存在** → 整个 /plans/matrix 报 500
 *   (2026-08-11 实测,矩阵整片打不开)。⛔ 别顺手删掉这个 cast。
 */
/**
 * 锚点表达式 —— **两版口径**(见 @pac/types 的 `AnchorMode`)。
 *
 *   · diagnosis  `anc.at`            = 该标签最新的未治疗诊断(默认,历史行为)
 *   · last_visit `lvp.last_visit_at` = 患者末诊(**患者级**,同一人所有标签同值)
 *
 * ⚠️ 两版都套**同一个** `temperatureBucketCaseSql`(90/180 天 + 自然年)——
 *   换的只是"距哪个时刻",档位切分本身一个字都不动。
 * ⚠️ `last_visit_at` 取 `patient_profiles`(本地实测 5,825 人 100% 有值、100% 与
 *   encounter/emr 事实的最大 occurred_at 同日),⛔ 别为它再去 join 一遍 patient_facts。
 * ⚠️ 末诊为 NULL → 三个值全 NULL → 落 `unknown` 档(与"边界缺失"同路),
 *   ⛔ 不许默认塞进某一档。
 */
function anchorExpr(mode: AnchorModeValue): Prisma.Sql {
  return mode === AnchorMode.LAST_VISIT ? Prisma.sql`lvp.last_visit_at` : Prisma.sql`anc.at`;
}
function boundsSql(mode: AnchorModeValue) {
  const a = anchorExpr(mode);
  return {
    hot: Prisma.sql`max(${a} + make_interval(days => ${HOT_BUCKET_DAYS}::int))`,
    warm: Prisma.sql`max(${a} + make_interval(days => ${WARM_BUCKET_DAYS}::int))`,
    anchor: Prisma.sql`max(${a})`,
  };
}
/// 末诊口径要多一张表;诊断口径下**不 join**(省一次无谓的扫描)
function lastVisitJoin(mode: AnchorModeValue): Prisma.Sql {
  return mode === AnchorMode.LAST_VISIT
    ? Prisma.sql`LEFT JOIN patient_profiles lvp ON lvp.patient_id = fp.patient_id`
    : Prisma.empty;
}

/**
 * 六档判定的 CASE。
 *
 * ⭐ **一个锚点走到底**:`anchor` = 该标签最新的未治疗诊断。
 *   前两档按固定 90/180 天、后四档按绝对年数,量的都是「距同一个锚点多久」。
 * ⚠️ 冷端用 `anchor + interval 'N years'` 而不是 `N*365 天` —— 与 TS 侧 `setFullYear` 同语义,
 *   365 天算法在闰年会漂,两边就对不上数。
 * ⚠️ 锚点/边界为 NULL → 落 NULL(调用方显式呈现"未知"),⛔ 不许默认塞进某个冷档。
 */
export function temperatureBucketCaseSql(
  hotUntil: Prisma.Sql,
  warmUntil: Prisma.Sql,
  anchor: Prisma.Sql,
): Prisma.Sql {
  const colds = COLD_TEMPERATURES.filter((t) => COLD_BUCKET_YEARS[t] != null).map(
    (t) =>
      Prisma.sql`WHEN NOW() <= ${anchor} + ${Prisma.raw(`interval '${COLD_BUCKET_YEARS[t]} years'`)} THEN ${t}`,
  );
  return Prisma.sql`
    CASE
      WHEN ${hotUntil} IS NULL OR ${anchor} IS NULL THEN NULL
      WHEN NOW() <= ${hotUntil}  THEN ${Temperature.HOT}
      WHEN NOW() <= ${warmUntil} THEN ${Temperature.WARM}
      ${Prisma.join(colds, ' ')}
      ELSE ${Temperature.COLD_OVER}
    END`;
}

/**
 * 每个 (plan, 标签) 一行:两个边界 + 锚点。**独立查询**,给矩阵用。
 *
 * ⚠️ 别名固定 `fp`(followup_plans)/ `p`(patients)—— 因为 `planFilter` 传进来的是
 *   `poolBaseSql`,它引用的就是这两个别名。改名就得同步改那边,⛔ 别只改一处。
 * ⚠️ 关联子查询场景**不要用本函数**(内部的 `fp` 会遮蔽外层同名别名,
 *   关联条件变成恒真且 SQL 不报错)—— 那种场景用下方的 `labelExistsSql` / `labelTemperatureExistsSql`。
 *
 * ⚠️ `f.status = 'active'`:治完的诊断是 `fulfilled` 不是删除。拿它当锚 =
 *   对着一个已经做完的诊断说"您还没做"。当前数据 5,034 条证据全是 active,
 *   这条过滤是**不变量守卫** —— 治疗落库后引擎还没重算的窗口期里,它就是唯一防线。
 * ⚠️ 三个聚合都用 max(取最热 / 取最新)—— 见 temperature.ts:统一成"最早"会让
 *   hot/warm 反单调(多一条旧需求反而更冷)。
 */
export function planLabelAnchorsSql(
  planFilter: Prisma.Sql,
  mode: AnchorModeValue = AnchorMode.DIAGNOSIS,
): Prisma.Sql {
  const b = boundsSql(mode);
  return Prisma.sql`
    SELECT fp.id AS plan_id, fp.patient_id, lab.lbl AS label,
           ${b.hot}    AS hot_until,
           ${b.warm}   AS warm_until,
           ${b.anchor} AS anchor_at
    FROM followup_plans fp
    JOIN patients p ON p.id = fp.patient_id
    JOIN plan_reasons pr ON pr.plan_id = fp.id
    CROSS JOIN LATERAL jsonb_array_elements_text(pr.evidence->'factIds') fid
    JOIN patient_facts f ON f.id = fid::uuid AND f.status = 'active'
    CROSS JOIN LATERAL (SELECT COALESCE(f.occurred_at, f.planned_for) AS at) anc
    CROSS JOIN LATERAL (SELECT ${labelCaseSql('f', AGE_YEARS_SQL)} AS lbl) lab
    ${lastVisitJoin(mode)}
    WHERE ${planFilter}
      AND lab.lbl IS NOT NULL
      AND anc.at IS NOT NULL
    GROUP BY 1, 2, 3`;
}

/**
 * 关联片段:外层 `fp`(followup_plans)/ `p`(patients)必须在作用域内。
 *
 * ⭐ **不重新 SELECT followup_plans** —— 直接从 plan_reasons 出发关联 `pr.plan_id = fp.id`,
 *   这样内部没有 `fp` 别名,不会遮蔽外层(那是上面那个函数不能用在这里的原因)。
 */
export function labelExistsSql(label: string): Prisma.Sql {
  return Prisma.sql`
    AND EXISTS (
      SELECT 1
      FROM plan_reasons pr
      CROSS JOIN LATERAL jsonb_array_elements_text(pr.evidence->'factIds') fid
      JOIN patient_facts f ON f.id = fid::uuid AND f.status = 'active'
      WHERE pr.plan_id = fp.id
        AND ${labelCaseSql('f', AGE_YEARS_SQL_P)} = ${label}
    )`;
}

/**
 * 关联片段:该 plan 在 `label` 这一格上的温度落在 `buckets` 之内。
 *
 * ⚠️ 必须先聚合再判档 —— 温度是「该标签所有未治疗证据**取最热**」后的结果,
 *   逐条判再 OR 会把"有一条还热"错读成整格热(反过来也一样)。所以走 GROUP BY + HAVING。
 */
export function labelTemperatureExistsSql(
  label: string,
  buckets: readonly TemperatureValue[],
  /**
   * 🔴 **必须与矩阵那次查询用同一个 mode**。这里是**圈人**侧,矩阵是**看**侧 ——
   *   两边 mode 不一致 = 主管看到 87 人、确认单捞出另一批,而且全程不报错(T14)。
   *   调用方(`cohort-filter`)从 `criteria.anchorMode` 取,⛔ 别在这里给"聪明的默认"。
   */
  mode: AnchorModeValue = AnchorMode.DIAGNOSIS,
): Prisma.Sql {
  const b = boundsSql(mode);
  return Prisma.sql`
    AND EXISTS (
      SELECT 1
      FROM plan_reasons pr
      CROSS JOIN LATERAL jsonb_array_elements_text(pr.evidence->'factIds') fid
      JOIN patient_facts f ON f.id = fid::uuid AND f.status = 'active'
      CROSS JOIN LATERAL (SELECT COALESCE(f.occurred_at, f.planned_for) AS at) anc
      ${lastVisitJoin(mode)}
      WHERE pr.plan_id = fp.id
        AND ${labelCaseSql('f', AGE_YEARS_SQL_P)} = ${label}
        AND anc.at IS NOT NULL
      HAVING ${temperatureBucketCaseSql(b.hot, b.warm, b.anchor)} IN (${Prisma.join(
        buckets.map((t) => Prisma.sql`${t}`),
        ', ',
      )})
    )`;
}

