import { Prisma } from '@prisma/client';
import {
  GAP_POSTOP_CARE_REVIEW_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 含"先天" → 正畸统筹,不自动召修复
}

/// §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,
  },
};

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: ['EXTRACTION_RECOMMENDED'] },
  K09: { dxCodes: ['K09'], recCodes: ['JAW_CYST_REMOVAL_RECOMMENDED'] },
  K00: { dxCodes: ['K00'], recCodes: ['ERUPTION_INTERVENTION_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=治疗家族 / ptx=术后护理证据 共用同一口径)
  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');

  // §E (d) 正畸减数位(仅 missing_tooth):该牙有外科拔除 + 患者有正畸语境 → 折进 resolved 减掉。
  const orthoExtractBranch = cfgFlags.excludeOrthoExtractionSites
    ? Prisma.sql`
      UNION
      SELECT eet AS t
        FROM patient_facts ex
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`ex.content->>'tooth_position'`)}) 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 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;

  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') 术后护理 = 该牙已做手术/种植的铁证(拆线/种植复查;单一源 GAP_POSTOP_CARE_REVIEW_PATTERNS)。
      --      光杆"拆线"落 review 类(chain S4 信号,fact 保真),gap 侧将其计入处理证据 —
      --      否则术后复诊 EMR 复述诊断(如 李然 47:1/25 种植,2/7 拆线+复述"牙齿缺少")
      --      会把最新诊断推到治疗后 → 时间方向误判"未启动"。暂观/观察 = 未治疗,不在此列。
      SELECT pct AS t
        FROM patient_facts ptx
        CROSS JOIN unnest(${toothArrSql(Prisma.sql`ptx.content->>'tooth_position'`)}) AS pct
       WHERE ptx.patient_id = p.id
         AND ptx.type = 'treatment_record' AND ptx.kind = 'actual'
         AND ptx.status IN ('active', 'fulfilled')
         AND ptx.content->>'category' = 'review'
         AND ptx.content->>'subtype' LIKE ANY(${[...GAP_POSTOP_CARE_REVIEW_PATTERNS]}::text[])
         ${afterDxFor('ptx')}
      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}
    ) 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`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}), ''), '') = ''
          )
        ELSE
          cardinality(lat.remaining_teeth) > 0
        END
      )`;

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