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

/**
 * rfm 价值分群(RFM 八象限 + 生命周期)— 统计层
 *
 * ⭐ 口径直接采用业务整理好的 CDP RFM 标签定义(见 persona-design-v2.md §四 / 注册表 spec):
 *   数据字段:R=last_visit_time · F=visit_times · M=net_receipts_total(均 lifetime)
 *   数据来源:现从 PAC 事实层自算(宿主 CDP 报表给出后切宿主值,口径不变)
 *
 *   Step1 R/F/M 各 1-5 分
 *     R 距上次就诊:≤540=5 / 541-730=4 / 731-1095=3 / 1096-1460=2 / >1460=1
 *     F 累计就诊次:≥5=5 / 3-4=4 / 2=2 / 1=1(0 视为 1)
 *     M 累计净消费分位:TOP20%=5 / 20-40%=4 / 40-60%=3 / 60-80%=2 / BOTTOM20%=1
 *       (分位阈值由 PersonaService 群体计算注入 ctx.populationStats;缺失→绝对¥档兜底)
 *   Step2 八象限(决策树):
 *     R≥4 F≥3 M≥4 重要价值 / R=3 F≥3 M≥4 重要保持 / R≥4 F=2 M≥4 重要发展 / R≤2 F≥3 M≥4 重要挽留
 *     R≥4 F≥3 M<4 一般价值 / R=3 F≥3 M<4 一般保持 / R≥4 F=2 M<4 一般发展 / 其余(含 R≤2 任意)低活跃
 *
 * 另产(非图,PAC 消费用):valueTier 0-4(绝对¥档,喂 valueBonus + VIP 展示)、
 *   riskScore 0-3(=旧 recall_risk,喂 likelihoodBonus)、lifecycle 生命周期。
 * 特征只产事实(data),分值映射由各消费场景自管(score 列已弃用语义)。
 */
@Injectable()
export class RfmFeatureExtractor implements FeatureExtractor {
  readonly key = PersonaFeatureKey.RFM;

  // valueTier 绝对¥档(与旧 value 一致,保 valueBonus 行为不变)
  private static readonly VALUE_TIERS = [
    { cents: 3000_000, tier: 4 },
    { cents: 1000_000, tier: 3 },
    { cents: 300_000, tier: 2 },
    { cents: 50_000, tier: 1 },
  ];
  // M 无群体分位时的绝对¥兜底档(→1-5)
  private static readonly M_ABS = [3000_000, 1000_000, 300_000, 50_000];

  private static rScore(d: number): number {
    if (d <= 540) return 5;
    if (d <= 730) return 4;
    if (d <= 1095) return 3;
    if (d <= 1460) return 2;
    return 1;
  }
  private static fScore(n: number): number {
    if (n >= 5) return 5;
    if (n >= 3) return 4;
    if (n === 2) return 2;
    return 1; // 1 次或 0 次
  }
  private static mScore(cents: number, quantiles: number[]): number {
    if (quantiles.length === 4) {
      const [p20, p40, p60, p80] = quantiles;
      if (cents >= p80!) return 5;
      if (cents >= p60!) return 4;
      if (cents >= p40!) return 3;
      if (cents >= p20!) return 2;
      return 1;
    }
    // 兜底:绝对¥档
    const [a, b, c, d] = RfmFeatureExtractor.M_ABS;
    if (cents >= a!) return 5;
    if (cents >= b!) return 4;
    if (cents >= c!) return 3;
    if (cents >= d!) return 2;
    return 1;
  }

  /** RFM 8 分群决策树(R/F/M 为 1-5 分),图 B.1.1 Step2。
   *  弥补:图的 develop 档只写 F=2,这里扩成 F<3(含 F=1 的近期单次新客),
   *  避免"近期来过一次"误落低活跃;其余分层逐字照图。 */
  private static segmentOf(r: number, f: number, m: number): { key: string; zh: string } {
    const mHi = m >= 4; // M≥4 = 累计消费 TOP40%
    if (r >= 4 && f >= 3 && mHi) return { key: 'important_value', zh: '重要价值' };
    if (r === 3 && f >= 3 && mHi) return { key: 'important_retain', zh: '重要保持' };
    if (r >= 4 && f < 3 && mHi) return { key: 'important_develop', zh: '重要发展' }; // 图 F=2 → 扩 F<3
    if (r <= 2 && f >= 3 && mHi) return { key: 'important_winback', zh: '重要挽留' };
    if (r >= 4 && f >= 3 && !mHi) return { key: 'general_value', zh: '一般价值' };
    if (r === 3 && f >= 3 && !mHi) return { key: 'general_retain', zh: '一般保持' };
    if (r >= 4 && f < 3 && !mHi) return { key: 'general_develop', zh: '一般发展' }; // 图 F=2 → 扩 F<3
    return { key: 'low_active', zh: '低活跃' }; // 图 rule8(R≤2 任意)+ R=3&F<3 低频流失中
  }

