import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { TreatmentInitiationRecallScenario } from './scenarios/treatment-initiation-recall.scenario';
import type { PlanScenarioPlugin, ScenarioHit, ScenarioScope } from './scenario.interface';

/**
 * PlanEngineService — 跑所有 scenario plugin,产 FollowupPlan + PlanReason
 *
 * 模式:**Plan 是 patient 级触达单元**,Reason 是 scenario 维度命中理由。
 * 同 patient 多 scenario 命中 → 合并到同一 active Plan(多条 reason);
 * 同 plan 内同 scenario 最多一条 reason(`@@unique([planId, scenario])` 兜底)。
 *
 * 入口:
 *   - runAllForHost({ hostId, tenantId, now }) — 批量,所有 patient 全扫
 *   - runForPatient({ ..., patientId })        — 单 patient(W3+ 单刷场景)
 *
 * Plan 状态机:
 *   - 无 active plan + 有 hits → 创建新 plan(version=1)
 *   - 有 active plan + hits 一致(reason set 同)→ no-op(只更新 priorityScore)
 *   - 有 active plan + hits 不同 → supersede 旧 + 创建新版本
 *   - 已有 assigned plan → 不动(避免抢正在跟进的客服)— v1 用 simple skip
 */
@Injectable()
export class PlanEngineService {
  private readonly logger = new Logger(PlanEngineService.name);
  private readonly scenarios: PlanScenarioPlugin[];

  constructor(
    private readonly prisma: PrismaService,
    private readonly initiation: TreatmentInitiationRecallScenario,
  ) {
    // v2.1:一期只跑潜在治疗新链召回(treatment_initiation_recall)
    // 链已完成召回(treatment_aftercare_recall)留后续
    this.scenarios = [initiation];
  }

  /**
   * 单 patient 重算(W2 兼容入口 — 走 runAllForHost 内置过滤)。
   */
  async recomputeForPatient(input: {
    hostId: string;
    tenantId: string;
    clinicId?: string;
    patientId: string;
  }): Promise<{
    plansCreated: number;
    plansClosed: number;
    /// 本次重算对该患者 plan 的结果(前端刷新据此提示/切换版本)
    outcome: 'created' | 'superseded' | 'unchanged' | 'suppressed' | 'closed' | 'none';
    /// 重算后该患者当前 active/assigned plan id(关闭/无 plan 时 null)
    currentPlanId: string | null;
  }> {
    // 真·单 patient:scenario SQL 带 patientId 收窄(只扫该患者),只 upsert 这一个 plan。
    // (旧版走 runAllForHost 全租户扫 + 写全员 plan,详情页单刷被拖到分钟级 → 现 O(1))
    const scope: ScenarioScope = {
      hostId: input.hostId,
      tenantId: input.tenantId,
      now: new Date(),
      patientId: input.patientId,
    };
    const hits: ScenarioHitWithKey[] = [];
    for (const sc of this.scenarios) {
      const scHits = await sc.selectHits(scope);
      for (const h of scHits) hits.push({ ...h, scenarioKey: sc.key });
    }
    // ⭐ 缺口1 修复:本轮 0 命中 = 该患者已无任何活信号(治完疗/诊断消失/被排除闸过滤)。
    //   旧版直接 return → 患者遗留的 active plan 永远残留在召回池(治完疗该结案却一直 active)。
    //   现在主动 supersede 关闭它那条 active(非 assigned)plan,让它退出召回池。
    //   - assigned 不动(客服正在跟进,关掉会打断)
    //   - superseded/completed/abandoned 已是终态,无需处理
    if (hits.length === 0) {
      const closed = await this.closeStaleActivePlan(scope, input.patientId);
      return {
        plansCreated: 0,
        plansClosed: closed ? 1 : 0,
        outcome: closed ? 'closed' : 'none',
        currentPlanId: null,
      };
    }

    const log = await this.prisma.planGenerationLog.create({
      data: {
        hostId: input.hostId,
        tenantId: input.tenantId,
        patientId: input.patientId,
        triggeredBy: 'patient_refresh',
        status: 'running',
        scenarioRunCount: this.scenarios.length,
      },
    });
    try {
      const result = await this.upsertPlan({ scope, patientId: input.patientId, hits });
      const created = result === 'created' || result === 'superseded' ? 1 : 0;
      await this.prisma.planGenerationLog.update({
        where: { id: log.id },
        data: { status: 'success', plansCreated: created, endedAt: new Date() },
      });
      // 重算后该患者当前 plan id(可能是新版本)→ 回给前端做"切到新版本"
      const current = await this.prisma.followupPlan.findFirst({
        where: {
          hostId: input.hostId,
          tenantId: input.tenantId,
          patientId: input.patientId,
          status: { in: ['active', 'assigned'] },
        },
        orderBy: { version: 'desc' },
        select: { id: true },
      });
      // upsert 路径自身会 supersede 旧版本(版本流),不算"关闭退池";plansClosed 只统计 0 命中关闭。
      return { plansCreated: created, plansClosed: 0, outcome: result, currentPlanId: current?.id ?? null };
    } catch (err) {
      await this.prisma.planGenerationLog.update({
        where: { id: log.id },
        data: {
          status: 'failed',
          errorMessage: err instanceof Error ? err.message : String(err),
          endedAt: new Date(),
        },
      });
      throw err;
    }
  }

