import { Prisma } from '@prisma/client';
import {
  EXTERNAL_TREATMENT_VISIT_POS_RE,
  EXTERNAL_TREATMENT_VISIT_NEG_RE,
  NO_RESTORATION_GAP_EXAM_PATTERNS,
  TREATMENT_REFUSAL_SUBTYPE_PATTERNS,
  RESTORATION_INELIGIBLE_DX_NAMES,
  STRUCTURAL_DX_CODE_LIST,
  type DxTreatmentRule,
} from '@pac/types';

/**
 * 潜在治疗 gap 核心 SQL —— 单一真理源(Layer 1 facts 消费层,中立)
 *
 * "诊断了/建议了某治疗,但没启动对应治疗" 的牙位级 gap 计算。**两个消费方共用**:
 *   - Layer 3 召回(treatment-initiation-recall.scenario):gap + 时间门 + 6 因子打分 + 建 plan
 *   - Layer 2 画像(potential-treatment.feature):gap + 年龄/name 拆分 → 8 业务标签
 * 都 import 本模块,口径 byte 级一致(圈人群 = 召回候选)。
 *
 * ⚠️ 本文件 = 原 scenario 里 toothArrSql / resolvedTeethSql / lateral / gapWhere 的【原样搬家】,
 *    SQL 逻辑零改动(W7 抽取,verify-recall.sql 验 FP/FN 不变)。改 gap 口径只改这一处。
 *
 * 别名契约:消费方主查询必须用 `p`(patients)/ `sig`(触发 fact)别名,本模块片段按此关联。
 */

export interface GapCfgFlags {
  excludeDeciduous?: boolean; // 乳牙不进种植/修复(FDI 51-85 + 象限 1A-4E)
  excludeThirdMolar?: boolean; // §E 智齿位 18/28/38/48 不召种植
  excludeOrthoExtractionSites?: boolean; // §E 正畸减数位(外科拔除 + 正畸语境)折进 resolved
  excludeCongenitalName?: boolean; // §E name 含"先天" → 正畸统筹,不自动召修复
  deferToToothDx?: boolean; // 「建议拔除」场景专用:有同牙编码病种诊断 → 让位给该病种场景(避免双召)
}

/// §E gap 修正 flag 的单一真理源(按 primaryCode 查)。召回 SUB_SCENARIOS 与画像标签都引此。
/// 仅 K08(缺失牙→种植)需要:影像AI/name_map 常把废用牙/智齿/正畸减数位/先天缺失误当缺牙。
export const GAP_FLAGS_BY_PRIMARY: Record<string, GapCfgFlags> = {
  K08: {
    excludeDeciduous: true,
    excludeThirdMolar: true,
    excludeOrthoExtractionSites: true,
    excludeCongenitalName: true,
  },
  // 「建议拔除」独立场景:有同牙编码病种诊断 → 让位给病种场景,不重复召(残根=K03、阻生=K01…)
  EXTRACTION_RECOMMENDED: {
    deferToToothDx: true,
  },
};

const RESTORATION_INELIGIBLE_NAMES = [...RESTORATION_INELIGIBLE_DX_NAMES];

