import { Prisma } from '@prisma/client';
import { Logger } from '@nestjs/common';
import { currentTenant } from '../common/tenant-context';

/**
 * tenantGuardExtension — 纵深防御:对 scoped 模型的读查询**自动注入 tenantId**(+ source_unit)。
 *
 * 仅当请求级租户上下文存在(AsyncLocalStorage,见 tenant-context.ts)时生效;
 * 系统任务(cold-import / 重算 / scheduler)无上下文 → 不注入,照常全量跑。
 *
 * 只拦**列表/聚合类读**(findMany/findFirst/count/aggregate/groupBy):
 *   - findUnique 不拦 —— 其 where 是唯一选择器,注入 tenantId 会破坏唯一约束;
 *     by-id 取数由 service 显式守卫(plan/patient 已加 sourceUnit 校验)。
 *   - 写操作不拦 —— 写路径都在系统任务或显式 service,且写错 tenant 不是泄漏。
 *   - $queryRaw 不经扩展 —— 召回引擎原生 SQL 自带「①隔离闸」,且是系统任务无上下文。
 *
 * 注入是 **AND 叠加**:service 已带 tenantId → 幂等;漏了 → 这里兜住。
 */

const logger = new Logger('TenantGuard');

/**
 * 带 tenant_id 的 16 张表(模型名)。
 *
 * ⚠️ 新建带 tenant_id 的表时**必须登记到这里** —— 漏了不会报错,只是少一层兜底,
 * 而这一层正是"service 忘了加 tenantId"时唯一还站着的东西。
 * 2026-08 补登 PlanEventLog + PlanAssignment:前者此前只写不读,分配的批次跟踪
 * 是它的第一个读场景 —— 正好是纵深防御该兜底的时候。
 */
const SCOPED_MODELS = new Set<string>([
  'Patient',
  'PatientTransaction',
  'PatientFact',
  'PatientReturnVisit',
  'PatientRelation',
  'Persona',
  'PersonaRecomputeLog',
  'FollowupPlan',
  'PlanExecution',
  'PlanGenerationLog',
  'PlanScript',
  'PlanSummary',
  'PlanEventLog',
  'PlanAssignment',
  'AgentInvocation',
  'SyncLog',
]);

/// ⛔ PlanEventLog / PlanAssignment **不进** SOURCE_UNIT_MODELS —— 两表都没有 source_unit 列,
///   加进去会让每次查询都注入一个不存在的字段(Prisma 直接抛错)。
///   品牌隔离对它们由 scope.clinicIds + 关联 plan/patient 覆盖。
/** 同时带 source_unit 列的模型(品牌粒度过滤可直接打在自身)。 */
const SOURCE_UNIT_MODELS = new Set<string>(['Patient', 'PatientFact', 'PatientReturnVisit']);

const READ_OPS = new Set<string>(['findMany', 'findFirst', 'findFirstOrThrow', 'count', 'aggregate', 'groupBy']);

/** warn = 只告警不改;enforce = 注入(默认)。env PAC_TENANT_GUARD=warn 可切观察模式。 */
const MODE = process.env.PAC_TENANT_GUARD === 'warn' ? 'warn' : 'enforce';

export const tenantGuardExtension = Prisma.defineExtension({
  name: 'tenant-guard',
  query: {
    $allModels: {
      async $allOperations({ model, operation, args, query }) {
        const ctx = currentTenant();
        if (!ctx || !SCOPED_MODELS.has(model) || !READ_OPS.has(operation)) {
          return query(args);
        }
        const a = args as { where?: Record<string, unknown> };
        const hasTenant = a.where && 'tenantId' in a.where;
        if (!hasTenant) {
          if (MODE === 'warn') {
            logger.warn(`[guard] ${model}.${operation} 缺 tenantId 过滤(请求上下文 tenant=${ctx.tenantId})— warn 模式不注入`);
          } else {
            a.where = { ...a.where, tenantId: ctx.tenantId };
          }
        }
        // 品牌粒度:有 source_unit 列的模型 + 用户受限品牌 → 注入(空数组=不限,集团级角色)
        if (MODE === 'enforce' && SOURCE_UNIT_MODELS.has(model) && ctx.sourceUnits.length > 0) {
          if (!(a.where && 'sourceUnit' in a.where)) {
            a.where = { ...a.where, sourceUnit: { in: ctx.sourceUnits } };
          }
        }
        return query(args);
      },
    },
  },
});