  /**
   * ⭐ 缺口1 helper:关闭该患者遗留的 active(非 assigned)plan(本轮 0 命中 → 退出召回池)。
   *
   * 关 status='active' **或 'assigned'** 的最新一条(2026-06 决策:缺口全消失 → 即使 assigned 也关,
   *   否则客服会拿"已解决"的理由白打;前端刷新时据此弹"已关闭"提示):
   *   - superseded/completed/abandoned 已是终态,不动
   * 关闭 = supersede(status='superseded' + supersededAt=now),不创建后继版本
   *   —— "无信号了"是终结,不是版本演进,故无新版本(版本流纪律:只有"换 reason set"才升版本)。
   * 返回是否真的关了一条(给调用方计数)。
   */
  private async closeStaleActivePlan(
    scope: ScenarioScope,
    patientId: string,
  ): Promise<boolean> {
    const latest = await this.prisma.followupPlan.findFirst({
      where: { hostId: scope.hostId, tenantId: scope.tenantId, patientId },
      orderBy: { version: 'desc' },
      select: { id: true, status: true },
    });
    if (!latest || (latest.status !== 'active' && latest.status !== 'assigned')) return false;
    await this.prisma.followupPlan.update({
      where: { id: latest.id },
      data: { status: 'superseded', supersededAt: scope.now },
    });
    return true;
  }