/// 诊断码 ↔ 建议码分组(单一真理源)。召回 SUB_SCENARIOS 与潜在治疗画像/selector 都引此,
/// 保证"哪些 code 同属一个 gap"口径一致。建议码 = EMR/影像AI/客服 抽出的第二信号源。
export const GAP_PRIMARY_GROUPS: Record<string, { dxCodes: string[]; recCodes: string[] }> = {
  K08: { dxCodes: ['K08'], recCodes: ['IMPLANT_RECOMMENDED'] },
  K07: { dxCodes: ['K07'], recCodes: ['ORTHO_CONSULT_RECOMMENDED'] },
  K04: { dxCodes: ['K04'], recCodes: ['RCT_RECOMMENDED'] },
  K05: { dxCodes: ['K05'], recCodes: ['SRP_RECOMMENDED'] },
  K02: { dxCodes: ['K02'], recCodes: ['FILLING_RECOMMENDED'] },
  K03: { dxCodes: ['K03'], recCodes: ['HARD_TISSUE_REPAIR_RECOMMENDED', 'CROWN_RECOMMENDED'] },
  K06: { dxCodes: ['K06'], recCodes: ['GUM_TREATMENT_RECOMMENDED'] },
  K01: { dxCodes: ['K01'], recCodes: [] }, // 阻生牙只认真诊断(阻生/埋伏);「建议拔除」拆到下方独立场景
  K09: { dxCodes: ['K09'], recCodes: ['JAW_CYST_REMOVAL_RECOMMENDED'] },
  K00: { dxCodes: ['K00'], recCodes: ['ERUPTION_INTERVENTION_RECOMMENDED'] },
  // 「建议拔除」是跨病种的治疗动作(阻生/残根/松动牙/龋坏…都可能拔),不绑任何 K 病种 → 独立场景。
  // 显示「牙未拔除(医生建议)」,不冒充某病;有同牙编码诊断时让位给病种场景(deferToToothDx)。
  EXTRACTION_RECOMMENDED: { dxCodes: [], recCodes: ['EXTRACTION_RECOMMENDED'] },
};

/// 牙位字符串 → 牙位数组(只剥"牙位base+空格+牙面字母"后缀,保 FDI 数字 & Palmer 乳牙字母)。
/// 口径必须跟 tooth-position.util.ts 的 toothSet 严格一致。详见原 scenario 注释。
export function toothArrSql(
  expr: Prisma.Sql,
  opts: { dropDeciduous?: boolean; dropThirdMolar?: boolean } = {},
): Prisma.Sql {
  const deciduousFilter = opts.dropDeciduous
    ? Prisma.sql`AND x !~ '^[5-8][1-5]$' AND x !~ '^[1-4][A-Ea-e]$'`
    : Prisma.empty;
  // §E 智齿位 18/28/38/48(^[1-4]8$ = 恒牙第三磨牙;乳牙 51-85 第二位 1-5,不匹配,无误伤)
  const thirdMolarFilter = opts.dropThirdMolar ? Prisma.sql`AND x !~ '^[1-4]8$'` : Prisma.empty;
  return Prisma.sql`(SELECT COALESCE(array_agg(x), ARRAY[]::text[]) FROM unnest(array_remove(string_to_array(
    regexp_replace(
      regexp_replace(${expr}, '([0-9]+[A-Ea-e]?)[[:space:]]+[DMOBLPIdmoblpi]+', '\\1', 'g'),
      '[[:space:]]+', '', 'g'),
    ';'), '')) AS x WHERE x ~ '^[0-9].' ${deciduousFilter} ${thirdMolarFilter})`;
}

export interface GapCoreInput {
  rule: DxTreatmentRule;
  cfgFlags: GapCfgFlags;
  allCodes: readonly string[]; // dxCodes ∪ recCodes
  resolverCats: readonly string[]; // resolverCategoriesFor(primaryCode)
}

export interface GapCorePieces {
  /// 信号牙位表达式(全口码 = NULL)
  sigToothExpr: Prisma.Sql;
  /// 是否全口码(K05/K07)
  wholeMouthFlag: Prisma.Sql;
  /// ④' 废用牙/无功能牙剔除(WHERE add-on,作用于所有 sig;只 K08 有这些 name)
  restorationIneligibleFrag: Prisma.Sql;
  /// ④' §E 先天缺失剔除(WHERE add-on;仅 excludeCongenitalName 时非空)
  congenitalFrag: Prisma.Sql;
  /// LEFT JOIN LATERAL (...) lat ON true —— 算 sig_teeth / resolved_teeth / remaining_teeth
  lateralJoin: Prisma.Sql;
  /// tooth 输出 CASE(全口 → sigToothExpr;有牙位 → 剩余未治牙位字符串)
  toothOutput: Prisma.Sql;
  /// ⑤a gap 判定(WHERE add-on):全口 → NOT EXISTS 同类治疗;有牙位 → 剩余非空
  gapWhere: Prisma.Sql;
}

/**
 * 构造 gap 核心 SQL 片段(原样搬自 scenario,逻辑不变)。
 * 消费方按需把片段拼进自己的主查询(召回再加时间门 + 打分;画像加年龄 + 标签映射)。
 */
