import { Injectable } from '@nestjs/common';
import { lookupDxTreatment, resolverCategoriesFor } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';
import {
  buildGapCore,
  GAP_FLAGS_BY_PRIMARY,
  GAP_PRIMARY_GROUPS,
} from './potential-treatment-gap.sql';

/**
 * PotentialTreatmentSelector — 潜在治疗 gap 选择器(Layer 1 facts 消费,中立)
 *
 * 复用召回的 gap 核心(buildGapCore),产出每患者"诊断了/建议了但没启动对应治疗"的牙位级 gap,
 * 但**去掉召回的"现在能不能打"门**(cooldown / 未来预约 / 近期到诊 / 合规 DNC)——
 * 因为潜在治疗是"有客观需求"的**常态属性**(画像标签),不是"现在该打"(召回)。
 *
 * 消费方:Layer 2 潜在治疗画像(potential-treatment.feature)。
 * 单一真理源:gap 算法 / DiagnosisTreatmentMap / resolverCategoriesFor / §E flag / 码分组 全共享。
 */
@Injectable()
export class PotentialTreatmentSelector {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * 单患者 gap(按 activeCodes 剪枝:只查该患者真有 active 诊断/建议码的子场景)。
   * activeCodes = 该患者 active diagnosis_record + recommendation_record 的 content.code 集合(调用方从 ctx 取)。
   */
  async selectForPatient(opts: {
    hostId: string;
    tenantId: string;
    patientId: string;
    now: Date;
    activeCodes: Set<string>;
  }): Promise<PotentialGap[]> {
    const { hostId, tenantId, patientId, now, activeCodes } = opts;
    const out: PotentialGap[] = [];

    for (const [primaryCode, group] of Object.entries(GAP_PRIMARY_GROUPS)) {
      const allCodes = [...group.dxCodes, ...group.recCodes];
      // 剪枝:患者没有该组任一码 → 跳过(0 行,免一次 SQL)
      if (!allCodes.some((c) => activeCodes.has(c))) continue;
      const rule = lookupDxTreatment(primaryCode);
      if (!rule) continue;
      const resolverCats = resolverCategoriesFor(primaryCode) as readonly string[];
      const cfgFlags = GAP_FLAGS_BY_PRIMARY[primaryCode] ?? {};
      const gap = buildGapCore({ rule, cfgFlags, allCodes, resolverCats });

      const rows = await this.prisma.$queryRaw<RawGapRow[]>`
        SELECT
          sig.id                       AS fact_id,
          sig.content->>'code'         AS code,
          sig.content->>'name_zh'      AS name_zh,
          sig.type                     AS signal_type,
          ${gap.toothOutput}           AS tooth,
          sig.content->>'confidence'   AS confidence,
          EXTRACT(DAY FROM ${now}::timestamptz - COALESCE(sig.occurred_at, sig.planned_for))::int AS days_since,
          COALESCE(sig.occurred_at, sig.planned_for) AS anchor_at
        FROM patients p
        JOIN patient_facts sig ON sig.patient_id = p.id
        ${gap.lateralJoin}
        WHERE p.host_id = ${hostId}::uuid
          AND p.tenant_id = ${tenantId}
          AND p.id = ${patientId}::uuid
          AND p.active = true
          AND sig.status = 'active'
          AND sig.type IN ('diagnosis_record', 'recommendation_record')
          AND sig.content->>'code' = ANY(${allCodes}::text[])
          AND COALESCE(sig.occurred_at, sig.planned_for) IS NOT NULL
          ${gap.restorationIneligibleFrag}
          ${gap.congenitalFrag}
          ${gap.gapWhere}
      `;
      for (const r of rows) {
        out.push({
          primaryCode,
          factId: r.fact_id,
          code: r.code,
          nameZh: r.name_zh ?? null,
          tooth: r.tooth ?? null,
          daysSince: r.days_since,
          anchorAt: r.anchor_at,
          signalType: r.signal_type === 'recommendation_record' ? 'recommendation' : 'diagnosis',
          confidence: r.confidence
            ? Number(r.confidence)
            : r.signal_type === 'recommendation_record'
              ? 0.8
              : 1.0,
        });
      }
    }
    return out;
  }
}

export interface PotentialGap {
  primaryCode: string; // K08 / K02 / ...
  factId: string; // 触发该 gap 的 diagnosis/recommendation fact id(证据链)
  code: string; // 实际触发码(K08 / IMPLANT_RECOMMENDED ...)
  nameZh: string | null; // 诊断中文名(K03 拆 拔牙/修复 用)
  tooth: string | null; // 剩余未治牙位(';' 分隔;全口码为 null)
  daysSince: number;
  /**
   * 信号发生时刻(诊断 occurred_at / 推荐 planned_for)—— **不可变锚点**。
   *
   * 与 `daysSince` 的分工同 `ReasonSignals.signalOccurredAt` / `daysSince` 那一对:
   * 天数是算出来那一刻的快照(会陈旧),锚点是事实(不会)。窗口温度只认锚点 ——
   * 见 `@pac/types` 的 `gapTemperatureBounds`,以及那里「存边界时刻不存天数」的整段理由。
   */
  anchorAt: Date;
  signalType: 'diagnosis' | 'recommendation';
  confidence: number; // 诊断 1.0 / 建议 0.8
}

interface RawGapRow {
  fact_id: string;
  code: string;
  name_zh: string | null;
  signal_type: string;
  tooth: string | null;
  confidence: string | null;
  days_since: number;
  anchor_at: Date;
}