  /**
   * 批量跑某 host 全 patient 的召回算法,产 plans。
   */
  async runAllForHost(input: {
    hostId: string;
    tenantId: string;
    now?: Date;
  }): Promise<EngineRunResult> {
    const startedAt = new Date();
    const now = input.now ?? new Date();
    const scope: ScenarioScope = {
      hostId: input.hostId,
      tenantId: input.tenantId,
      now,
    };

    // 1. 各 scenario 跑 selector,汇总 hits
    const hitsByPatient = new Map<string, ScenarioHitWithKey[]>();
    for (const sc of this.scenarios) {
      const hits = await sc.selectHits(scope);
      for (const h of hits) {
        const arr = hitsByPatient.get(h.patientId) ?? [];
        arr.push({ ...h, scenarioKey: sc.key });
        hitsByPatient.set(h.patientId, arr);
      }
    }

    let plansCreated = 0;
    let plansSuperseded = 0;
    let plansUnchanged = 0;
    const plansSkippedAssigned = 0; // assigned 不再 skip(保留返回字段=0,兼容旧结构)
    let plansSuppressed = 0;
    let plansClosed = 0;

    // 2. 逐 patient 写 plan(每个 patient 一条 PlanGenerationLog,跟 schema 设计一致)
    for (const [patientId, hits] of hitsByPatient.entries()) {
      const log = await this.prisma.planGenerationLog.create({
        data: {
          hostId: scope.hostId,
          tenantId: scope.tenantId,
          patientId,
          triggeredBy: 'cli:batch',
          status: 'running',
          scenarioRunCount: this.scenarios.length,
        },
      });
      try {
        const result = await this.upsertPlan({ scope, patientId, hits });
        if (result === 'created') plansCreated++;
        else if (result === 'superseded') plansSuperseded++;
        else if (result === 'unchanged') plansUnchanged++;
        else if (result === 'suppressed') plansSuppressed++;

        await this.prisma.planGenerationLog.update({
          where: { id: log.id },
          data: {
            status: 'success',
            plansCreated: result === 'created' || result === 'superseded' ? 1 : 0,
            endedAt: new Date(),
          },
        });
      } catch (err) {
        await this.prisma.planGenerationLog.update({
          where: { id: log.id },
          data: {
            status: 'failed',
            errorMessage: err instanceof Error ? err.message : String(err),
            endedAt: new Date(),
          },
        });
        this.logger.error(
          `plan upsert failed patient=${patientId}: ${err instanceof Error ? err.message : err}`,
        );
      }
    }

    // 3. ⭐ 缺口1 修复:关闭"有 active plan 但本轮 0 命中"的患者的遗留 plan。
    //   旧版只遍历 hitsByPatient(有命中的患者)→ 治完疗变 0 命中的患者,其 active plan 永远残留召回池。
    //   现在:跑完 upsert(已产生本轮新 active)后,扫该 (host, tenant) 全部 status='active' plan,
    //   凡 patientId 不在本轮 hitsByPatient 的,supersede 关闭(退出召回池)。
    //   纪律:
    //     - 按 tenant 限定(隔离闸)
    //     - 只关 status='active';assigned 不动(客服跟进中),终态不动
    //     - 必须在 upsert 循环之后扫:这样本轮 created/superseded 出的新 active(其患者在 hitsByPatient)
    //       会被 hitsByPatient.has 命中而跳过,不会误关
    //     - 不创建后继版本(无信号是终结,非版本演进)
    const activePlans = await this.prisma.followupPlan.findMany({
      where: { hostId: scope.hostId, tenantId: scope.tenantId, status: 'active' },
      select: { id: true, patientId: true },
    });
    const staleIds = activePlans
      .filter((pl) => !hitsByPatient.has(pl.patientId))
      .map((pl) => pl.id);
    if (staleIds.length > 0) {
      const res = await this.prisma.followupPlan.updateMany({
        where: { id: { in: staleIds } },
        data: { status: 'superseded', supersededAt: now },
      });
      plansClosed = res.count;
    }

    return {
      scenariosRun: this.scenarios.length,
      patientsHit: hitsByPatient.size,
      plansCreated,
      plansSuperseded,
      plansUnchanged,
      plansSkippedAssigned,
      plansSuppressed,
      plansClosed,
      durationMs: Date.now() - startedAt.getTime(),
    };
  }

