import { Injectable, NotFoundException } from '@nestjs/common';
import {
  lookupDxTreatment,
  resolverCategoriesFor,
  diagnosisCodeNameZh,
} from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { GAP_PRIMARY_GROUPS } from '../../clinical-gap/potential-treatment-gap.sql';
import { TreatmentInitiationRecallScenario } from '../engine/scenarios/treatment-initiation-recall.scenario';

/**
 * RecallDebugService — 研发自查:一个患者「为什么召 / 为什么没召」。
 *
 * 设计立场(见 docs/algorithm 证据链讨论):证据链要的是**可定位**,不是可重建。
 *
 *   「为什么召」(presence)= 倒读现有行。引擎产出的 plan → reason →(evidence.factIds)
 *      → fact →(transactionIds)→ transaction。这是 **T0 快照**(reason.evidence 引的是
 *      生成时冻结的 fact 版本 id),忠实反映"引擎当时凭什么召"。
 *
 *   「为什么没召」(absence)= 缺失没有产出行,无从倒读 → 只能**现在时**回放闸门。
 *      做法:① 跑真·引擎 selectHits({patientId}) 拿 ground truth(是否当下可召);
 *           ② 再跑一遍人话版闸门漏斗(合规/冷静期/已治疗/未来预约)定位**卡在哪道闸**;
 *           ③ 漏斗与 ground truth 交叉核对,不一致就标注"引擎有更细牙位级判定,以引擎为准"。
 *      漏斗是**近似定位器**(patient/category 级),精确牙位级 gap 以引擎 selectHits 为准。
 *
 * 零新增存储:全是 on-demand 读 / 重放。仅研发用(控制器 PLATFORM_MANAGE 门控)。
 */
@Injectable()
export class RecallDebugService {
  /** 信号 code → 主码(primaryCode);用于查 cooldown / resolverCats。 */
  private static readonly CODE_TO_PRIMARY: Record<string, string> = (() => {
    const m: Record<string, string> = {};
    for (const [primary, grp] of Object.entries(GAP_PRIMARY_GROUPS)) {
      for (const c of [...grp.dxCodes, ...grp.recCodes]) m[c] = primary;
    }
    return m;
  })();

  private static readonly ALL_RECALL_CODES = new Set(
    Object.keys(RecallDebugService.CODE_TO_PRIMARY),
  );

  /** primaryCode → subKey(missing_tooth 等);用于把信号对齐到引擎 hit.subKey 做交叉核对。 */
  private static readonly PRIMARY_TO_SUBKEY: Record<string, string> = (() => {
    const m: Record<string, string> = {};
    for (const [subKey, cfg] of Object.entries(
      TreatmentInitiationRecallScenario.SUB_SCENARIOS,
    )) {
      m[(cfg as { primaryCode: string }).primaryCode] = subKey;
    }
    return m;
  })();

  constructor(
    private readonly prisma: PrismaService,
    private readonly initiation: TreatmentInitiationRecallScenario,
  ) {}

