import { z } from 'zod';

// =============================================================
// User Activity — 通用行为埋点  (POST /pac/v1/activities)
// =============================================================
//
// 一张表回答三类问题,别再为每个新页面建表:
//   ① 谁在用      —— PV/UV(按 surface 分组、按天 distinct userId)
//   ② 卡在哪一步  —— 漏斗(同一 surface 下 action 的逐级衰减)
//   ③ 谁什么时候以什么角色进来的 —— 登录审计(surface=auth)
//
// ── 设计纪律(改之前先读) ────────────────────────────────────
// ⛔ **身份一律从 JWT 取,不信客户端**。请求体里没有 userId / role / orgScope 字段,
//    前端想伪造也没有入口。唯一例外是 surface=auth 的换票,那是服务端自己落的。
// ⛔ **surface / action 在库里是 text,不是 PG enum**。加一个新埋点不该要一次迁移;
//    枚举只在这里(TS 侧)约束,库里保持宽松 —— 老数据里的历史值也不会因为改枚举变非法。
// ⚠️ **payload 只放"这次行为的参数",不放业务实体快照**。放快照会让这张表变成一份
//    永远对不上的影子数据(业务表改了它不改),而且体积失控。
// ⚠️ 埋点是**旁路**:后端 fire-and-forget,写失败只落日志不影响主流程 —— 埋点挂了
//    不能让主管点不了确认。

/// 埋点面(页面 / 模块)。加新页面就加一个值,不用迁移。
export const ActivitySurface = {
  /// 主管工作台(矩阵 + 分配 + 团队跟踪)
  SUPERVISOR_WORKBENCH: 'supervisor_workbench',
  /// 客服执行页(单详情)—— 注:单维度的"看过没"仍走 plan_event_logs.view,
  /// 这里只记页面级进入,两者口径不同别混用。
  STAFF_EXECUTION: 'staff_execution',
  /// 召回池列表
  PLAN_POOL: 'plan_pool',
  /// 认证(换票 / 换 code)—— 服务端落,登录审计就是这一类
  AUTH: 'auth',
} as const;
export type ActivitySurface = (typeof ActivitySurface)[keyof typeof ActivitySurface];

/// 行为。**同一个 action 可以出现在不同 surface 下**(open / view 都是通用动作),
/// 漏斗永远按 (surface, action) 这一对来看,别只按 action 聚合。
export const ActivityAction = {
  /// 进入该面(PV 的分子)
  OPEN: 'open',
  /// 主管点了矩阵格子(payload: { category, bucket, count })
  MATRIX_CELL: 'matrix_cell',
  /// 助手把确认单摆出来了(payload: { candidateTotal, batchSize })
  PROPOSAL_SHOWN: 'proposal_shown',
  /// 主管点了确认,分配真的落库(payload: { assignmentId, items })
  ASSIGN_CONFIRM: 'assign_confirm',
  /// 摆出来了但放弃(payload: { reason? })
  PROPOSAL_ABANDON: 'proposal_abandon',
  /// 宿主换票(surface=auth,服务端落)
  TOKEN_EXCHANGE: 'token_exchange',
  /// 一次性 code 换票成功(surface=auth,服务端落)
  CODE_EXCHANGE: 'code_exchange',
} as const;
export type ActivityAction = (typeof ActivityAction)[keyof typeof ActivityAction];

/// 单条埋点。⛔ 没有身份字段 —— 见文件头注释。
export const ActivityItemSchema = z.strictObject({
  surface: z.string().min(1).max(64),
  action: z.string().min(1).max(64),
  /// 这次行为的参数。⛔ 别塞业务实体快照;单条 8KB 上限由后端兜。
  payload: z.record(z.string(), z.unknown()).optional(),
  /// 客户端发生时刻(ISO)。缺省用服务端收到的时刻 —— 客户端时钟不可信,
  /// 所以**两个时刻都存**:排序统计用服务端的,排查前端问题看客户端的。
  occurredAt: z.string().datetime().optional(),
});
export type ActivityItem = z.infer<typeof ActivityItemSchema>;

/// 批量上报 —— 前端一次交互可能产生好几条,合并成一个请求少打扰主流程。
export const TrackActivityRequestSchema = z.strictObject({
  items: z.array(ActivityItemSchema).min(1).max(50),
});
export type TrackActivityRequest = z.infer<typeof TrackActivityRequestSchema>;

export const TrackActivityResponseSchema = z.object({
  accepted: z.number().int(),
});
export type TrackActivityResponse = z.infer<typeof TrackActivityResponseSchema>;

// ── 查询侧(admin)──────────────────────────────────────────
export const ActivityStatsQuerySchema = z.strictObject({
  /// 起始日(含),YYYY-MM-DD;缺省 = 近 30 天
  from: z.string().optional(),
  to: z.string().optional(),
  surface: z.string().optional(),
});
export type ActivityStatsQuery = z.infer<typeof ActivityStatsQuerySchema>;

export const ActivityStatRowSchema = z.object({
  day: z.string(),
  surface: z.string(),
  action: z.string(),
  /// PV = 事件条数
  pv: z.number().int(),
  /// UV = 去重用户数
  uv: z.number().int(),
  /// 出现过的角色。⚠️ 是 `string[]` 不是 UserRole[] —— 库里 role 存的是**快照文本**,
  /// 角色由宿主换票时下发,将来宿主多一个角色、或 PAC 改了枚举,老行仍是老值。
  /// 拿枚举去卡会让历史数据在读取时校验失败(写得进读不出,最难查的一类 bug)。
  roles: z.array(z.string()),
});
export type ActivityStatRow = z.infer<typeof ActivityStatRowSchema>;

export const ActivityStatsResponseSchema = z.object({
  rows: z.array(ActivityStatRowSchema),
});
export type ActivityStatsResponse = z.infer<typeof ActivityStatsResponseSchema>;
