import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import type { ActivityItem, ActivityStatsQuery, ActivityStatsResponse } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';
import type { AccessTokenPayload } from '@pac/types';

/// 单条 payload 体积上限。超了截断而不是拒收 —— 埋点是旁路,不该因为一个大字段
/// 让前端拿到 4xx 然后（多半）默默吞掉,那样丢的是整条行为记录。
const PAYLOAD_MAX_BYTES = 8 * 1024;

@Injectable()
export class ActivityService {
  private readonly logger = new Logger(ActivityService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * 落埋点。**永不抛** —— 调用方一律 fire-and-forget。
   *
   * ⚠️ 身份(userId/role/orgScope/host/tenant)全部取自 `user`(JWT 解出来的),
   *    `items` 里只有 surface/action/payload。前端伪造身份没有入口。
   */
  async track(
    user: AccessTokenPayload,
    items: ActivityItem[],
    ctx?: { hostOrigin?: string | null; sessionId?: string | null },
  ): Promise<number> {
    if (!items.length) return 0;
    try {
      const rows: Prisma.UserActivityCreateManyInput[] = items.map((it) => ({
        hostId: user.hostId,
        tenantId: user.tenantId,
        surface: it.surface,
        action: it.action,
        userId: user.sub,
        role: user.role,
        orgScope: user.orgScope ?? [],
        hostOrigin: ctx?.hostOrigin ?? null,
        sessionId: ctx?.sessionId ?? null,
        payload: this.clampPayload(it.payload),
        occurredAt: it.occurredAt ? new Date(it.occurredAt) : null,
      }));
      const r = await this.prisma.userActivity.createMany({ data: rows });
      return r.count;
    } catch (e) {
      // 埋点挂了不能影响主流程(主管点不了确认比少一条 PV 严重得多)
      this.logger.warn(`[activity] 落库失败(已吞): ${e instanceof Error ? e.message : String(e)}`);
      return 0;
    }
  }

  /**
   * 服务端自己落的埋点(登录审计等)—— 没有 JWT 上下文,身份是刚签发出去的那份。
   * 同样永不抛。
   */
  async trackServerSide(input: {
    hostId: string;
    tenantId: string;
    surface: string;
    action: string;
    userId: string;
    role: string;
    orgScope: string[];
    hostOrigin?: string | null;
    payload?: Record<string, unknown>;
  }): Promise<void> {
    try {
      await this.prisma.userActivity.create({
        data: {
          hostId: input.hostId,
          tenantId: input.tenantId,
          surface: input.surface,
          action: input.action,
          userId: input.userId,
          role: input.role,
          orgScope: input.orgScope ?? [],
          hostOrigin: input.hostOrigin ?? null,
          payload: this.clampPayload(input.payload),
        },
      });
    } catch (e) {
      this.logger.warn(`[activity] 服务端埋点失败(已吞): ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  /**
   * PV/UV + 漏斗。按 (day, surface, action) 聚合。
   * ⚠️ 时间轴用 `created_at`(服务端时刻),⛔ 不用 occurred_at —— 客户端时钟不可信。
   */
  async stats(
    scope: { hostId: string; tenantId: string },
    q: ActivityStatsQuery,
  ): Promise<ActivityStatsResponse> {
    const from = q.from ? new Date(`${q.from}T00:00:00+08:00`) : new Date(Date.now() - 30 * 864e5);
    const to = q.to ? new Date(`${q.to}T23:59:59+08:00`) : new Date();
    const rows = await this.prisma.$queryRaw<
      Array<{ day: string; surface: string; action: string; pv: number; uv: number; roles: string[] }>
    >`
      SELECT to_char(a.created_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS "day",
             a.surface                                   AS "surface",
             a.action                                    AS "action",
             COUNT(*)::int                               AS "pv",
             COUNT(DISTINCT a.user_id)::int              AS "uv",
             array_agg(DISTINCT a.role)                  AS "roles"
      FROM user_activities a
      WHERE a.host_id = ${scope.hostId}::uuid
        AND a.tenant_id = ${scope.tenantId}
        AND a.created_at >= ${from}
        AND a.created_at <= ${to}
        ${q.surface ? Prisma.sql`AND a.surface = ${q.surface}` : Prisma.empty}
      GROUP BY 1, 2, 3
      ORDER BY 1 DESC, 2, 3
    `;
    return { rows };
  }

  /// payload 超限就换成一条自述的占位,别把原值塞进去撑爆行
  private clampPayload(p: unknown): Prisma.InputJsonValue | undefined {
    if (p == null) return undefined;
    const s = JSON.stringify(p);
    if (s.length <= PAYLOAD_MAX_BYTES) return p as Prisma.InputJsonValue;
    return { _truncated: true, _bytes: s.length } as Prisma.InputJsonValue;
  }
}
