import { Injectable } from '@nestjs/common';
import { PersonaFeatureKey } from '@pac/types';
import type {
  FeatureExtractor,
  FeatureExtractorContext,
  PersonaFeatureDraft,
} from './feature.interface';
import type { PotentialGap } from '../../clinical-gap/potential-treatment.selector';
import { nextAgeBoundary } from './time-boundary';

/**
 * potential_treatment 潜在治疗(C.1.1 转化机会)— 规则层,snapshot · 多标签
 *
 * 标签:潜在种植/正畸/早矫/修复/牙周/根管/拔牙/补牙。"有客观需求但未完成治疗"的转化机会。
 *
 * ⭐ 复用召回 gap 核心(ctx.potentialGaps,来自 PotentialTreatmentSelector → 共享 buildGapCore):
 *   = 召回挖的"诊断了/建议了但没启动对应治疗",**去掉召回的时间门**(常态属性)。
 *   口径与召回完全一致(圈人群 = 召回候选),单一真理源。
 *
 * K 码 → 8 业务标签映射:
 *   种植←K08(年龄>18)· 补牙←K02 · 根管←K04 · 牙周←K05/K06 ·
 *   正畸←K07(>12且≤40)· 早矫←K07(3-12)·
 *   修复←K03(默认)· 拔牙←K01 + K03(name 含 残根/残冠/无法保留/不能保留)
 *   K00 发育 / K09 囊肿 不在业务 8 标签 → 不出此标签(召回仍覆盖)。
 *
 * 业务 spec 对账:
 *   - 置信度(病历100%/影像AI 70-90%/客服勾选 50-70%)= PAC diagnosis=1.0 / recommendation=0.8(已建模)。
 *   - Step3 主诉意愿加分 = 排序事,消费方自算(score 弃用原则,不进标签)。
 *   - "非已丢单"(sales_chance)= PAC 未摄入丢单数据 → 省略(注明,follow-up)。
 */
const EXTRACTION_NAME_KW = ['残根', '残冠', '无法保留', '不能保留'];

/**
 * classifyGapToLabel 里的年龄闸(满这些岁数时,同一批 gap 的标签映射会变):
 *   3  → 够岁数进早矫;13 → 早矫转正畸;19 → 够岁数进种植(K08 要 >18);41 → 超龄退出正畸。
 * 与下方 switch 的边界一一对应,改一处要改两处 —— 见 tests/persona-time-boundary.spec.ts 的对账用例。
 */
const AGE_GATES = [3, 13, 19, 41] as const;

/// 周岁(单一源,potential_treatment / urgency_level 共用)
export function ageYearsAt(birth: Date | null, now: Date): number | null {
  if (!birth) return null;
  let age = now.getFullYear() - birth.getFullYear();
  const m = now.getMonth() - birth.getMonth();
  if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
  return age >= 0 && age <= 120 ? age : null;
}

/// 单个 gap → 8 业务标签 key(年龄/name 拆分);不命中(K00/K09/年龄外)→ null。
/// ⭐ 单一真理源:potential_treatment 出标签 + urgency_level 判"潜在待转" 共用此分类。
export function classifyGapToLabel(
  g: PotentialGap,
  age: number | null,
): { key: string; zh: string } | null {
  switch (g.primaryCode) {
    case 'K08':
      return age !== null && age > 18 ? { key: 'implant', zh: '潜在种植' } : null;
    case 'K02':
      return { key: 'filling', zh: '潜在补牙' };
    case 'K04':
      return { key: 'endo', zh: '潜在根管' };
    case 'K05':
    case 'K06':
      return { key: 'perio', zh: '潜在牙周' };
    case 'K01':
      return { key: 'extraction', zh: '潜在拔牙' };
    case 'K03': {
      const nm = g.nameZh ?? '';
      const isExtract = EXTRACTION_NAME_KW.some((k) => nm.includes(k));
      return isExtract ? { key: 'extraction', zh: '潜在拔牙' } : { key: 'restoration', zh: '潜在修复' };
    }
    case 'K07':
      if (age === null) return null; // 年龄未知 → 无法分早矫/正畸
      if (age >= 3 && age <= 12) return { key: 'early_ortho', zh: '潜在早矫' };
      if (age > 12 && age <= 40) return { key: 'ortho', zh: '潜在正畸' };
      return null; // >40 / <3 不召正畸
    default:
      return null; // K00 / K09 不在业务 8 标签
  }
}

@Injectable()
export class PotentialTreatmentFeatureExtractor implements FeatureExtractor {
  readonly key = PersonaFeatureKey.POTENTIAL_TREATMENT;

  extract(ctx: FeatureExtractorContext): PersonaFeatureDraft | null {
    const gaps = ctx.potentialGaps ?? [];
    if (gaps.length === 0) return null;
    const age = ageYearsAt(ctx.patient.birthDate, ctx.now);

    // 按业务标签聚合:teeth 并集 / daysSince 取最大(最早需求)/ confidence 取最大 / 来源
    const agg = new Map<
      string,
      { zh: string; teeth: Set<string>; daysSince: number; confidence: number; hasDx: boolean; hasRec: boolean }
    >();
    const factIds = new Set<string>();
    for (const g of gaps) {
      const lbl = classifyGapToLabel(g, age);
      if (!lbl) continue;
      factIds.add(g.factId);
      const cur =
        agg.get(lbl.key) ??
        { zh: lbl.zh, teeth: new Set<string>(), daysSince: 0, confidence: 0, hasDx: false, hasRec: false };
      for (const t of (g.tooth ?? '').split(';').map((s) => s.trim()).filter(Boolean)) cur.teeth.add(t);
      cur.daysSince = Math.max(cur.daysSince, g.daysSince);
      cur.confidence = Math.max(cur.confidence, g.confidence);
      if (g.signalType === 'diagnosis') cur.hasDx = true;
      else cur.hasRec = true;
      agg.set(lbl.key, cur);
    }
    if (agg.size === 0) return null;

    // 稳定顺序:价值/紧迫高的在前
    const ORDER = ['implant', 'ortho', 'early_ortho', 'restoration', 'perio', 'endo', 'extraction', 'filling'];
    const keys = [...agg.keys()].sort((a, b) => ORDER.indexOf(a) - ORDER.indexOf(b));
    const labels = keys.map((k) => agg.get(k)!.zh);
    const detail = keys.map((k) => {
      const v = agg.get(k)!;
      const teeth = [...v.teeth].sort();
      return {
        key: k,
        label: v.zh,
        teeth,
        daysSince: v.daysSince,
        confidence: v.confidence,
        source: v.hasDx && v.hasRec ? 'both' : v.hasRec ? 'recommendation' : 'diagnosis',
      };
    });

    return {
      key: this.key,
      description: labels.join(' / '),
      score: null,
      data: { types: keys, labels, detail },
      evidence: { factIds: [...factIds] },
    };
  }

  /**
   * 标签映射里有年龄闸(见 classifyGapToLabel):种植要 >18、早矫 3-12、正畸 >12 且 ≤40。
   * 跨过这些生日,同一批 gap 会映射出不同标签 —— 事实没变但标签要变。
   */
  nextBoundaryAt(ctx: FeatureExtractorContext): Date | null {
    if (!(ctx.potentialGaps ?? []).length) return null; // 无 gap → 本特征不产出,也无从改档
    return nextAgeBoundary(ctx.patient.birthDate, ctx.now, AGE_GATES);
  }

}