  async explain(
    scope: { hostId: string; tenantId: string },
    patientId: string,
  ): Promise<RecallDebugReport> {
    const now = new Date();
    const { hostId, tenantId } = scope;

    const patient = await this.prisma.patient.findFirst({
      where: { id: patientId, hostId, tenantId },
      include: { profile: true },
    });
    if (!patient) {
      throw new NotFoundException('patient 不在本 host 范围内或不存在');
    }

    // ── 当下事实(active 信号 + actual 治疗 active/fulfilled + active 预约)──
    const facts = await this.prisma.patientFact.findMany({
      where: {
        patientId,
        hostId,
        tenantId,
        status: { in: ['active', 'fulfilled'] },
      },
      select: {
        id: true,
        type: true,
        kind: true,
        status: true,
        occurredAt: true,
        plannedFor: true,
        content: true,
      },
    });

    // ── 为什么没召:合规闸 + 逐信号漏斗 ──
    const compliance = {
      active: patient.active,
      doNotContact: patient.profile?.doNotContact ?? false,
      deceased: patient.profile?.deceased ?? false,
      /// 宿主跟进闸:只有显式 true 才算被挡(null = 宿主未提供该信号 → 不启用)
      hostFollowUpActive: patient.profile?.hostFollowUpActive === true,
    };
    const compliancePass =
      compliance.active &&
      !compliance.doNotContact &&
      !compliance.deceased &&
      !compliance.hostFollowUpActive;

    const futureAppt = facts.find((f) => {
      if (f.type !== 'appointment_record' || f.status !== 'active') return false;
      const at = f.plannedFor ?? f.occurredAt;
      return !!at && at.getTime() > now.getTime();
    });

    const actualTreatments = facts.filter(
      (f) =>
        f.type === 'treatment_record' &&
        f.kind === 'actual' &&
        (f.status === 'active' || f.status === 'fulfilled'),
    );

    const signalFacts = facts.filter(
      (f) =>
        (f.type === 'diagnosis_record' || f.type === 'recommendation_record') &&
        f.status === 'active' &&
        RecallDebugService.ALL_RECALL_CODES.has(codeOf(f.content)),
    );

    // ground truth:跑真·引擎(单患者收窄)。拿到引擎确认的 subKey 集合,给逐信号交叉核对
    // ——漏斗是 patient/category 级近似,引擎做牙位级/先天剔除等更细判定,以引擎为准。
    const hits = await this.initiation.selectHits({ hostId, tenantId, now, patientId });
    const engineWouldRecall = hits.length > 0;
    const engineSubKeys = new Set(
      hits.map((h) => (h.subKey ?? '').split('@')[0]).filter(Boolean),
    );

    const signals: SignalFunnel[] = signalFacts.map((f) => {
      const code = codeOf(f.content);
      const primary = RecallDebugService.CODE_TO_PRIMARY[code] ?? code;
      const subKey = RecallDebugService.PRIMARY_TO_SUBKEY[primary] ?? null;
      const rule = lookupDxTreatment(primary);
      const resolverCats = resolverCategoriesFor(primary) as readonly string[];
      const sigTime = f.occurredAt ?? f.plannedFor;
      const daysSince = sigTime
        ? Math.floor((now.getTime() - sigTime.getTime()) / 86_400_000)
        : null;

      // ④ 冷静期:诊断后 < cooldownDays 不召
      const cooldownDays = rule?.cooldownDays ?? 0;
      const cooldownPass =
        sigTime != null &&
        sigTime.getTime() <= now.getTime() - cooldownDays * 86_400_000;

      // ⑤a 已治疗:同治疗家族 actual 在诊断之后(K05/K07 whole-mouth → 任何历史治疗即算)
      const everTreated = rule?.excludeIfEverTreated === true;
      const treated = actualTreatments.filter((t) => {
        const cat = String((t.content as Record<string, unknown>)?.category ?? '');
        if (!resolverCats.includes(cat)) return false;
        if (everTreated) return true;
        return !!t.occurredAt && !!sigTime && t.occurredAt >= sigTime;
      });

      // 阻断闸:按引擎顺序取第一道未过(合规已在外层统一判)
      let blockingGate: string | null = null;
      if (!cooldownPass) blockingGate = 'cooldown';
      else if (treated.length > 0) blockingGate = 'already_treated';
      else if (futureAppt) blockingGate = 'future_appointment';

      return {
        factId: f.id,
        code,
        codeName: diagnosisCodeNameZh(code) || code,
        subKey,
        // 引擎是否真的因该子场景召了(对齐 hit.subKey)。漏斗判 would_recall 但此处 false
        // = 引擎用更细判定排除了,以引擎为准。
        engineConfirmed: subKey ? engineSubKeys.has(subKey) : false,
        type: f.type,
        occurredAt: sigTime,
        daysSince,
        gates: {
          cooldown: {
            pass: cooldownPass,
            detail: `诊断后 ${daysSince ?? '—'}d / 需 ≥ ${cooldownDays}d`,
          },
          alreadyTreated: {
            pass: treated.length === 0,
            detail: everTreated
              ? '全口码(K05/K07):任何历史同类治疗即排除'
              : '诊断之后启动的同类 actual 治疗即排除',
            evidence: treated.map((t) => ({
              factId: t.id,
              category: String(
                (t.content as Record<string, unknown>)?.category ?? '',
              ),
              occurredAt: t.occurredAt,
            })),
          },
          futureAppointment: {
            pass: !futureAppt,
            evidence: futureAppt
              ? {
                  factId: futureAppt.id,
                  plannedFor: futureAppt.plannedFor ?? futureAppt.occurredAt,
                }
              : null,
          },
        },
        verdict: !compliancePass
          ? 'blocked_compliance'
          : blockingGate
            ? 'blocked'
            : 'would_recall',
        blockingGate: compliancePass ? blockingGate : 'compliance',
      };
    });

    // ── 为什么召:倒读 plan → reason → fact → transaction(含历史版本,前端可选)──
    const plans = await this.prisma.followupPlan.findMany({
      where: { patientId, hostId, tenantId },
      orderBy: { version: 'desc' },
      take: 10,
      include: { reasons: { orderBy: { priorityScore: 'desc' } } },
    });
    const activePlanCount = plans.filter(
      (p) => p.supersededAt === null && (p.status === 'active' || p.status === 'assigned'),
    ).length;

    const evidenceFactIds = uniq(
      plans.flatMap((p) =>
        p.reasons.flatMap((r) => evidenceFactIdsOf(r.evidence)),
      ),
    );
    const evFacts = evidenceFactIds.length
      ? await this.prisma.patientFact.findMany({
          where: { id: { in: evidenceFactIds } },
          select: {
            id: true,
            type: true,
            status: true,
            version: true,
            occurredAt: true,
            content: true,
            transactionIds: true,
          },
        })
      : [];
    const evFactMap = new Map(evFacts.map((f) => [f.id, f]));
    const evTxIds = uniq(evFacts.flatMap((f) => f.transactionIds));
    const evTxs = evTxIds.length
      ? await this.prisma.patientTransaction.findMany({
          where: { id: { in: evTxIds } },
          select: { id: true, sourceEventId: true, subjectType: true, occurredAt: true },
        })
      : [];
    const txMap = new Map(evTxs.map((t) => [t.id, t]));

    const recalledPlans: TracePlan[] = plans.map((p) => ({
      planId: p.id,
      version: p.version,
      status: p.status,
      isActive: p.supersededAt === null && (p.status === 'active' || p.status === 'assigned'),
      priorityScore: p.priorityScore,
      generatedAt: p.updatedAt,
      reasons: p.reasons.map((r) => ({
        scenario: r.scenario,
        subKey: r.subKey,
        priorityScore: r.priorityScore,
        reason: r.reason,
        breakdown: r.breakdown ?? null,
        signals: r.signals ?? null,
        evidence: evidenceFactIdsOf(r.evidence).map((fid) => {
          const f = evFactMap.get(fid);
          const content = (f?.content ?? null) as Record<string, unknown> | null;
          return {
            factId: fid,
            type: f?.type ?? null,
            status: f?.status ?? null,
            version: f?.version ?? null,
            code: content ? codeOf(content) || null : null,
            doctor: content?.doctor_name ? String(content.doctor_name) : null,
            occurredAt: f?.occurredAt ?? null,
            transactions: (f?.transactionIds ?? []).map((tid) => {
              const t = txMap.get(tid);
              return {
                transactionId: tid,
                sourceEventId: t?.sourceEventId ?? null,
                subjectType: t?.subjectType ?? null,
                occurredAt: t?.occurredAt ?? null,
              };
            }),
          };
        }),
      })),
    }));

    // ── 交叉核对说明 ──
    // 漏斗判 would_recall 但引擎未确认该子场景(engineConfirmed=false)= 引擎用更细判定排除。
    const divergent = uniq(
      signals
        .filter((s) => s.verdict === 'would_recall' && !s.engineConfirmed)
        .map((s) => s.subKey ?? s.code),
    );
    let note: string;
    if (signals.length === 0) {
      note = '无召回信号(该患者无 K 码诊断 / 医嘱推荐)。';
    } else if (engineWouldRecall && activePlanCount === 0) {
      note = '引擎判当下可召,但尚无 active plan —— 等下次重算/手动重算即生成。';
    } else if (divergent.length > 0) {
      note = `漏斗判应召的 [${divergent.join(', ')}] 引擎按更细判定(牙位级 / 先天剔除等)排除了 —— 以引擎 ground truth 为准。`;
    } else {
      note = '人话漏斗与引擎 ground truth 一致。';
    }

    return {
      patient: {
        id: patient.id,
        externalId: patient.externalId,
        name: patient.name,
        active: patient.active,
      },
      recalled: {
        hasActivePlan: activePlanCount > 0,
        engineWouldRecall,
        plans: recalledPlans,
      },
      notRecalled: {
        compliance: { pass: compliancePass, ...compliance },
        signalCount: signals.length,
        signals,
      },
      groundTruth: {
        engineHitCount: hits.length,
        hitSubKeys: uniq(hits.map((h) => h.subKey).filter(Boolean) as string[]),
      },
      note,
      computedAt: now,
    };
  }

