/**
 * recall-oracle —— 召回算法的**独立第二实现**(对抗 / 差分验证用)
 *
 * 目的:用一套**完全不同范式**的实现重算"这颗牙该不该召回",跟生产 SQL
 *       (treatment-initiation-recall.scenario.ts)的结论比对。两边一致才可信,
 *       分歧处即 bug 捕获点(可能是生产误召/漏召,也可能是本 oracle 写错)。
 *
 * ⚠️ 对抗的纪律线(务必遵守,否则对抗失效):
 *   - 可以共享:**配置数据** = DiagnosisTreatmentMap / 废用牙名单 / 主诉映射(单一真理源,
 *     本就该两边引同一份,改窗口不漂移)。
 *   - 绝不共享:**判定逻辑本身**。生产是 SQL 集合运算(array overlap / LATERAL 按牙相减 /
 *     union-find 聚类);本 oracle 故意用**按单颗牙的时间序状态机**(chronological walk),
 *     心智模型不同 → 同一个 bug 不会同时出现在两边互相掩护。
 *   - 本文件照**算法语义**写,不照 SQL 逐行翻译。
 *
 * 覆盖的召回门控(对齐 scenario.ts 注释编号):
 *   ③ 信号(diagnosis/recommendation + 召回码)   ④ cooldown 下界(per-code)
 *   ④' 废用牙剔除                                 ⑤a 同类实治(afterDx)
 *   ⑤c 同牙拔除(afterDx)                         ⑤e 同牙替代定性(种植/冠 afterDx)
 *   ⑤b 未来预约                                   ⑤d 主诉匹配预约(afterSig)
 *   ⑤f 14 天就诊冷静                              按牙相减 / wholeMouth(K05/K07)
 *
 * 不覆盖(患者级硬闸,plan 存在即已通过,不在单牙粒度判):
 *   ① 隔离(host/tenant)  ② 合规(active/DNC/deceased)
 */
import {
  DiagnosisTreatmentMap,
  APPT_COMPLAINT_TO_CATEGORY,
  isRestorationIneligibleDxName,
  resolverCategoriesFor,
  treatmentCategoryNameZh,
} from '@pac/types';
import type { AdaptedFact } from './adapt-data';

export const WHOLE_PERIO = '全口 · 牙周';
export const WHOLE_ORTHO = '全口 · 正畸';
export const WHOLE_OTHER = '全口 · 其他';

const POST_VISIT_COOLDOWN_DAYS = 14;

/// 召回码 → 子场景(subKey + 取 rule 用的 primaryCode)。
/// 跟 scenario.ts 的 SUB_SCENARIOS 一一对应(配置同源,允许共享)。
const CODE_TO_SUB: Record<string, { subKey: string; primary: string; label: string }> = {
  K08: { subKey: 'missing_tooth', primary: 'K08', label: '缺失牙修复' },
  IMPLANT_RECOMMENDED: { subKey: 'missing_tooth', primary: 'K08', label: '缺失牙修复' },
  K07: { subKey: 'ortho_no_consult', primary: 'K07', label: '正畸矫治' },
  ORTHO_CONSULT_RECOMMENDED: { subKey: 'ortho_no_consult', primary: 'K07', label: '正畸矫治' },
  K04: { subKey: 'endo_no_rct', primary: 'K04', label: '根管治疗' },
  RCT_RECOMMENDED: { subKey: 'endo_no_rct', primary: 'K04', label: '根管治疗' },
  K05: { subKey: 'perio_no_srp', primary: 'K05', label: '牙周治疗' },
  SRP_RECOMMENDED: { subKey: 'perio_no_srp', primary: 'K05', label: '牙周治疗' },
  K02: { subKey: 'caries_no_filling', primary: 'K02', label: '龋齿充填' },
  FILLING_RECOMMENDED: { subKey: 'caries_no_filling', primary: 'K02', label: '龋齿充填' },
  K03: { subKey: 'hard_tissue_damage', primary: 'K03', label: '牙体修复' },
  HARD_TISSUE_REPAIR_RECOMMENDED: { subKey: 'hard_tissue_damage', primary: 'K03', label: '牙体修复' },
  CROWN_RECOMMENDED: { subKey: 'hard_tissue_damage', primary: 'K03', label: '牙体修复' },
  K06: { subKey: 'gum_alveolar_lesion', primary: 'K06', label: '牙龈/牙槽嵴' },
  GUM_TREATMENT_RECOMMENDED: { subKey: 'gum_alveolar_lesion', primary: 'K06', label: '牙龈/牙槽嵴' },
  K01: { subKey: 'impacted_tooth', primary: 'K01', label: '阻生牙拔除' },
  EXTRACTION_RECOMMENDED: { subKey: 'impacted_tooth', primary: 'K01', label: '阻生牙拔除' },
  K09: { subKey: 'jaw_cyst', primary: 'K09', label: '颌骨囊肿' },
  JAW_CYST_REMOVAL_RECOMMENDED: { subKey: 'jaw_cyst', primary: 'K09', label: '颌骨囊肿' },
  K00: { subKey: 'development_eruption', primary: 'K00', label: '发育/萌出' },
  ERUPTION_INTERVENTION_RECOMMENDED: { subKey: 'development_eruption', primary: 'K00', label: '发育/萌出' },
};

