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

/**
 * special_attention 特别关注(D.2.3)— 规则层,snapshot · 多标签
 *
 * 标签:屡次爽约 / 经常迟到 / 免打扰 / 不可等候。一线服务需特别注意,影响排班与触达。
 *
 * 算法(近1年):
 *   屡次爽约:履约率 = 到诊/(到诊+爽约) < 50% 且 决定数(到诊+爽约)≥3。
 *     到诊 = completed/arrived/in_treatment;爽约 = no_show;cancelled/rescheduled/scheduled 不计分母。
 *   经常迟到:有到店时间的预约中,迟到(arrived_at > planned_for+15min)占比≥50% 且 ≥3 次。
 *   免打扰:profile.doNotContact=true(客户明确标注/客服标记;复用现有合规字段)。
 *   不可等候:notes/tags/病历(emr)含 不可等候/时间敏感/赶时间/不能等。
 *   全不命中 → 不打标签。
 */
@Injectable()
export class SpecialAttentionFeatureExtractor implements FeatureExtractor {
  readonly key = PersonaFeatureKey.SPECIAL_ATTENTION;
  private static readonly DAY = 86400_000;
  private static readonly LATE_MS = 15 * 60_000;
  // 注:曾因源 CH 跑 UTC 导致 arrived_at(in_time)摄入早 8h,这里加过 +8h band-aid。
  //   已根治 —— 源 CH(本地镜像 + 远程 DW)统一 Asia/Shanghai 时区,in_time 摄入即为正确北京时刻,
  //   band-aid 移除(留着反而会让到店时间晚 8h)。详见 apps/pac-docs/content/docs/operations/dw-data-issues.mdx。
  private static readonly ATTENDED = new Set(['completed', 'arrived', 'in_treatment']);
  private static readonly WAIT_KW = ['不可等候', '时间敏感', '赶时间', '不能等'];

  extract(ctx: FeatureExtractorContext): PersonaFeatureDraft | null {
    const now = ctx.now.getTime();
    const yearAgo = now - 365 * SpecialAttentionFeatureExtractor.DAY;
    // 用 appointmentsAll(各状态;factsByType 只 active/fulfilled,no_show 预约不在其中)
    const appts = (ctx.appointmentsAll ?? []) as ActiveFact[];

    // 近1年预约:履约 / 爽约 / 迟到
    let attended = 0;
    let noShow = 0;
    let arrivalRecs = 0;
    let lateRecs = 0;
    const noShowIds: string[] = [];
    const lateIds: string[] = [];
    for (const f of appts) {
      const c = (f.content ?? {}) as Record<string, unknown>;
      const planned = f.plannedFor ?? null;
      if (!planned || planned.getTime() < yearAgo) continue;
      const status = String(c.status ?? '');
      if (SpecialAttentionFeatureExtractor.ATTENDED.has(status)) attended++;
      else if (status === 'no_show') {
        noShow++;
        noShowIds.push(f.id);
      }
      // 迟到:到店时间 vs 预约时间(arrived_at 已按正确时区摄入,不再补偿)
      const arrRaw = c.arrived_at;
      if (arrRaw) {
        const arr = new Date(String(arrRaw)).getTime();
        if (Number.isFinite(arr)) {
          arrivalRecs++;
          if (arr - planned.getTime() > SpecialAttentionFeatureExtractor.LATE_MS) {
            lateRecs++;
            lateIds.push(f.id);
          }
        }
      }
    }
    const decided = attended + noShow;
    const evidenceIds = new Set<string>();

    const codes: string[] = [];
    const labels: string[] = [];
    const add = (c: string, z: string) => {
      codes.push(c);
      labels.push(z);
    };
    if (decided >= 3 && attended / decided < 0.5) {
      add('frequent_no_show', '屡次爽约');
      noShowIds.forEach((id) => evidenceIds.add(id));
    }
    if (arrivalRecs >= 3 && lateRecs / arrivalRecs >= 0.5) {
      add('often_late', '经常迟到');
      lateIds.forEach((id) => evidenceIds.add(id));
    }
    if (ctx.profile?.doNotContact) add('do_not_disturb', '免打扰'); // 来自 profile,非 fact

    // 不可等候:notes / tags / 病历自由文本关键词(命中的 emr fact 入证据)
    const profileHay = `${ctx.profile?.notes ?? ''} ${(ctx.profile?.tags ?? []).join(' ')}`;
    let waitHit = SpecialAttentionFeatureExtractor.WAIT_KW.some((k) => profileHay.includes(k));
    for (const f of ctx.factsByType.get(FactType.EMR_RECORD) ?? []) {
      const c = (f.content ?? {}) as Record<string, unknown>;
      const text = [c.illness_desc, c.diagnosis_text, c.treatment_plan, c.doctor_advice, c.notes].join(' ');
      if (SpecialAttentionFeatureExtractor.WAIT_KW.some((k) => text.includes(k))) {
        waitHit = true;
        evidenceIds.add(f.id);
      }
    }
    if (waitHit) add('cannot_wait', '不可等候');

    if (codes.length === 0) return null;
    return {
      key: this.key,
      description: labels.join(' / '),
      score: null,
      data: {
        types: codes,
        labels,
        fulfillRate: decided > 0 ? Math.round((100 * attended) / decided) : null,
        apptDecided: decided,
        lateRate: arrivalRecs > 0 ? Math.round((100 * lateRecs) / arrivalRecs) : null,
        arrivalRecs,
      },
      evidence: { factIds: [...evidenceIds] },
    };
  }
}