  /**
   * 按姓名 / 病历号 / external_id / 手机号查患者(研发用人名查,重名返回多条供挑选)。
   * 按 host/tenant scope。
   */
  async search(
    scope: { hostId: string; tenantId: string },
    q: string,
  ): Promise<PatientCandidate[]> {
    const term = q.trim();
    if (!term) return [];
    const rows = await this.prisma.patient.findMany({
      where: {
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        OR: [
          { name: { contains: term } },
          { externalId: term },
          { medicalRecordNumber: term },
          { phone: term },
        ],
      },
      select: {
        id: true,
        externalId: true,
        name: true,
        gender: true,
        birthDate: true,
        medicalRecordNumber: true,
        active: true,
      },
      take: 25,
      orderBy: { name: 'asc' },
    });
    return rows.map((r) => ({
      id: r.id,
      externalId: r.externalId,
      name: r.name,
      gender: r.gender,
      age: ageFromBirth(r.birthDate),
      medicalRecordNumber: r.medicalRecordNumber,
      active: r.active,
    }));
  }

  /**
   * 链底:某 transaction 的 rawPayload(宿主推送 / DW 那条最原始字段级记录)。
   * 懒加载——证据链溯源最后一跳"点开看原始",不进主响应避免撑大 payload。
   * 按 host/tenant scope,碰不到别家。
   */
  async getRawPayload(
    scope: { hostId: string; tenantId: string },
    transactionId: string,
  ): Promise<TransactionRaw> {
    const tx = await this.prisma.patientTransaction.findFirst({
      where: { id: transactionId, hostId: scope.hostId, tenantId: scope.tenantId },
      select: {
        id: true,
        sourceEventId: true,
        subjectType: true,
        occurredAt: true,
        rawPayload: true,
      },
    });
    if (!tx) {
      throw new NotFoundException('transaction 不在本 host 范围内或不存在');
    }
    return tx;
  }
}