  // ── plan upsert(单 patient,事务内 supersede + insert)──
  private async upsertPlan(input: {
    scope: ScenarioScope;
    patientId: string;
    hits: ScenarioHitWithKey[];
  }): Promise<'created' | 'superseded' | 'unchanged' | 'suppressed'> {
    const { scope, patientId, hits } = input;

    const latest = await this.prisma.followupPlan.findFirst({
      where: { hostId: scope.hostId, tenantId: scope.tenantId, patientId },
      orderBy: { version: 'desc' },
      include: { reasons: true },
    });

    // ⭐ 设计决策(2026-06):assigned 不再整体 skip。
    //   理由:客服"正在执行"恰好撞上"正在重算"概率极低,为这点概率冻结内容、让客服拿过时理由白打,
    //   得不偿失。改为:照常重算,但**升新版本时继承分配**(assignee/assignedAt/recycleAt/contactAttempts
    //   带到新版)→ 客服不丢单、不被抢,只是召回理由刷新到最新;理由没变则只就地刷分(下面 unchanged 分支)。
    //   缺口全消失(0 命中)→ 即使 assigned 也关闭(见 closeStaleActivePlan)。

    // ⭐ 信号级抑制(召回闭环核心)— 抑制粒度 = 召回算法粒度 (scenario, subKey)。
    //   只压"已结案那条召回覆盖的诊断";患者新长的、不在已结案理由里的诊断照常召回(不受冷静期影响)。
    //   做法:取该患者所有"终态(completed/abandoned)+ 冷静期未到期"plan 的 reason (scenario, subKey)
    //         并集作抑制集,逐 hit 过滤掉命中抑制集的信号;剩下的(全新诊断)继续走下面生成逻辑。
    //   修复的 bug:旧版"整患者抑制" → 成功转化 K08 后,snooze 期内新长的 K02 也被压住不召(误伤新缺口)。
    //   多次结案也正确:查的是全部未到期终态 plan 的并集(不止 latest 一条)。
    const snoozedKeys = await this.fetchSnoozedSignalKeys(scope, patientId, scope.now);
    const usableHits =
      snoozedKeys.size === 0
        ? hits
        : hits.filter((h) => !snoozedKeys.has(`${h.scenarioKey}|${h.subKey ?? ''}`));
    if (usableHits.length === 0) {
      // 当前所有活信号都在冷静期内(= 刚结案那批)→ 不生成新 plan
      return 'suppressed';
    }

    const newPriorityScore = Math.max(...usableHits.map((h) => h.priorityScore));

    // 比对 active plan 的 reason 是否变化
    // ⭐ W3 末修:比对维度 = (scenario, subKey) 二元组集合,**不能只看 scenario**
    // 旧 bug:加新 SUB_SCENARIOS(K01/K07/K00 等)后,oldScenarios={treatment_initiation_recall}
    //        跟 newScenarios={treatment_initiation_recall} 相等 → 判 unchanged → 新 subKey 丢弃
    //        实际只刷了 priorityScore,plan_reasons 残留老的 1 条 missing_tooth
    const isActive = latest && (latest.status === 'active' || latest.status === 'assigned');
    if (isActive) {
      const key = (sc: string, sk: string | null | undefined) => `${sc}|${sk ?? ''}`;
      const oldSubScenarios = new Set(latest.reasons.map((r) => key(r.scenario, r.subKey)));
      const newSubScenarios = new Set(usableHits.map((h) => key(h.scenarioKey, h.subKey)));
      const sameSubScenarios =
        oldSubScenarios.size === newSubScenarios.size &&
        [...oldSubScenarios].every((k) => newSubScenarios.has(k));
      if (sameSubScenarios) {
        // 只刷 priorityScore,不创新版本
        if (latest.priorityScore !== newPriorityScore) {
          await this.prisma.followupPlan.update({
            where: { id: latest.id },
            data: { priorityScore: newPriorityScore },
          });
        }
        return 'unchanged';
      }
    }

    // 需要新版本:supersede 旧 + 创建新
    const nextVersion = (latest?.version ?? 0) + 1;
    // ⭐ 若旧版是 assigned:新版本继承分配(客服不丢单、不被抢;只是理由换到最新)。
    //   contactAttempts 一并带过去 → 频控/熔断计数延续(执行明细行仍挂旧版本,符合版本流语义)。
    const carryAssignment = latest?.status === 'assigned';
    // head = 优先级最高的 hit(用于 plan 顶层字段:goal / recommendedRole / recommendedAt / recommendedChannel)
    const head = [...usableHits].sort((a, b) => b.priorityScore - a.priorityScore)[0]!;

    // 拉对应 patient 的最新 active persona(可空)
    const persona = await this.prisma.persona.findFirst({
      where: { patientId, supersededAt: null },
      orderBy: { version: 'desc' },
      select: { id: true },
    });

    await this.prisma.$transaction(async (tx) => {
      if (latest && (latest.status === 'active' || latest.status === 'assigned')) {
        await tx.followupPlan.update({
          where: { id: latest.id },
          data: { status: 'superseded', supersededAt: new Date() },
        });
      }
      await tx.followupPlan.create({
        data: {
          hostId: scope.hostId,
          tenantId: scope.tenantId,
          patientId,
          personaId: persona?.id ?? null,
          targetClinicId: head.targetClinicId ?? null,
          version: nextVersion,
          priorityScore: newPriorityScore,
          goal: head.goal ?? null,
          recommendedAt: head.recommendedAt ?? null,
          recommendedRole: head.recommendedRole ?? null,
          recommendedChannel: head.recommendedChannel ?? null,
          // assigned 继承:新版本保持 assigned + 原客服 + 原回收窗 + 触达计数;否则回池(active)
          status: carryAssignment ? 'assigned' : 'active',
          assigneeUserId: carryAssignment ? latest!.assigneeUserId : null,
          assignedAt: carryAssignment ? latest!.assignedAt : null,
          recycleAt: carryAssignment ? latest!.recycleAt : null,
          contactAttempts: carryAssignment ? latest!.contactAttempts : 0,
          reasons: {
            // W3 末改:plan_reasons 维度 = (plan, scenario, sub_key) — 每独立临床缺口一行,
            // 不再合并子规则(K08 缺牙 / K05 牙周 / K04 根管 是不同治疗体系,合并 reason
            // 长且生硬、evidence 粒度丢)。话术 / 摘要 / 执行回写仍 plan 级(展示细 / 触达粗)。
            // UNIQUE(plan_id, scenario, sub_key)约束保证同子规则不重复。
            createMany: {
              data: usableHits.map((h) => ({
                scenario: h.scenarioKey,
                subKey: h.subKey ?? null,
                priorityScore: h.priorityScore,
                reason: h.reason,
                evidence: {
                  factIds: h.evidence.factIds,
                } as Prisma.InputJsonValue,
                breakdown: h.priorityBreakdown
                  ? ({
                      priority: { ...h.priorityBreakdown },
                      subKey: h.subKey ?? null,
                    } as Prisma.InputJsonValue)
                  : Prisma.JsonNull,
                // 结构化召回信号(DB 存 raw enum,前端字典翻译富文本渲染)
                signals: h.signals
                  ? (h.signals as unknown as Prisma.InputJsonValue)
                  : Prisma.JsonNull,
                lifecycle: this.lifecycleFor(h.scenarioKey, h.subKey),
                source: 'algorithm',
              })),
            },
          },
        },
      });
    }, { maxWait: 30000, timeout: 60000 });

    return latest ? 'superseded' : 'created';
  }

