/**
 * CronsService —— Agent 定时任务。
 *
 * cron 表达式 + sessionId + prompt 入库；@Cron 心跳每 30s 扫到期任务，并行 fire
 * 每个 task 走 messages.runTurn 完整 LLM pipeline，更新 nextRunAt + 计数。
 *
 * 配额：per-user 10 / per-org 50。时区 v1=UTC（LLM 自行从中文 → cron 时用 UTC）。
 */

import { Injectable, BadRequestException, ForbiddenException, Logger, Inject, forwardRef } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import type { AgentCron } from '@prisma/client';
import * as cronParser from 'cron-parser';
import { AgentMessagesService } from './messages.service';
import { assertOwn } from '../utils/ownership.util';
import { assertNonEmptyString } from '../utils/validation.util';

export interface CreateCronInput {
  organizationId: string;
  createdById: string;
  sessionId: string;
  name: string;
  cronExpr: string;
  prompt: string;
}

export interface UpdateCronInput {
  name?: string;
  cronExpr?: string;
  prompt?: string;
  enabled?: boolean;
}

const PER_USER_LIMIT = 10;
const PER_ORG_LIMIT = 50;
const NAME_MAX = 120;
const MAX_PROMPT_LEN = 4000;

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

  constructor(
    private readonly prisma: PrismaService,
    @Inject(forwardRef(() => AgentMessagesService))
    private readonly messagesService: AgentMessagesService,
  ) {}

  async list(organizationId: string, createdById: string): Promise<AgentCron[]> {
    return this.prisma.agentCron.findMany({
      where: { organizationId, createdById },
      orderBy: { updatedAt: 'desc' },
    });
  }

  async create(input: CreateCronInput): Promise<AgentCron> {
    const name = assertNonEmptyString(input.name, 'name', NAME_MAX);
    const prompt = assertNonEmptyString(input.prompt, 'prompt', MAX_PROMPT_LEN);
    const nextRunAt = this.computeNext(input.cronExpr);

    const [userCount, orgCount] = await Promise.all([
      this.prisma.agentCron.count({ where: { organizationId: input.organizationId, createdById: input.createdById } }),
      this.prisma.agentCron.count({ where: { organizationId: input.organizationId } }),
    ]);
    if (userCount >= PER_USER_LIMIT) {
      throw new ForbiddenException(`per-user cron limit reached (${PER_USER_LIMIT})`);
    }
    if (orgCount >= PER_ORG_LIMIT) {
      throw new ForbiddenException(`per-organization cron limit reached (${PER_ORG_LIMIT})`);
    }

    const session = await this.prisma.agentSession.findUnique({ where: { id: input.sessionId } });
    if (!session) throw new BadRequestException(`session ${input.sessionId} not found`);
    if (session.organizationId !== input.organizationId) {
      throw new ForbiddenException('cron sessionId crosses organization');
    }

    return this.prisma.agentCron.create({
      data: {
        organizationId: input.organizationId,
        createdById: input.createdById,
        sessionId: input.sessionId,
        name,
        cronExpr: input.cronExpr.trim(),
        prompt,
        enabled: true,
        nextRunAt,
      },
    });
  }

  @SkipAssertAccess('入口即 assertOwnCron，写动作前已校验 orgId + ownership')
  async update(
    id: string,
    organizationId: string,
    createdById: string,
    patch: UpdateCronInput,
  ): Promise<AgentCron> {
    const existing = await this.assertOwnCron(id, organizationId, createdById);
    let name: string | undefined;
    let prompt: string | undefined;
    if (patch.name !== undefined) name = assertNonEmptyString(patch.name, 'name', NAME_MAX);
    if (patch.prompt !== undefined) prompt = assertNonEmptyString(patch.prompt, 'prompt', MAX_PROMPT_LEN);
    let nextRunAt = existing.nextRunAt;
    if (patch.cronExpr !== undefined && patch.cronExpr !== existing.cronExpr) {
      nextRunAt = this.computeNext(patch.cronExpr);
    }
    return this.prisma.agentCron.update({
      where: { id },
      data: {
        ...(name !== undefined ? { name } : {}),
        ...(patch.cronExpr !== undefined ? { cronExpr: patch.cronExpr.trim(), nextRunAt } : {}),
        ...(prompt !== undefined ? { prompt } : {}),
        ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
      },
    });
  }

  @SkipAssertAccess('入口即 assertOwnCron，写动作前已校验')
  async remove(id: string, organizationId: string, createdById: string): Promise<{ ok: true }> {
    await this.assertOwnCron(id, organizationId, createdById);
    await this.prisma.agentCron.delete({ where: { id } });
    return { ok: true };
  }

  /**
   * 心跳 30s 一次。due 列表并行 fire，避免一个慢 LLM 阻塞后续。
   * 失败：写 lastError + failCount + 仍推 nextRunAt（避免一直 retry 同一个时间点）。
   */
  @SkipAssertAccess('内部 scheduler，无 user 上下文；fire 时走 messages.runTurn 自身已含 org 隔离')
  @Cron('*/30 * * * * *')
  async tick(): Promise<void> {
    const now = new Date();
    const due = await this.prisma.agentCron.findMany({
      where: { enabled: true, nextRunAt: { lte: now } },
      take: 20,
      orderBy: { nextRunAt: 'asc' },
    });
    if (due.length === 0) return;
    this.logger.log(`tick: firing ${due.length} cron(s) in parallel`);
    await Promise.allSettled(due.map((cron) => this.fireOne(cron)));
  }

  @SkipAssertAccess('private 内部方法，仅由 tick 调用；fire 时走 messages.runTurn 自身已含 org 隔离')
  private async fireOne(cron: AgentCron): Promise<void> {
    let next: Date;
    try {
      next = this.computeNext(cron.cronExpr);
    } catch (err) {
      this.logger.warn(`cron ${cron.id} invalid expr "${cron.cronExpr}": disabled`);
      await this.prisma.agentCron.update({
        where: { id: cron.id },
        data: { enabled: false, lastError: `invalid cron: ${(err as Error).message}` },
      });
      return;
    }
    // 预先推 nextRunAt：即使 LLM 跑很久，下次心跳不会重复触发
    await this.prisma.agentCron.update({
      where: { id: cron.id },
      data: { nextRunAt: next, lastRunAt: new Date() },
    });

    try {
      await this.messagesService.runTurn({
        sessionId: cron.sessionId,
        organizationId: cron.organizationId,
        userId: cron.createdById,
        prompt: cron.prompt,
        surface: 'web',
      });
      await this.prisma.agentCron.update({
        where: { id: cron.id },
        data: { runCount: { increment: 1 }, lastError: null },
      });
    } catch (err) {
      const msg = (err as Error).message.slice(0, 500);
      this.logger.warn(`cron ${cron.id} fire failed: ${msg}`);
      await this.prisma.agentCron.update({
        where: { id: cron.id },
        data: { runCount: { increment: 1 }, failCount: { increment: 1 }, lastError: msg },
      });
    }
  }

  private computeNext(expr: string): Date {
    try {
      // utc:true 对齐 docs/modules/agent/07-api.md 「v1=UTC，LLM 自行从中文 → cron 时用 UTC」
      // 容器不一定是 UTC，按服务器本地时区解释会跟 LLM 生成的 cron 偏移
      const interval = cronParser.parseExpression(expr.trim(), { utc: true });
      return interval.next().toDate();
    } catch (err) {
      throw new BadRequestException(`invalid cron expression: ${(err as Error).message}`);
    }
  }

  private async assertOwnCron(id: string, organizationId: string, createdById: string): Promise<AgentCron> {
    const c = await this.prisma.agentCron.findUnique({ where: { id } });
    return assertOwn(c, organizationId, createdById, { entityLabel: 'cron' });
  }
}