export function buildGapCore(input: GapCoreInput): GapCorePieces {
  const { rule, cfgFlags, allCodes, resolverCats } = input;

  const latestDxOfCode = Prisma.sql`(
    SELECT max(COALESCE(dd.occurred_at, dd.planned_for))
    FROM patient_facts dd
    WHERE dd.patient_id = p.id AND dd.status = 'active'
      AND dd.type IN ('diagnosis_record', 'recommendation_record')
      AND dd.content->>'code' = ANY(${allCodes}::text[])
  )`;
  const afterDxFrag = rule.excludeIfEverTreated
    ? Prisma.sql`AND tx.occurred_at >= ${latestDxOfCode}`
    : Prisma.sql`AND tx.occurred_at >= COALESCE(sig.occurred_at, sig.planned_for)`;

  const sigToothExpr = rule.wholeMouth
    ? Prisma.sql`NULL::text`
    : Prisma.sql`sig.content->>'tooth_position'`;
  const wholeMouthFlag = rule.wholeMouth ? Prisma.sql`TRUE` : Prisma.sql`FALSE`;

  /// 治疗时间方向片段(按表别名生成;rtx=治疗家族 / btx=桥区间 共用同一口径)
  const afterDxFor = (alias: string): Prisma.Sql =>
    rule.excludeIfEverTreated
      ? Prisma.sql`AND ${Prisma.raw(alias)}.occurred_at >= ${latestDxOfCode}`
      : Prisma.sql`AND ${Prisma.raw(alias)}.occurred_at >= COALESCE(sig.occurred_at, sig.planned_for)`;
  const afterDxFragRtx = afterDxFor('rtx');
  const noRestorMsgRe = NO_RESTORATION_GAP_EXAM_PATTERNS.join('|');
  const refusalRe = TREATMENT_REFUSAL_SUBTYPE_PATTERNS.join('|');
  /// 外院已治疗(回访记录 result)患者级排除:POS 紧邻命中 ∧ 非 NEG 意图 ∧ 回访日 ≥ 本次诊断 → 排除。
  ///   患者级(不看牙位/类目);时间方向同治疗排除(新诊断更晚 → 不排)。只读 patient_return_visits.result,
  ///   不影响已治疗事实。一线反馈②(李姝妤 回访"已在外院矫正中")。
  const externalTreatmentGate = Prisma.sql`AND NOT EXISTS (
        SELECT 1 FROM patient_return_visits rv
        WHERE rv.patient_id = p.id
          AND rv.result ~ ${Prisma.sql`${EXTERNAL_TREATMENT_VISIT_POS_RE}`}
          AND rv.result !~ ${Prisma.sql`${EXTERNAL_TREATMENT_VISIT_NEG_RE}`}
          AND rv.task_date >= COALESCE(sig.occurred_at, sig.planned_for)::date
      )`;
  /// 拒绝记录时间方向:planned 记录 occurred_at 为空 → 用 COALESCE(occurred_at, planned_for)
  const refusalAfterDx = rule.excludeIfEverTreated
    ? Prisma.sql`AND COALESCE(rfx.occurred_at, rfx.planned_for) >= ${latestDxOfCode}`
    : Prisma.sql`AND COALESCE(rfx.occurred_at, rfx.planned_for) >= COALESCE(sig.occurred_at, sig.planned_for)`;


  // §E (d) 正畸减数位(仅 missing_tooth):正畸为排齐做的【对称前磨牙减数】才剔 → 折进 resolved。
  //   2026-06 收紧(张学军案例):旧口径"任意 actual 外科拔牙 + 患者曾有任意正畸语境"过宽,
  //   把"种植/全口修复前的拔牙"误当减数位剔掉(24 缺牙不召;拔牙 2021 早于正畸诊断 2023、且拔了
  //   磨牙/相邻牙,根本不是减数)。新口径要求该拔牙记录呈【正畸减数模式】:
  //     ① 该牙是前磨牙(4/5 号);② 该记录"纯前磨牙"(混磨牙/切牙/尖牙 = 非减数,如全口修复拔牙);
  //     ③ 记录含对称第一前磨牙对(14&24 或 34&44 —— 减数的临床特征是成对拔前磨牙)。
  //   去掉时间窗(case-specific);用减数模式纯度判别,更通用、贴临床。
  const exTeeth = toothArrSql(Prisma.sql`ex.content->>'tooth_position'`);
  const orthoExtractBranch = cfgFlags.excludeOrthoExtractionSites
    ? Prisma.sql`
      UNION
      SELECT eet AS t
        FROM patient_facts ex
        CROSS JOIN unnest(${exTeeth}) AS eet
       WHERE ex.patient_id = p.id
         AND ex.type = 'treatment_record' AND ex.kind = 'actual' AND ex.status IN ('active','fulfilled')
         AND ex.content->>'category' = 'surgical'
         AND eet ~ '^[1-4][45]$'                                   -- ① 该牙是前磨牙(4/5 号)
         AND NOT EXISTS (                                           -- ② 该拔牙记录"纯前磨牙"(无磨牙/切牙/尖牙混入)
           SELECT 1 FROM unnest(${exTeeth}) AS xt WHERE xt !~ '^[1-4][45]$'
         )
         AND (${exTeeth} @> ARRAY['14','24']::text[]                -- ③ 含对称第一前磨牙对(上颌)
           OR ${exTeeth} @> ARRAY['34','44']::text[])               --     或(下颌)
         AND EXISTS (SELECT 1 FROM patient_facts oc WHERE oc.patient_id = p.id
           AND ((oc.type='diagnosis_record' AND oc.status='active' AND oc.content->>'code'='K07')
             OR (oc.type='treatment_record' AND oc.content->>'category'='orthodontic')))`
    : Prisma.empty;

  // (e) 「建议拔除」让位:同牙有【编码病种诊断】(K00-K04/K06/K08/K09 牙位级)→ 折进 resolved,
  //   该牙交给对应病种场景召回,建议拔除场景不重复召(残根→K03、阻生→K01…)。仅 deferToToothDx 启用。
  //   毕强 12;31;… 是松动牙(无 code)→ 不被折进 → 由建议拔除场景正常召回。
  const deferDxBranch = cfgFlags.deferToToothDx
    ? Prisma.sql`
      UNION
      SELECT ddt AS t
        FROM patient_facts dxx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`dxx.content->>'tooth_position'`)}) AS ddt
       WHERE dxx.patient_id = p.id
         AND dxx.type = 'diagnosis_record' AND dxx.status = 'active'
         AND dxx.content->>'code' = ANY(ARRAY['K00','K01','K02','K03','K04','K06','K08','K09']::text[])`
    : Prisma.empty;

  const congenitalFrag = cfgFlags.excludeCongenitalName
    ? Prisma.sql`AND COALESCE(sig.content->>'name_zh','') NOT LIKE '%先天%'`
    : Prisma.empty;

  const resolvedTeethSql = Prisma.sql`
    (SELECT COALESCE(array_agg(DISTINCT t), ARRAY[]::text[]) FROM (
      -- (a) 治疗家族 resolver(afterDx):同牙诊断后做了 resolverCats 家族里任一治疗
      SELECT rtt AS t
        FROM patient_facts rtx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`rtx.content->>'tooth_position'`)}) AS rtt
       WHERE rtx.patient_id = p.id
         AND rtx.type = 'treatment_record' AND rtx.kind = 'actual'
         AND rtx.status IN ('active', 'fulfilled')
         AND COALESCE(NULLIF(trim(rtx.content->>'tooth_position'), ''), '') != ''
         AND rtx.content->>'category' = ANY(${resolverCats}::text[])
         ${afterDxFragRtx}
      UNION
      -- (a'') 桥类修复的牙位盲点(2026-06-11 一线反馈④ 祁小夏:31;41 缺失由 32;33;42;43
      --   基牙固定桥修复,治疗记录牙位只写基牙 → 与缺失牙位不重叠 → 误召)。
      --   判定:prosthodontic actual 且【同一牙弓 ≥2 颗】→ 基牙跨度区间内全部牙位计入
      --   resolved(FDI 转弓位线性序:上颌 18..11|21..28,下颌 48..41|31..38)。
      --   不依赖 subtype 含"桥"(④ 实测桥的 treat 名是"冠修复(美学区)",桥字只在 EMR 文本):
      --   连续多冠(贴面/联冠)跨度=颗数 → 填洞为空操作,无副作用;跨度>颗数(夹着缺牙)
      --   即桥语义。已知取舍:缺牙两侧各做独立单冠且未做桥的罕见场景会被误销,接受。
      SELECT cov.arch_tooth AS t
        FROM patient_facts btx
        CROSS JOIN LATERAL (
          SELECT min(z.idx) AS mi, max(z.idx) AS ma, z.arch
          FROM (
            SELECT CASE substr(bt,1,1)
                     WHEN '1' THEN 9 - substr(bt,2,1)::int
                     WHEN '2' THEN 8 + substr(bt,2,1)::int
                     WHEN '4' THEN 9 - substr(bt,2,1)::int
                     WHEN '3' THEN 8 + substr(bt,2,1)::int END AS idx,
                   CASE WHEN substr(bt,1,1) IN ('1','2') THEN 'U' ELSE 'L' END AS arch
            FROM unnest(${toothArrSql(Prisma.sql`btx.content->>'tooth_position'`)}) AS bt
            WHERE bt ~ '^[1-4][1-8]$'
          ) z GROUP BY z.arch HAVING count(*) >= 2
        ) span
        CROSS JOIN LATERAL (
          SELECT CASE WHEN span.arch = 'U'
                   THEN CASE WHEN gi <= 8 THEN '1' || (9 - gi)::text ELSE '2' || (gi - 8)::text END
                   ELSE CASE WHEN gi <= 8 THEN '4' || (9 - gi)::text ELSE '3' || (gi - 8)::text END
                 END AS arch_tooth
          FROM generate_series(span.mi, span.ma) AS gi
        ) cov
       WHERE btx.patient_id = p.id
         AND btx.type = 'treatment_record' AND btx.kind = 'actual'
         AND btx.status IN ('active', 'fulfilled')
         AND btx.content->>'category' = 'prosthodontic'
         ${afterDxFor('btx')}
      UNION
      -- (a''') 检查所见"缺牙但间隙关闭/无修复间隙" → 该牙无修复指征,计入 resolved
      --   (一线反馈⑤ 王敏 26:诊断"牙列缺损"但检查"缺失,缺牙间隙已关闭"→ 无需修复)。
      --   读 emr_record.exam_findings(JSON 字符串数组),按牙位 message 匹配;
      --   限"缺失/缺牙"与"间隙关闭/无修复间隙"同段共现(排除正畸"关闭间隙"治疗语境噪音)。
      SELECT nrt AS t
        FROM (
          SELECT emrx.content->>'exam_findings' AS ef_text, emrx.occurred_at
          FROM patient_facts emrx
          WHERE emrx.patient_id = p.id
            AND emrx.type = 'emr_record' AND emrx.status IN ('active', 'fulfilled')
            AND emrx.content->>'exam_findings' ~ '^\\['
            AND emrx.content->>'exam_findings' ~ '缺[牙失]'
            AND emrx.content->>'exam_findings' ~ ${Prisma.sql`${noRestorMsgRe}`}
            ${afterDxFor('emrx')}
        ) src
        CROSS JOIN LATERAL jsonb_array_elements(src.ef_text::jsonb) AS ef
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`ef->>'toothPosition'`)}) AS nrt
       WHERE (ef->>'message') ~ '缺[牙失]'
         AND (ef->>'message') ~ ${Prisma.sql`${noRestorMsgRe}`}
      UNION
      -- (a'''') 患者无意愿:该 category 治疗被患者拒绝(treatment subtype 含无意愿/不愿/拒绝)
      --   → 对应牙位计入 resolved(对本次诊断;新诊断更晚 → 重新可召)。一线反馈⑧ 余奕铭。
      SELECT rft AS t
        FROM patient_facts rfx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`rfx.content->>'tooth_position'`)}) AS rft
       WHERE rfx.patient_id = p.id
         AND rfx.type = 'treatment_record' AND rfx.status IN ('active', 'fulfilled')
         AND rfx.content->>'subtype' ~ ${Prisma.sql`${refusalRe}`}
         AND rfx.content->>'category' = ANY(${resolverCats}::text[])
         ${refusalAfterDx}
      UNION
      -- (b) 同牙位以【最新诊断】为准:更晚的真实结构诊断 → 旧诊断对该牙失效
      SELECT ldt AS t
        FROM patient_facts ldx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`ldx.content->>'tooth_position'`)}) AS ldt
       WHERE ldx.patient_id = p.id
         AND ldx.type = 'diagnosis_record' AND ldx.status = 'active'
         AND ldx.content->>'code' = ANY(${[...STRUCTURAL_DX_CODE_LIST]}::text[])
         AND ldx.occurred_at > COALESCE(sig.occurred_at, sig.planned_for)
      UNION
      -- (c) 诊断 vs 建议冲突,以【建议】为准(医生治疗决定 > 诊断)
      SELECT rdt AS t
        FROM patient_facts rdx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`rdx.content->>'tooth_position'`)}) AS rdt
       WHERE rdx.patient_id = p.id
         AND rdx.type = 'recommendation_record' AND rdx.status = 'active'
         AND rdx.content->>'code' = ANY(${[...STRUCTURAL_DX_CODE_LIST]}::text[])
         AND COALESCE(rdx.occurred_at, rdx.planned_for) >= COALESCE(sig.occurred_at, sig.planned_for)
         AND sig.type = 'diagnosis_record'
      ${orthoExtractBranch}
      ${deferDxBranch}
    ) u)`;

  const lateralJoin = Prisma.sql`
    LEFT JOIN LATERAL (
      SELECT st AS sig_teeth, rt AS resolved_teeth,
        (SELECT COALESCE(array_agg(x), ARRAY[]::text[])
           FROM unnest(st) AS x WHERE x <> ALL(rt)) AS remaining_teeth
      FROM (
        SELECT COALESCE(${toothArrSql(sigToothExpr, {
          dropDeciduous: cfgFlags.excludeDeciduous === true,
          dropThirdMolar: cfgFlags.excludeThirdMolar === true,
        })}, ARRAY[]::text[]) AS st,
               ${resolvedTeethSql} AS rt
      ) base
    ) lat ON true`;

  const toothOutput = Prisma.sql`CASE WHEN ${wholeMouthFlag}
       THEN ${sigToothExpr}
       ELSE array_to_string(lat.remaining_teeth, ';') END`;

  const restorationIneligibleFrag = Prisma.sql`AND COALESCE(sig.content->>'name_zh', '') <> ALL(${RESTORATION_INELIGIBLE_NAMES}::text[])`;

  const gapWhere = Prisma.sql`${externalTreatmentGate}
      AND (
        CASE WHEN ${wholeMouthFlag} THEN
          NOT EXISTS (
            SELECT 1 FROM patient_facts tx
            WHERE tx.patient_id = p.id
              AND tx.type = 'treatment_record'
              AND tx.kind = 'actual'
              AND tx.status IN ('active', 'fulfilled')
              AND tx.content->>'category' = ANY(${resolverCats}::text[])
              ${afterDxFrag}
              AND COALESCE(NULLIF(trim(${sigToothExpr}), ''), '') = ''
          )
          -- 全口病(K05/K07):患者拒绝该类治疗 → 排除本次诊断召回(一线反馈⑧)
          AND NOT EXISTS (
            SELECT 1 FROM patient_facts rfx
            WHERE rfx.patient_id = p.id
              AND rfx.type = 'treatment_record'
              AND rfx.status IN ('active', 'fulfilled')
              AND rfx.content->>'subtype' ~ ${Prisma.sql`${refusalRe}`}
              AND rfx.content->>'category' = ANY(${resolverCats}::text[])
              ${refusalAfterDx}
          )
        ELSE
          cardinality(lat.remaining_teeth) > 0
        END
      )`;

  return {
    sigToothExpr,
    wholeMouthFlag,
    restorationIneligibleFrag,
    congenitalFrag,
    lateralJoin,
    toothOutput,
    gapWhere,
  };
}
