import { Prisma } from '@prisma/client';
import {
  COLD_BUCKET_YEARS,
  COLD_TEMPERATURES,
  DiagnosisTreatmentMap,
  POTENTIAL_LABEL_RULES,
  Temperature,
  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`;
}

/**
 * 窗口配置表(code, urgencyDays, windowDays)—— 从 `DiagnosisTreatmentMap` 生成的 VALUES。
 * 只含 8 类标签实际用到的组主码;⛔ 别手写数字。
 */
export function windowValuesSql(): Prisma.Sql {
  const codes = [...new Set(POTENTIAL_LABEL_RULES.map((r) => r.code))];
  const rows = codes
    .map((c) => ({ c, rule: (DiagnosisTreatmentMap as Record<string, { urgencyDayThreshold: number; windowDays: number }>)[c] }))
    .filter((x) => !!x.rule)
    .map((x) => Prisma.sql`(${x.c}, ${x.rule!.urgencyDayThreshold}, ${x.rule!.windowDays})`);
  return Prisma.sql`(VALUES ${Prisma.join(rows, ', ')}) AS w(code, urg, wnd)`;
}

/**
 * 六档判定的 CASE。
 *
 * ⭐ **一个锚点走到底**:`anchor` = 该标签最新的未治疗诊断。
 *   前两档按该治疗项自己的临床窗口、后四档按绝对年数,量的都是「距同一个锚点多久」。
 * ⚠️ 冷端用 `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): Prisma.Sql {
  return Prisma.sql`
    SELECT fp.id AS plan_id, fp.patient_id, lab.lbl AS label,
           max(anc.at + (w.urg || ' days')::interval) AS hot_until,
           max(anc.at + (w.wnd || ' days')::interval) AS warm_until,
           max(anc.at)                                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
    JOIN ${windowValuesSql()} ON w.code = f.content->>'code'
    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[],
): 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'
      CROSS JOIN LATERAL (SELECT COALESCE(f.occurred_at, f.planned_for) AS at) anc
      JOIN ${windowValuesSql()} ON w.code = f.content->>'code'
      WHERE pr.plan_id = fp.id
        AND ${labelCaseSql('f', AGE_YEARS_SQL_P)} = ${label}
        AND anc.at IS NOT NULL
      HAVING ${temperatureBucketCaseSql(
        Prisma.raw("max(anc.at + (w.urg || ' days')::interval)"),
        Prisma.raw("max(anc.at + (w.wnd || ' days')::interval)"),
        Prisma.raw('max(anc.at)'),
      )} IN (${Prisma.join(
        buckets.map((t) => Prisma.sql`${t}`),
        ', ',
      )})
    )`;
}