function ageFromBirth(birth: Date | null): number | null {
  if (!birth) return null;
  const ms = Date.now() - birth.getTime();
  if (ms <= 0) return null;
  return Math.floor(ms / (365.25 * 86_400_000));
}

function codeOf(content: unknown): string {
  const c = content as Record<string, unknown> | null;
  return c?.code != null ? String(c.code) : '';
}

function evidenceFactIdsOf(evidence: unknown): string[] {
  const e = evidence as { factIds?: unknown } | null;
  return Array.isArray(e?.factIds) ? (e!.factIds as unknown[]).map(String) : [];
}

function uniq<T>(arr: T[]): T[] {
  return [...new Set(arr)];
}

// ── 返回类型(控制器直出 JSON;研发调试端点不强约束 zod)──

export interface RecallDebugReport {
  patient: { id: string; externalId: string; name: string | null; active: boolean };
  recalled: {
    hasActivePlan: boolean;
    engineWouldRecall: boolean;
    plans: TracePlan[];
  };
  notRecalled: {
    compliance: {
      pass: boolean;
      active: boolean;
      doNotContact: boolean;
      deceased: boolean;
      hostFollowUpActive: boolean;
    };
    signalCount: number;
    signals: SignalFunnel[];
  };
  groundTruth: { engineHitCount: number; hitSubKeys: string[] };
  note: string;
  computedAt: Date;
}

export interface SignalFunnel {
  factId: string;
  code: string;
  codeName: string;
  subKey: string | null;
  engineConfirmed: boolean;
  type: string;
  occurredAt: Date | null;
  daysSince: number | null;
  gates: {
    cooldown: { pass: boolean; detail: string };
    alreadyTreated: {
      pass: boolean;
      detail: string;
      evidence: { factId: string; category: string; occurredAt: Date | null }[];
    };
    futureAppointment: {
      pass: boolean;
      evidence: { factId: string; plannedFor: Date | null } | null;
    };
  };
  verdict: 'would_recall' | 'blocked' | 'blocked_compliance';
  blockingGate: string | null;
}

export interface TracePlan {
  planId: string;
  version: number;
  status: string;
  isActive: boolean;
  priorityScore: number;
  generatedAt: Date;
  reasons: TraceReason[];
}

export interface PatientCandidate {
  id: string;
  externalId: string;
  name: string | null;
  gender: string | null;
  age: number | null;
  medicalRecordNumber: string | null;
  active: boolean;
}

export interface TraceReason {
  scenario: string;
  subKey: string | null;
  priorityScore: number;
  reason: string;
  breakdown: unknown;
  signals: unknown;
  evidence: TraceEvidenceFact[];
}

export interface TransactionRaw {
  id: string;
  sourceEventId: string | null;
  subjectType: string | null;
  occurredAt: Date | null;
  rawPayload: unknown;
}

export interface TraceEvidenceFact {
  factId: string;
  type: string | null;
  status: string | null;
  version: number | null;
  code: string | null;
  doctor: string | null;
  occurredAt: Date | null;
  transactions: {
    transactionId: string;
    sourceEventId: string | null;
    subjectType: string | null;
    occurredAt: Date | null;
  }[];
}