export type OracleVerdictKind =
  | 'recall' // 应召回
  | 'resolved' // 已被同类/拔除/替代治疗解决
  | 'cooldown' // 诊断未过冷静期
  | 'ineligible' // 废用牙/无功能牙,非修复对象
  | 'suppressed'; // 被患者级闸压制(未来预约/主诉预约/近期到诊)

export type OracleVerdict = {
  laneKey: string; // 牙位 toothBase(如 "26")或全口泳道名(WHOLE_PERIO 等)— 跟 ToothTimeline 泳道同空间
  isWhole: boolean;
  subKey: string; // 'caries_no_filling' …
  subLabel: string; // '龋齿充填'
  code: string; // 'K02'
  signalFactId: string;
  signalDate: string | null; // ISO
  daysSince: number | null;
  kind: OracleVerdictKind;
  detail: string; // 人读判定理由
};

const VERDICT_META: Record<OracleVerdictKind, { zh: string; tone: string }> = {
  recall: { zh: '应召回', tone: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
  resolved: { zh: '已解决', tone: 'bg-slate-100 text-slate-500 border-slate-200' },
  cooldown: { zh: '考虑期', tone: 'bg-amber-50 text-amber-700 border-amber-200' },
  ineligible: { zh: '非修复', tone: 'bg-slate-100 text-slate-400 border-slate-200' },
  suppressed: { zh: '被压制', tone: 'bg-slate-100 text-slate-500 border-slate-200' },
};

export function verdictMeta(kind: OracleVerdictKind) {
  return VERDICT_META[kind];
}

// ── 牙位归一(独立实现,跟 tooth-timeline 同义:剥牙面后缀,保留牙位 base)──
function toothBase(t: string): string {
  const m = /^(\d{1,2}[A-E]?)/i.exec(t.trim());
  return m ? m[1]!.toUpperCase() : t.trim().toUpperCase();
}
function factTeeth(c: Record<string, unknown>): string[] {
  const tp = c.tooth_positions;
  const raw = Array.isArray(tp) ? tp.join(';') : String(c.tooth_position ?? tp ?? '');
  return raw
    .split(/[;,]+/)
    .map((s) => s.trim())
    .filter(Boolean)
    .map(toothBase);
}

type Rule = {
  categories: readonly string[];
  cooldownDays: number;
  wholeMouth?: boolean;
  excludeIfEverTreated?: boolean;
};

/// 反查:哪些 host 主诉文本(complaint_category 值)对应这些治疗类别 → 给 ⑤d 用
function complaintTextsFor(cats: readonly string[]): string[] {
  return Object.entries(APPT_COMPLAINT_TO_CATEGORY)
    .filter(([, v]) => cats.includes(v))
    .map(([text]) => text);
}

/**
 * 独立 oracle 主入口:对一个患者的全部 fact 跑一遍,产出每颗牙(+ 全口泳道)的召回判定。
 */
export function runRecallOracle(facts: AdaptedFact[], now: Date): OracleVerdict[] {
  const ms = now.getTime();
  const daysAgoMs = (n: number) => ms - n * 86400000;

  // ── 一遍扫描,收集治疗 / 预约 / 到诊 / 信号 ──
  const treatments: Array<{ teeth: Set<string>; cat: string; t: number; iso: string }> = [];
  let hasFutureAppt = false;
  let recentVisit = false;
  const complaintAppts: Array<{ t: number; cats: Set<string> }> = [];
  const signals: Array<{
    factId: string;
    c: Record<string, unknown>;
    code: string;
    t: number;
    iso: string;
  }> = [];

  for (const f of facts) {
    const c = (f.content ?? {}) as Record<string, unknown>;
    if (
      f.type === 'treatment_record' &&
      f.kind === 'actual' &&
      (f.status === 'active' || f.status === 'fulfilled')
    ) {
      const iso = f.occurredAt ?? f.plannedFor;
      if (iso) {
        treatments.push({
          teeth: new Set(factTeeth(c)),
          cat: String(c.category ?? ''),
          t: new Date(iso).getTime(),
          iso,
        });
      }
    } else if (f.type === 'appointment_record') {
      const iso = f.plannedFor ?? f.occurredAt;
      const at = iso ? new Date(iso).getTime() : null;
      if (f.status === 'active' && at != null && at > ms) hasFutureAppt = true;
      if ((f.status === 'active' || f.status === 'fulfilled') && at != null) {
        const cats = new Set(
          String(c.complaint_category ?? '')
            .split(',')
            .map((s) => s.trim())
            .filter(Boolean),
        );
        if (cats.size) complaintAppts.push({ t: at, cats });
      }
    } else if (f.type === 'encounter_record' || f.type === 'emr_record') {
      if (f.occurredAt) {
        const vt = new Date(f.occurredAt).getTime();
        if (vt > daysAgoMs(POST_VISIT_COOLDOWN_DAYS)) recentVisit = true;
      }
    }
    // 信号(诊断 / 推荐)
    if (
      (f.type === 'diagnosis_record' || f.type === 'recommendation_record') &&
      f.status === 'active'
    ) {
      const code = String(c.code ?? '');
      const iso = f.occurredAt ?? f.plannedFor;
      if (CODE_TO_SUB[code] && iso) {
        signals.push({ factId: f.id, c, code, t: new Date(iso).getTime(), iso });
      }
    }
  }

  const out: OracleVerdict[] = [];

  for (const sig of signals) {
    const sub = CODE_TO_SUB[sig.code]!;
    const rule = DiagnosisTreatmentMap[sub.primary as keyof typeof DiagnosisTreatmentMap] as Rule;
    const wholeMouth = !!rule.wholeMouth;
    // ⭐ 治疗家族 resolver(共享单一真理源 resolverCategoriesFor;判定逻辑下面独立走)
    //   结构码 = 局部结构治疗全集(含 cosmetic/endodontic/restorative…);牙周/正畸沿用 rule.categories。
    const resolverCats = new Set<string>(resolverCategoriesFor(sub.primary));
    const nameZh = String(sig.c.name_zh ?? '');
    const daysSince = Math.floor((ms - sig.t) / 86400000);
    const inCooldown = sig.t > daysAgoMs(rule.cooldownDays);
    const complaintTexts = complaintTextsFor(rule.categories);

    const evalUnit = (laneKey: string, isWhole: boolean, toothFilter: string | null) => {
      let kind: OracleVerdictKind;
      let detail: string;

      // ④' 废用牙
      if (isRestorationIneligibleDxName(nameZh)) {
        kind = 'ineligible';
        detail = `「${nameZh}」非修复对象(该拔除/观察)`;
      } else {
        // 解决者:resolverCats 家族里任一治疗。
        //   wholeMouth → 任意时间 if excludeIfEverTreated,否则 afterDx;
        //   按牙 → afterDx,且落在本颗牙。
        let resolver: { cat: string; iso: string } | undefined;
        if (wholeMouth) {
          resolver = treatments.find(
            (tr) =>
              resolverCats.has(tr.cat) &&
              (rule.excludeIfEverTreated ? true : tr.t >= sig.t),
          );
        } else {
          resolver = treatments.find(
            (tr) =>
              resolverCats.has(tr.cat) &&
              tr.t >= sig.t &&
              (toothFilter == null || tr.teeth.has(toothFilter)),
          );
        }

        if (resolver) {
          kind = 'resolved';
          detail = `诊断后做过 ${treatmentCategoryNameZh(resolver.cat) || resolver.cat}(${resolver.iso.slice(0, 10)})`;
        } else if (inCooldown) {
          kind = 'cooldown';
          detail = `诊断仅 ${daysSince} 天 < 冷静期 ${rule.cooldownDays} 天`;
        } else if (hasFutureAppt) {
          kind = 'suppressed';
          detail = '患者已有未来预约(⑤b)';
        } else if (
          complaintAppts.some(
            (a) => a.t >= sig.t && [...a.cats].some((x) => complaintTexts.includes(x)),
          )
        ) {
          kind = 'suppressed';
          detail = '诊断后有主诉匹配的预约(⑤d)';
        } else if (recentVisit) {
          kind = 'suppressed';
          detail = `近 ${POST_VISIT_COOLDOWN_DAYS} 天内到过诊(⑤f)`;
        } else {
          kind = 'recall';
          detail = `已 ${daysSince} 天未启动 ${rule.categories.map((c) => treatmentCategoryNameZh(c) || c).join('/')}`;
        }
      }

      out.push({
        laneKey,
        isWhole,
        subKey: sub.subKey,
        subLabel: sub.label,
        code: sig.code,
        signalFactId: sig.factId,
        signalDate: sig.iso,
        daysSince,
        kind,
        detail,
      });
    };

    if (wholeMouth) {
      const laneKey =
        sub.primary === 'K05' ? WHOLE_PERIO : sub.primary === 'K07' ? WHOLE_ORTHO : WHOLE_OTHER;
      evalUnit(laneKey, true, null);
    } else {
      const teeth = [...new Set(factTeeth(sig.c))];
      if (teeth.length === 0) {
        evalUnit(WHOLE_OTHER, true, null);
      } else {
        for (const t of teeth) evalUnit(toothBase(t), false, toothBase(t));
      }
    }
  }

  return out;
}

// ─────────────────────────────────────────────────────────
// 对账:oracle 判定 vs 生产 plan_reasons
// ─────────────────────────────────────────────────────────

export type ReconMark = 'agree' | 'oracle_only' | 'prod_only';

export type ReconRow = {
  key: string; // subKey|laneKey
  laneKey: string;
  toothLabel: string; // 牙位展示(全口泳道直接用泳道名)
  subKey: string;
  subLabel: string;
  oracle: OracleVerdict | null; // 该单元的 oracle 判定(可能非 recall)
  oracleRecall: boolean;
  prodRecall: boolean;
  mark: ReconMark;
};

/// 生产侧召回单元:从 plan.reasons[].signals 摊平成 (subKey, laneKey)
export type ProdSignal = {
  subKey?: string | null;
  toothPosition?: string | null;
} | null;

function wholeLaneForSub(subKey: string): string {
  if (subKey === 'perio_no_srp') return WHOLE_PERIO;
  if (subKey === 'ortho_no_consult') return WHOLE_ORTHO;
  return WHOLE_OTHER;
}

export function reconcile(
  verdicts: OracleVerdict[],
  prodSignals: ProdSignal[],
): { rows: ReconRow[]; agree: number; oracleOnly: number; prodOnly: number } {
  // 生产召回 key 集合
  const prodSet = new Set<string>();
  for (const s of prodSignals) {
    if (!s?.subKey) continue;
    const teethRaw = (s.toothPosition ?? '').trim();
    if (!teethRaw) {
      prodSet.add(`${s.subKey}|${wholeLaneForSub(s.subKey)}`);
    } else {
      for (const t of teethRaw.split(/[;,]+/).map((x) => x.trim()).filter(Boolean)) {
        prodSet.add(`${s.subKey}|${toothBase(t)}`);
      }
    }
  }

  // oracle 单元(同 (subKey, laneKey) 取最强:recall > suppressed/cooldown/resolved/ineligible)
  const rank: Record<OracleVerdictKind, number> = {
    recall: 5,
    suppressed: 4,
    cooldown: 3,
    resolved: 2,
    ineligible: 1,
  };
  const oracleByKey = new Map<string, OracleVerdict>();
  for (const v of verdicts) {
    const k = `${v.subKey}|${v.laneKey}`;
    const cur = oracleByKey.get(k);
    if (!cur || rank[v.kind] > rank[cur.kind]) oracleByKey.set(k, v);
  }

  const allKeys = new Set<string>([...prodSet, ...oracleByKey.keys()]);
  const rows: ReconRow[] = [];
  let agree = 0,
    oracleOnly = 0,
    prodOnly = 0;

  for (const k of allKeys) {
    const [subKey, laneKey] = k.split('|') as [string, string];
    const oracle = oracleByKey.get(k) ?? null;
    const oracleRecall = oracle?.kind === 'recall';
    const prodRecall = prodSet.has(k);

    let mark: ReconMark;
    if (oracleRecall && prodRecall) {
      mark = 'agree';
      agree++;
    } else if (oracleRecall && !prodRecall) {
      mark = 'oracle_only';
      oracleOnly++;
    } else if (!oracleRecall && prodRecall) {
      mark = 'prod_only';
      prodOnly++;
    } else {
      // 两边都不召(oracle 判 resolved/cooldown/… 且生产也没召)→ 不算分歧,不进对账行
      continue;
    }

    const isWhole = laneKey.startsWith('全口');
    rows.push({
      key: k,
      laneKey,
      toothLabel: isWhole ? laneKey : `牙位 ${laneKey}`,
      subKey,
      subLabel: oracle?.subLabel ?? CODE_TO_SUB[subKeyToCode(subKey)]?.label ?? subKey,
      oracle,
      oracleRecall,
      prodRecall,
      mark,
    });
  }

  // 排序:分歧优先(prod_only / oracle_only)→ agree;同组按牙位
  const markOrder: Record<ReconMark, number> = { prod_only: 0, oracle_only: 1, agree: 2 };
  rows.sort(
    (a, b) =>
      markOrder[a.mark] - markOrder[b.mark] ||
      a.laneKey.localeCompare(b.laneKey, undefined, { numeric: true }),
  );

  return { rows, agree, oracleOnly, prodOnly };
}

/// subKey → 任一对应 code(仅用于 prod_only 行兜底 subLabel)
function subKeyToCode(subKey: string): string {
  for (const [code, v] of Object.entries(CODE_TO_SUB)) {
    if (v.subKey === subKey) return code;
  }
  return '';
}

export { toothBase as oracleToothBase };
