import { Injectable } from '@nestjs/common';
import { PersonaFeatureKey, FactType, FactKind, lookupDxTreatment } from '@pac/types';
import type {
  FeatureExtractor,
  FeatureExtractorContext,
  PersonaFeatureDraft,
} from './feature.interface';

/**
 * recall_risk 召回风险 / 流失 / 复发(v2.1)
 *
 * v2.1 重塑(对齐 canonical-fact-layer.md §四):
 *   - 不再读 encounter_record.content.treatments[] 嵌套
 *   - 改读独立 fact 的 occurred_at 算"距上次活跃"
 *   - 改读 diagnosis_record + treatment_record 配对缺口算"链有缺口"
 *
 * 信号:
 *   1. 距上次任意临床事件天数(取 max(occurred_at) over diagnosis/treatment/encounter)
 *   2. 是否有"诊断后超期未治疗"缺口(K02 后 > 90d 无 restorative,K08 后 > 180d 无 implant)
 *   3. 投诉 complaint_record 存在 → 加风险但同时 DNC,本特征只看风险维度
 *
 * 分档:
 *   - 距上次 ≥ 540 天 + 链有缺口 → 'high'   score=3
 *   - 距上次 ≥ 360 天 / 链有缺口  → 'medium' score=2
 *   - 距上次 ≥ 180 天              → 'low'    score=1
 *   - 否则                         → 'none'   score=0
 */
@Injectable()
export class RecallRiskFeatureExtractor implements FeatureExtractor {
  readonly key = PersonaFeatureKey.RECALL_RISK;

  // 诊断码 → 期望 treatment category + 黄金窗(天)用单一真理源
  // canonical-codes.DiagnosisTreatmentMap(lookupDxTreatment)

  extract(ctx: FeatureExtractorContext): PersonaFeatureDraft | null {
    const diagnoses = ctx.factsByType.get(FactType.DIAGNOSIS_RECORD) ?? [];
    const treatments = ctx.factsByType.get(FactType.TREATMENT_RECORD) ?? [];
    const recommendations =
      ctx.factsByType.get(FactType.RECOMMENDATION_RECORD) ?? [];
    const encounters = ctx.factsByType.get(FactType.ENCOUNTER_RECORD) ?? [];

    const allClinical = [
      ...diagnoses,
      ...treatments,
      ...recommendations,
      ...encounters,
    ];
    if (allClinical.length === 0) {
      return {
        key: this.key,
        description: '无召回风险数据(从未有临床事件)',
        score: 0,
        evidence: { factIds: [] },
      };
    }

    // 1. 距上次任意临床事件天数
    const today = ctx.now;
    let latest: Date | null = null;
    const factIds: string[] = [];
    for (const f of allClinical) {
      factIds.push(f.id);
      if (!f.occurredAt) continue;
      if (!latest || f.occurredAt.getTime() > latest.getTime())
        latest = f.occurredAt;
    }
    const daysSince = latest
      ? Math.floor((today.getTime() - latest.getTime()) / 86400_000)
      : 9999;

    // 2. 缺口:遍历 dx/recommendation,看相应 treatment category 是否有,且诊断超窗
    const actualTreatmentCats = new Set<string>();
    for (const tx of treatments) {
      if (tx.kind !== FactKind.ACTUAL) continue;
      const c = tx.content as Record<string, unknown>;
      const cat = String(c.category ?? '');
      if (cat) actualTreatmentCats.add(cat);
    }

    let hasGap = false;
    for (const sig of [...diagnoses, ...recommendations]) {
      if (!sig.occurredAt) continue;
      const c = sig.content as Record<string, unknown>;
      const code = String(c.code ?? '');
      const rule = lookupDxTreatment(code);
      if (!rule) continue;
      const fulfilled = rule.categories.some((cat) =>
        actualTreatmentCats.has(cat),
      );
      if (fulfilled) continue;
      const sinceDx = Math.floor(
        (today.getTime() - sig.occurredAt.getTime()) / 86400_000,
      );
      if (sinceDx > rule.windowDays) {
        hasGap = true;
        break;
      }
    }

    let level: string;
    let score: number;
    if (daysSince >= 540 && hasGap) {
      level = 'high';
      score = 3;
    } else if (daysSince >= 360 || hasGap) {
      level = 'medium';
      score = 2;
    } else if (daysSince >= 180) {
      level = 'low';
      score = 1;
    } else {
      level = 'none';
      score = 0;
    }

    const gapStr = hasGap ? ',治疗链有未闭合缺口' : '';
    return {
      key: this.key,
      description: `[${level}] 距上次临床事件 ${daysSince} 天${gapStr}`,
      score,
      evidence: { factIds },
    };
  }
}
