import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PlanEventType } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';
import { recordPlanEvent, computeHeldSeconds } from './plan-event.recorder';

/**
 * RecycleSchedulerService — 召回工单超时自动回收
 *
 * 背景:assign 时锁定 recycleAt = assignedAt + RECYCLE_TIMEOUT_HOURS(默认 24h)。
 *   最初实现只写 deadline 没有执行者 —— 客服领了工单去休假 / 漏跟,plan 永远停在
 *   assigned,引擎 upsert 时 `skipped_assigned` 永久跳过,患者既不会被别人捡走也不更新。
 *   本服务是那个兜底执行者。
 *
 * ⭐ 2026-07:**默认关闭**(PAC_PLAN_AUTO_RECYCLE 未设或非 'on' 即不跑)。
 *   关闭理由(业务决定):
 *     1. 「认领」现在要当作「该患者已被客服处理」的口径用于统计。自动回收会把
 *        assignee_user_id / assigned_at **就地清空**,认领痕迹消失 ——
 *        生产两天内回收 32 单,这些"接手了但没继续"的样本全部无法追溯。
 *     2. staff 角色**没有 PLAN_RECYCLE 权限**(leader 独占),所以自动回收曾是
 *        staff 认领后唯一的释放路径。关闭后需注意工单沉淀,见下方「关闭后的代价」。
 *
 *   代码保留而非删除:超时兜底的原始问题(领了不做 → 患者被锁死)依然成立,
 *   后续若改成更长超时(如 7d)或加"仅提醒不回收",打开开关即可。
 *
 * ⚠️ 关闭后的代价(运维需知):
 *   staff 认领后若不处理,plan 停在 assigned、离开召回池,**staff 自己无法返池**,
 *   只有 leader 能手动回收。按当前速率约每天沉淀 30 单。
 *   缓解选项(未做,需业务定):给 staff 自助返池权限(只能返自己的),或拉长超时后重开。
 *
 * 行为(开启时):每 10 分钟扫一遍,把"已分配 + 已过 recycleAt"的 plan 退回召回池,
 *   并为每一条写 PlanEventLog(event='auto_release'),使归属历史可追溯。
 */
@Injectable()
export class RecycleSchedulerService implements OnModuleInit {
  private readonly logger = new Logger(RecycleSchedulerService.name);

  constructor(private readonly prisma: PrismaService) {}

  /** 显式开启才跑 —— 默认关闭,避免"部署上去才发现认领记录又被抹了" */
  private get enabled(): boolean {
    return (process.env.PAC_PLAN_AUTO_RECYCLE ?? '').trim().toLowerCase() === 'on';
  }

  onModuleInit(): void {
    // 启动时把状态打出来 —— 这个开关会静默改变数据,不能靠猜
    this.logger.log(
      this.enabled
        ? '超时自动回收:已开启(PAC_PLAN_AUTO_RECYCLE=on),每 10 分钟扫一次'
        : '超时自动回收:已关闭(默认)。认领后仅能由 leader 手动返池;开启设 PAC_PLAN_AUTO_RECYCLE=on',
    );
  }

  @Cron(CronExpression.EVERY_10_MINUTES, { name: 'plan-recycle-stale' })
  async runRecycle(): Promise<void> {
    if (!this.enabled) return;

    const now = new Date();
    const due = await this.prisma.followupPlan.findMany({
      where: {
        status: 'assigned',
        recycleAt: { not: null, lt: now },
        // ⭐ 跳过仍在冷静期/约定回访中的 plan(snoozedUntil 在未来)。
        //   场景:客服约了 6/10 回访,plan snooze 到 6/10;在那之前**不能**因 24h 超时被回收,
        //   否则客服丢了已承诺的回访关系、6/10 一到还会被别人从池里捞走。
        //   回访日过后(snoozedUntil<=now)若仍未处理,才允许回收。
        OR: [{ snoozedUntil: null }, { snoozedUntil: { lte: now } }],
      },
      select: {
        id: true, hostId: true, tenantId: true, patientId: true,
        assigneeUserId: true, assignedAt: true,
      },
      // 单轮上限:防积压时一次性打爆事务;剩下的下一轮继续
      take: 500,
    });
    if (due.length === 0) return;

    // 逐条:要为每条留账本(持有时长只能在清空 assignedAt **之前**算出来)
    let recycled = 0;
    for (const p of due) {
      try {
        await this.prisma.$transaction(async (tx) => {
          await tx.followupPlan.updateMany({
            // 带 status 条件 → 并发下若已被人工返池/结案则本次不生效(幂等)
            where: { id: p.id, status: 'assigned' },
            data: { status: 'active', assigneeUserId: null, assignedAt: null, recycleAt: null },
          });
          await recordPlanEvent(tx, {
            hostId: p.hostId,
            tenantId: p.tenantId,
            planId: p.id,
            patientId: p.patientId,
            event: PlanEventType.AUTO_RELEASE,
            assigneeUserId: null,
            actorUserId: null, // 系统行为,无操作人
            heldSeconds: computeHeldSeconds(p.assignedAt, now),
            reason: 'timeout',
          });
        });
        recycled++;
      } catch (err) {
        this.logger.error(
          `auto-recycle 单条失败 plan=${p.id}: ${err instanceof Error ? err.message : err}`,
        );
      }
    }
    if (recycled > 0) {
      this.logger.log(`auto-recycle: ${recycled} 个超时未结案工单退回召回池(已记账本)`);
    }
  }
}