  /**
   * 信号级抑制集 —— 该患者所有"终态(completed/abandoned)+ 冷静期未到期(snoozedUntil>now)"plan
   * 覆盖的召回理由 (scenario, subKey) 并集。
   *
   * 语义:这些信号最近被召回处理过(成功转化/拒绝/外院/熔断…),在冷静期内不重复召回;
   *      但**不在此集合里的全新诊断不受影响**,照常生成召回(这是信号级抑制的核心,跟召回算法同粒度)。
   * 多次结案累积正确:取全部未到期终态 plan 的并集,而非只看最新一条。
   * 返回 key 格式 `${scenario}|${subKey ?? ''}`,跟 upsertPlan 过滤口径一致。
   */
  private async fetchSnoozedSignalKeys(
    scope: ScenarioScope,
    patientId: string,
    now: Date,
  ): Promise<Set<string>> {
    const terminalPlans = await this.prisma.followupPlan.findMany({
      where: {
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        patientId,
        status: { in: ['completed', 'abandoned'] },
        snoozedUntil: { gt: now },
      },
      select: { reasons: { select: { scenario: true, subKey: true } } },
    });
    const keys = new Set<string>();
    for (const plan of terminalPlans) {
      for (const r of plan.reasons) {
        keys.add(`${r.scenario}|${r.subKey ?? ''}`);
      }
    }
    return keys;
  }

  /// 子场景 → 生命周期类型(one_shot 短效 / recurring 长效)
  private lifecycleFor(scenario: string, subKey?: string): string {
    // 复查类长效(种植年度 / 牙周维护),其余短效
    if (subKey === 'implant_annual' || subKey === 'perio_maintenance') return 'recurring';
    return 'one_shot';
  }
}

type ScenarioHitWithKey = ScenarioHit & { scenarioKey: string };

export interface EngineRunResult {
  scenariosRun: number;
  patientsHit: number;
  plansCreated: number;
  plansSuperseded: number;
  plansUnchanged: number;
  plansSkippedAssigned: number;
  plansSuppressed: number;
  plansClosed: number;
  durationMs: number;
}
