import type { Prisma } from '@prisma/client';
import { PlanEventType } from '@pac/types';

/**
 * PlanEventLog 的**唯一写入口**。
 *
 * ── 为什么是应用层写,而不是数据库触发器 ──
 *   1. **触发器拿不到操作人**。DB 只看得见"某行变了",而 actor_user_id 在请求上下文里
 *      (JWT sub)。触发器要拿只能靠 session 变量(SET LOCAL app.user_id),那还是得应用
 *      配合,只是把依赖变成隐式的、更容易漏。本项目的 AsyncLocalStorage(tenant-context.ts)
 *      目前也只装 hostId/tenantId/sourceUnits,**不含 user**。
 *   2. 拿不到 actor 就**分不出 claim / assign** —— 而这正是这张表最核心的价值。
 *   3. **触发器会被运维脚本误触发**。任何一句
 *      `UPDATE followup_plans SET assignee_user_id=NULL ...`(数据修复、回滚、清理测试数据)
 *      都会凭空造出一条假的 release 事件,污染统计。应用层写则只记真实业务动作。
 *   4. Prisma 不管触发器 —— 得走 raw SQL 迁移,schema 里看不见,漂移/reset 时容易丢。
 *
 * ── 应用层写的弱点(得认)──
 *   靠自觉:将来若有人新加一条直接改 followup_plans 归属的代码路径,不会自动落账。
 *   缓解:① 写入点收口到本函数(找调用方一眼看全)
 *        ② 必须在**同一个事务**里调 —— 状态变更与账本同生共死,不会只成一半
 *        ③ 事件类型走 PlanEventType 枚举,编译期拦拼写错误
 *
 * @param tx **事务客户端**(prisma.$transaction 的回调参数)。刻意不接受裸 PrismaService:
 *           账本与状态变更必须原子,分开写就可能只落一半。
 */

/**
 * 只取事务客户端里 planEventLog 这一块 —— 调用方传 $transaction 的 tx 即可;
 * 单测传 mock 时 `as unknown as PlanEventLogWriter` 断言。
 */
export type PlanEventLogWriter = Pick<Prisma.TransactionClient, 'planEventLog'>;

export interface PlanEventInput {
  hostId: string;
  tenantId: string;
  planId: string;
  patientId: string;
  event: PlanEventType;
  /** 变更后的归属人;释放类事件传 null */
  assigneeUserId?: string | null;
  /** 操作人;系统行为(auto_release)传 null */
  actorUserId?: string | null;
  /** 归属持续秒数 —— 见 computeHeldSeconds 的注释 */
  heldSeconds?: number | null;
  /** 简短原因(立柱,可直接过滤):auto_release='timeout';feedback='up'|'down' */
  reason?: string | null;
  /** 事件专属细节(不按它查询的内容,如反馈文字) */
  details?: Prisma.InputJsonObject | null;
}

export function recordPlanEvent(tx: PlanEventLogWriter, input: PlanEventInput): Promise<unknown> {
  return tx.planEventLog.create({
    data: {
      hostId: input.hostId,
      tenantId: input.tenantId,
      planId: input.planId,
      patientId: input.patientId,
      event: input.event,
      assigneeUserId: input.assigneeUserId ?? null,
      actorUserId: input.actorUserId ?? null,
      heldSeconds: input.heldSeconds ?? null,
      reason: input.reason ?? null,
      // undefined 才让 Prisma 落 NULL;传 null 会被当成 JSON null 值
      details: input.details ?? undefined,
    },
  });
}

/**
 * 算归属持续秒数。
 * ⭐ 必须在把 assigned_at 清空**之前**调 —— 释放后该列即为 null,事后再也算不出来。
 * 这是 heldSeconds 需要落库(而非查询时现算)的全部理由。
 */
export function computeHeldSeconds(assignedAt: Date | null | undefined, now: Date): number | null {
  if (assignedAt == null) return null;
  return Math.max(0, Math.round((now.getTime() - assignedAt.getTime()) / 1000));
}
