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 }> {
    // v1 简化:单 patient 走全量过滤路径(scenario selector 自带 patient 过滤的成本可接受);
    // W3+ 加 patientId 入参深入 scenario plugin 真正只跑该 patient
    const r = await this.runAllForHost({
      hostId: input.hostId,
      tenantId: input.tenantId,
    });
    return { plansCreated: r.plansCreated };
  }

  /**
   * 批量跑某 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;
    let plansSkippedAssigned = 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 === 'skipped_assigned') plansSkippedAssigned++;

        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}`,
        );
      }
    }

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

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

    // 已分配给客服(assigned)→ 不动,避免打断客服跟进
    if (latest && latest.status === 'assigned') {
      return 'skipped_assigned';
    }

    const newPriorityScore = Math.max(...hits.map((h) => h.priorityScore));
    const newReasonSet = new Set(hits.map((h) => h.scenarioKey + ':' + (h.subKey ?? '')));

    // 比对 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(hits.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;
    // head = 优先级最高的 hit(用于 plan 顶层字段:goal / recommendedRole / recommendedAt / recommendedChannel)
    const head = [...hits].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,
          status: 'active',
          reasons: {
            // W3 末改:plan_reasons 维度 = (plan, scenario, sub_key) — 每独立临床缺口一行,
            // 不再合并子规则(K08 缺牙 / K05 牙周 / K04 根管 是不同治疗体系,合并 reason
            // 长且生硬、evidence 粒度丢)。话术 / 摘要 / 执行回写仍 plan 级(展示细 / 触达粗)。
            // UNIQUE(plan_id, scenario, sub_key)约束保证同子规则不重复。
            createMany: {
              data: hits.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';
  }

  /// 子场景 → 生命周期类型(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;
  durationMs: number;
}
