import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../prisma/prisma.service';
import type { AppConfig } from '../config/configuration';

/**
 * InvocationRetentionService —— 定期把 `agent_invocations` 里**过期的肥字段**清掉，只留元数据。
 *
 * ═══ 为什么必须有 ═══════════════════════════════════════════════
 * schema 上早就写着「成功调用 N 天后清 inputSnapshot 仅保留元数据」，但这个任务一直不存在。
 * 助手接进来之后写入频率上一个量级（每轮对话一行），⚠️ 而测试机盘常年 90%+、
 * PG 卷已占 75 G —— **不清就是把一个已知的紧张问题变成事故**。
 *
 * ═══ 清什么、留什么 ════════════════════════════════════════════
 * 清：inputSnapshot（置为 {"pruned":true}）· promptTemplate · systemPrompt · outputText
 *     —— 这四样占了一行 95% 以上的字节。
 * 留：token / 成本 / 延迟 / status / callKey / promptVersion / modelName / judge / userFeedback
 *     —— 仪表盘、成本聚合、通过率统计要的全在这些列上，⛔ 一个都不动。
 *
 * ⚠️ **失败的留更久**（默认 3 倍）：失败行的输入正是排查时唯一能看的东西。
 * ⚠️ `inputSnapshot` 是 NOT NULL 列 ⇒ 只能覆盖成占位对象，⛔ 不能置 null。
 * ⚠️ ⛔ 不删行：元数据行 ~1 KB，留着才有长期的成本 / 通过率曲线；肥字段清掉后
 *   增长量已经可控（按 1800 次/天算约 0.65 GB/年）。
 */
@Injectable()
export class InvocationRetentionService {
  private readonly logger = new Logger(InvocationRetentionService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService<AppConfig, true>,
  ) {}

  /** 每天 04:00（沪）—— 避开 DW 08:00 落库与随后的增量摄入。 */
  @Cron(process.env.PAC_INVOCATION_RETENTION_CRON || '0 4 * * *', {
    name: 'invocation-retention',
    timeZone: 'Asia/Shanghai',
  })
  async prune(): Promise<void> {
    const days = this.config.get('ai', { infer: true }).invocationRetentionDays;
    if (days <= 0) {
      this.logger.log('保留期配置为 0 —— 跳过清理（AI_INVOCATION_RETENTION_DAYS）');
      return;
    }
    const okCount = await this.pruneOlderThan(days, ['succeeded', 'cached']);
    // 失败的留 3 倍时长:排查时唯一能看的就是它的输入
    const failCount = await this.pruneOlderThan(days * 3, ['failed']);
    this.logger.log(
      `invocation 清理完成：成功/缓存 ${okCount} 行（>${days} 天）· 失败 ${failCount} 行（>${days * 3} 天）`,
    );
  }

  /**
   * 清一批。⚠️ `updatedAt` 是 `@updatedAt` 列，这次 update 会把它刷新 ——
   * 所以判据用 `startedAt`，⛔ 不能用 updatedAt（否则清过的行永远追不上闸门，每天重清一遍）。
   */
  private async pruneOlderThan(days: number, statuses: string[]): Promise<number> {
    const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
    const res = await this.prisma.agentInvocation.updateMany({
      where: {
        startedAt: { lt: cutoff },
        status: { in: statuses },
        // ⭐ 幂等闸：已经清过的不再进来（否则每天全表扫一遍已清的行）
        NOT: { inputSnapshot: { equals: { pruned: true } } },
      },
      data: {
        inputSnapshot: { pruned: true },
        promptTemplate: null,
        systemPrompt: null,
        outputText: null,
      },
    });
    return res.count;
  }
}
