import { Injectable } from '@nestjs/common';
import { FactType, PersonaFeatureKey, VISIT_RECENCY_BUCKETS } from '@pac/types';
import type {
  ActiveFact,
  FeatureExtractor,
  FeatureExtractorContext,
  PersonaFeatureDraft,
} from './feature.interface';
import { visitFactsOf, visitSpanOf } from './visit-facts';

/**
 * visit_recency 上次到诊 —— 时间分档 + 那次的医生 + 主治医生。
 *
 * 一个特征喂三处,刻意合并而不是拆三个:
 *   ① 列表卡片的「2022-06-19 · 张敏」;
 *   ② 圈人筛选的「上次时间」(0-3 / 3-6 / 6-12 / 12-18 / 18+ 月);
 *   ③ 圈人筛选的「上次医生 / 偏好医生」(数据已就位,面板待接可搜索下拉,见 persona-tag-filters)。
 * 合并的理由:三者同源于同一次就诊。拆开算迟早各走各的 —— 卡片写 A 医生、筛选筛出 B,
 * 客服当场就发现。
 *
 * 【到诊口径】用 visitFactsOf(encounter + actual treatment + 挂号)——
 *   与 rfm / lifecycle_stage / urgency_level **同一口径**。
 *   ⚠️ 不用「encounter ∪ emr」那套(plan-aggregate 详情页的口径):同一个患者两套口径会算出
 *   两个"末诊",画像标签与详情页打架。要改口径就改 visit-facts.ts,四个特征一起动。
 *
 * 【医生口径】doctor_name 只有 emr_record 带(encounter_record 实测两个宿主都 0% 有名):
 *   · lastDoctor = **末诊当天**那份病历的医生。跨天的不认 —— 否则会出现
 *     "日期是 A 次、医生是 B 次"的错配,客服照着说就露馅,宁可留空。
 *   · preferredDoctor = 全部病历里出现**频次最高**的医生(并列取最近一次的)。
 *     这是拿「主治医生」代替业务要的「偏好医生」—— DW 的 fact_client_out.favor_doctor_id
 *     才是真·偏好医生,但 PAC 尚未摄入。等摄入后换成真值,消费方不用改。
 *
 * 【随时间自行改档】纯时间流逝就会跨桶(今天 2 个月、下个月就 3 个月),
 *   所以必须实现 nextBoundaryAt,否则水位闸永远不放行、分档永远停在算的那一刻。
 */
@Injectable()
export class VisitRecencyFeatureExtractor implements FeatureExtractor {
  readonly key = PersonaFeatureKey.VISIT_RECENCY;

  extract(ctx: FeatureExtractorContext): PersonaFeatureDraft | null {
    const visits = visitFactsOf(ctx);
    const span = visitSpanOf(visits);
    if (!span.last) return null; // 从没到过诊 → 不打这个标签(潜客由 lifecycle_stage 负责)

    const days = daysBetween(span.last, ctx.now);
    const bucket = bucketOf(days);
    const emrs = ctx.factsByType.get(FactType.EMR_RECORD) ?? [];

    const lastDoctor = doctorOfSameDay(emrs, span.last);
    const preferredDoctor = mostFrequentDoctor(emrs);

    const lastVisitAt = span.last.toISOString().slice(0, 10);
    const bucketZh = VISIT_RECENCY_BUCKETS.find((b) => b.value === bucket)?.zh ?? bucket;

    return {
      key: this.key,
      description:
        `上次到诊 ${lastVisitAt}(${bucketZh})` +
        (lastDoctor ? `,接诊医生 ${lastDoctor}` : '') +
        (preferredDoctor && preferredDoctor !== lastDoctor ? `;主治医生 ${preferredDoctor}` : ''),
      score: days,
      data: { bucket, lastVisitAt, daysSince: days, lastDoctor, preferredDoctor },
      evidence: { factIds: [span.lastFactId].filter((x): x is string => !!x) },
    };
  }

  /** 跨到下一个桶的时刻 —— 纯时间流逝改档,必须报,否则水位闸不放行(见类注释) */
  nextBoundaryAt(ctx: FeatureExtractorContext): Date | null {
    const span = visitSpanOf(visitFactsOf(ctx));
    if (!span.last) return null;
    const days = daysBetween(span.last, ctx.now);
    const nextEdge = BUCKET_EDGES.find((d) => d > days);
    if (nextEdge === undefined) return null; // 已在最后一档(18 个月以上)→ 不会再变
    return new Date(span.last.getTime() + nextEdge * 86400_000);
  }
}

/// 桶边界(天)。与 VISIT_RECENCY_BUCKETS 的 value 一一对应,改一处要改两处。
const BUCKET_EDGES = [90, 180, 365, 547] as const;

function bucketOf(days: number): string {
  if (days < 90) return '0_3m';
  if (days < 180) return '3_6m';
  if (days < 365) return '6_12m';
  if (days < 547) return '12_18m'; // 18 个月 ≈ 547 天
  return '18m_plus';
}

function daysBetween(from: Date, to: Date): number {
  return Math.max(0, Math.floor((to.getTime() - from.getTime()) / 86400_000));
}

function doctorNameOf(f: ActiveFact): string | null {
  const n = (f.content as Record<string, unknown> | null)?.doctor_name;
  const s = typeof n === 'string' ? n.trim() : '';
  return s || null;
}

/** 末诊**当天**那份病历的医生;跨天不认(见类注释的错配说明) */
function doctorOfSameDay(emrs: ActiveFact[], lastVisit: Date): string | null {
  const day = lastVisit.toISOString().slice(0, 10);
  for (const f of emrs) {
    if (!f.occurredAt || f.occurredAt.toISOString().slice(0, 10) !== day) continue;
    const n = doctorNameOf(f);
    if (n) return n;
  }
  return null;
}

/** 出现频次最高的医生;并列取"最近一次出现"的那个(而不是任意一个,保证结果稳定可复现) */
function mostFrequentDoctor(emrs: ActiveFact[]): string | null {
  const stat = new Map<string, { n: number; latest: number }>();
  for (const f of emrs) {
    const name = doctorNameOf(f);
    if (!name) continue;
    const t = f.occurredAt?.getTime() ?? 0;
    const cur = stat.get(name) ?? { n: 0, latest: 0 };
    stat.set(name, { n: cur.n + 1, latest: Math.max(cur.latest, t) });
  }
  let best: { name: string; n: number; latest: number } | null = null;
  for (const [name, v] of stat) {
    if (!best || v.n > best.n || (v.n === best.n && v.latest > best.latest)) {
      best = { name, ...v };
    }
  }
  return best?.name ?? null;
}