  extract(ctx: FeatureExtractorContext): PersonaFeatureDraft {
    const get = (t: string) => ctx.factsByType.get(t) ?? [];
    const now = ctx.now;
    const DAY = 86400_000;

    // ── M:累计净消费(lifetime)──
    const moneyFactIds: string[] = [];
    let monetaryCents = 0;
    for (const f of get('payment_record')) {
      monetaryCents += Number((f.content as Record<string, unknown>).amount_cents ?? 0);
      moneyFactIds.push(f.id);
    }
    for (const f of get('recharge_record')) {
      monetaryCents += Number((f.content as Record<string, unknown>).amount_cents ?? 0);
      moneyFactIds.push(f.id);
    }
    for (const f of get('refund_record')) {
      monetaryCents -= Number((f.content as Record<string, unknown>).amount_cents ?? 0);
      moneyFactIds.push(f.id);
    }
    const valueTier = RfmFeatureExtractor.VALUE_TIERS.find((t) => monetaryCents >= t.cents)?.tier ?? 0;
    const mScore = RfmFeatureExtractor.mScore(monetaryCents, ctx.populationStats?.monetaryQuantiles ?? []);

    // ── R / F:就诊(encounter / actual treatment / 挂号)──
    const visitFacts: ActiveFact[] = [
      ...get(FactType.ENCOUNTER_RECORD),
      ...get(FactType.TREATMENT_RECORD).filter((f) => f.kind === FactKind.ACTUAL),
      ...get(FactType.VISIT_REGISTRATION_RECORD),
    ];
    const visitDays = new Set<string>();
    let lastVisit: Date | null = null;
    let firstVisit: Date | null = null;
    for (const f of visitFacts) {
      if (!f.occurredAt) continue;
      visitDays.add(f.occurredAt.toISOString().slice(0, 10));
      if (!lastVisit || f.occurredAt > lastVisit) lastVisit = f.occurredAt;
      if (!firstVisit || f.occurredAt < firstVisit) firstVisit = f.occurredAt;
    }
    const freqCount = visitDays.size; // lifetime 去重就诊天数 = visit_times
    const recencyDays = lastVisit ? Math.floor((now.getTime() - lastVisit.getTime()) / DAY) : 9999;
    const firstDays = firstVisit ? Math.floor((now.getTime() - firstVisit.getTime()) / DAY) : 9999;

    const rScore = RfmFeatureExtractor.rScore(recencyDays);
    const fScore = RfmFeatureExtractor.fScore(freqCount);
    const seg = RfmFeatureExtractor.segmentOf(rScore, fScore, mScore);

    // ── lifecycle 生命周期(临床节律,R 派生;reactivated 回流后续加)──
    let lifecycle: string;
    if (recencyDays > 730) lifecycle = 'churned';
    else if (recencyDays > 365) lifecycle = 'silent';
    else if (firstDays <= 180) lifecycle = 'new';
    else lifecycle = 'active';
    const lifecycleZh = { churned: '流失', silent: '沉默', new: '新客', active: '活跃' }[lifecycle]!;

    // ── riskScore 0-3(=旧 recall_risk:临床触点 recency + 治疗缺口),喂 likelihoodBonus ──
    const clinicalFactIds: string[] = [];
    let lastClinical: Date | null = null;
    for (const f of [
      ...get(FactType.DIAGNOSIS_RECORD),
      ...get(FactType.TREATMENT_RECORD),
      ...get(FactType.RECOMMENDATION_RECORD),
      ...get(FactType.ENCOUNTER_RECORD),
    ]) {
      clinicalFactIds.push(f.id);
      if (f.occurredAt && (!lastClinical || f.occurredAt > lastClinical)) lastClinical = f.occurredAt;
    }
    const clinicalDays = lastClinical ? Math.floor((now.getTime() - lastClinical.getTime()) / DAY) : 9999;
    const actualCats = new Set<string>();
    for (const tx of get(FactType.TREATMENT_RECORD)) {
      if (tx.kind !== FactKind.ACTUAL) continue;
      const cat = String((tx.content as Record<string, unknown>).category ?? '');
      if (cat) actualCats.add(cat);
    }
    let hasGap = false;
    for (const sig of [...get(FactType.DIAGNOSIS_RECORD), ...get(FactType.RECOMMENDATION_RECORD)]) {
      if (!sig.occurredAt) continue;
      const rule = lookupDxTreatment(String((sig.content as Record<string, unknown>).code ?? ''));
      if (!rule || rule.categories.some((c) => actualCats.has(c))) continue;
      if (Math.floor((now.getTime() - sig.occurredAt.getTime()) / DAY) > rule.windowDays) {
        hasGap = true;
        break;
      }
    }
    let riskScore: number;
    if (clinicalDays >= 540 && hasGap) riskScore = 3;
    else if (clinicalDays >= 360 || hasGap) riskScore = 2;
    else if (clinicalDays >= 180) riskScore = 1;
    else riskScore = 0;

    const yuan = (monetaryCents / 100).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
    const recencyStr = lastVisit ? `距上次 ${recencyDays} 天` : '无到诊记录';

    return {
      key: this.key,
      description: `${seg.zh} · ${lifecycleZh} · ${recencyStr} · 就诊${freqCount}次 · 累计¥${yuan} · R${rScore}F${fScore}M${mScore}`,
      // score 列已弃用语义(场景从 data 自算分);此处留空
      score: null,
      data: {
        segment: seg.key, // 八象限 → 圈人群
        lifecycle,
        rScore,
        fScore,
        mScore,
        recencyDays,
        firstSeenDays: firstDays,
        freqCount, // = visit_times(lifetime 去重就诊天数)
        monetaryCents, // = net_receipts_total
        valueTier, // 0-4 绝对¥档 → valueBonus / VIP 展示
        riskScore, // 0-3 → likelihoodBonus
        hasTreatmentGap: hasGap,
      },
      evidence: { factIds: [...new Set([...moneyFactIds, ...visitFacts.map((f) => f.id), ...clinicalFactIds])] },
    };
  }
}
