// aislop-ignore-file: duplicate-block -- 三个 SSE 生成入口共用同构的 runGenerationAndRelease 调用
import {
  BadRequestException,
  NotFoundException,
} from '@nestjs/common';
import { LessThan, LessThanOrEqual, MoreThan } from 'typeorm';
import type {
  AiChatServiceContext,
  AiSseEmitter,
  ModelContentPart,
  ModelMessage,
} from './ai-chat.types';
import {
  DEFAULT_TITLE,
  MAX_HISTORY_MESSAGES,
  SYSTEM_PROMPT,
} from './ai-chat.types';
import { AiMessage } from './entities';
import type { AuthenticatedUser } from '../authorization';
import {
  a2uiReviewSubmitInfo,
  a2uiSubmitInfo,
  persistExchange,
  runGenerationAndRelease,
} from './ai-chat.submissions';

export async function streamMessage(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  conversationId: number,
  dto: { message: string; attachmentIds?: number[]; clientRequestId: string; skillKey?: string | null; reasoningEffort?: string | null },
  signal: AbortSignal,
  emit: AiSseEmitter,
  onReady: () => void,
): Promise<void> {
  const conversation = await context.requireOwnedConversation(user.id, conversationId);
  const effectiveSkillKey = conversation.lockedSkillKey || dto.skillKey || null;
  context.assertSkillAvailable(user, effectiveSkillKey);
  const attachments = await context.attachmentService.requireReadyOwned(
    user.id,
    dto.attachmentIds ?? [],
  );
  const config = await context.configService.getRuntimeConfig();
  const focusContent = await context.buildUserContent(
    dto.message.trim(),
    attachments,
    config.supportsVision,
  );

  await context.acquireConversation(conversationId);
  try {
    const saved = await context.dataSource.transaction(async (manager) =>
      persistExchange(
        context,
        manager,
        conversation,
        user.id,
        dto.message.trim(),
        dto.clientRequestId,
        effectiveSkillKey,
        undefined,
        attachments,
        conversation.title === DEFAULT_TITLE ? context.titleFromMessage(dto.message) : undefined,
      ),
    );

    await runGenerationAndRelease(context, {
      user,
      conversation,
      userMessage: { ...saved.userMessage, attachments },
      assistant: saved.assistantMessage,
      clientRequestId: dto.clientRequestId,
      effectiveSkillKey,
      focusContent,
      reasoningEffort: dto.reasoningEffort ?? null,
      signal,
      emit,
      onReady,
    }, conversationId);
  } finally {
    context.activeConversations.delete(conversationId);
  }
}

export async function regenerateMessage(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  conversationId: number,
  assistantMessageId: number,
  clientRequestId: string,
  reasoningEffort: string | null | undefined,
  signal: AbortSignal,
  emit: AiSseEmitter,
  onReady: () => void,
): Promise<void> {
  const conversation = await context.requireOwnedConversation(user.id, conversationId);
  const target = await context.messages.findOne({
    where: { id: assistantMessageId, conversationId, role: 'assistant' },
  });
  if (!target) throw new NotFoundException('回答不存在');
  const userMessage = target.replyToMessageId
    ? await context.messages.findOne({
        where: { id: target.replyToMessageId, conversationId, role: 'user' },
        relations: { attachments: true },
      })
    : await context.messages.findOne({
        where: { conversationId, role: 'user', id: LessThan(target.id) },
        relations: { attachments: true },
        order: { id: 'DESC' },
      });
  if (!userMessage) throw new NotFoundException('原问题不存在');

  const effectiveSkillKey =
    conversation.lockedSkillKey || context.metadataSkillKey(target.metadata) || null;
  context.assertSkillAvailable(user, effectiveSkillKey);
  const config = await context.configService.getRuntimeConfig();
  const focusContent = await context.buildUserContent(
    userMessage.content,
    userMessage.attachments ?? [],
    config.supportsVision,
  );

  await context.acquireConversation(conversationId);
  try {
    const assistant = await context.messages.save(
      context.messages.create({
        conversationId,
        role: 'assistant',
        content: '',
        reasoningContent: null,
        status: 'pending',
        errorCode: null,
        replyToMessageId: userMessage.id,
        metadata: {
          clientRequestId,
          skillKey: effectiveSkillKey,
          regeneratedFromMessageId: target.id,
        },
      }),
    );
    await runGenerationAndRelease(context, {
      user,
      conversation,
      userMessage,
      assistant,
      clientRequestId,
      effectiveSkillKey,
      focusContent,
      reasoningEffort: reasoningEffort ?? null,
      signal,
      emit,
      onReady,
    }, conversationId);
  } finally {
    context.activeConversations.delete(conversationId);
  }
}

export async function editMessage(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  conversationId: number,
  messageId: number,
  dto: { content: string; clientRequestId: string; reasoningEffort?: string | null },
  signal: AbortSignal,
  emit: AiSseEmitter,
  onReady: () => void,
): Promise<void> {
  const conversation = await context.requireOwnedConversation(user.id, conversationId);
  const target = await context.messages.findOne({
    where: { id: messageId, conversationId, role: 'user' },
    relations: { attachments: true },
  });
  if (!target) throw new NotFoundException('消息不存在或不可编辑');
  if (target.status !== 'completed') {
    throw new BadRequestException('仅可编辑已发送完成的消息');
  }
  if (a2uiSubmitInfo(target.metadata) || a2uiReviewSubmitInfo(target.metadata)) {
    throw new BadRequestException('系统确认消息不可编辑');
  }
  const content = dto.content.trim();
  if (!content) throw new BadRequestException('消息内容不能为空');

  const effectiveSkillKey =
    conversation.lockedSkillKey || context.metadataSkillKey(target.metadata) || null;
  context.assertSkillAvailable(user, effectiveSkillKey);
  const config = await context.configService.getRuntimeConfig();
  const focusContent = await context.buildUserContent(
    content,
    target.attachments ?? [],
    config.supportsVision,
  );

  await context.acquireConversation(conversationId);
  try {
    const now = new Date();
    const oldTitleHint = context.titleFromMessage(target.content);
    const { assistant, orphanAttachmentIds } = await context.dataSource.transaction(
      async (manager) => {
        await manager.update(
          AiMessage,
          { id: target.id, conversationId },
          {
            content,
            metadata: {
              ...target.metadata,
              clientRequestId: dto.clientRequestId,
              editedAt: now.toISOString(),
            },
          },
        );

        const laterMessages = await manager.find(AiMessage, {
          where: { conversationId, id: MoreThan(target.id) },
          select: { id: true },
        });
        const laterIds = laterMessages.map((item) => item.id);
        let orphanAttachmentIds: number[] = [];
        if (laterIds.length > 0) {
          const attachmentRows = await manager
            .createQueryBuilder(AiMessage, 'message')
            .innerJoin('message.attachments', 'attachment')
            .where('message.id IN (:...ids)', { ids: laterIds })
            .select('attachment.id', 'id')
            .getRawMany<{ id: number }>();
          orphanAttachmentIds = attachmentRows.map((item) => Number(item.id));
          await manager
            .createQueryBuilder()
            .delete()
            .from('ai_message_attachments')
            .where('message_id IN (:...ids)', { ids: laterIds })
            .execute();
          await manager.delete(AiMessage, laterIds);
        }

        const assistantMessage = await manager.save(
          manager.create(AiMessage, {
            conversationId,
            role: 'assistant',
            content: '',
            reasoningContent: null,
            status: 'pending',
            errorCode: null,
            replyToMessageId: target.id,
            metadata: {
              clientRequestId: dto.clientRequestId,
              skillKey: effectiveSkillKey,
              editedFromMessageId: target.id,
            },
          }),
        );
        return { assistant: assistantMessage, orphanAttachmentIds };
      },
    );

    await context.conversations.update(
      { id: conversationId, userId: user.id },
      {
        lastMessageAt: now,
        ...(conversation.title === oldTitleHint ? { title: context.titleFromMessage(content) } : {}),
      },
    );
    await context.attachmentService.removeOrphans(user.id, orphanAttachmentIds);

    await runGenerationAndRelease(context, {
      user,
      conversation,
      userMessage: { ...target, content },
      assistant,
      clientRequestId: dto.clientRequestId,
      effectiveSkillKey,
      focusContent,
      reasoningEffort: dto.reasoningEffort ?? null,
      signal,
      emit,
      onReady,
    }, conversationId);
  } finally {
    context.activeConversations.delete(conversationId);
  }
}

export async function buildContext(
  context: AiChatServiceContext,
  conversationId: number,
  focusUserMessageId: number,
  focusContent: string | ModelContentPart[],
  skillKey: string | null,
  supportsVision: boolean,
): Promise<ModelMessage[]> {
  const history = await context.messages.find({
    where: { conversationId, id: LessThanOrEqual(focusUserMessageId) },
    relations: { attachments: true },
    order: { createdAt: 'DESC', id: 'DESC' },
    take: MAX_HISTORY_MESSAGES + 1,
  });
  const systemPrompt = skillKey
    ? `${SYSTEM_PROMPT}\n当前会话已锁定技能：${skillKey}。只能调用该技能内的工具。`
    : SYSTEM_PROMPT;
  const selected: ModelMessage[] = [];
  for (const message of history) {
    if (message.status !== 'completed') continue;
    const content =
      message.id === focusUserMessageId
        ? focusContent
        : message.role === 'user' && message.attachments?.length
          ? await context.buildUserContent(message.content, message.attachments, supportsVision)
          : message.content;
    selected.push({ role: message.role, content } as ModelMessage);
    if (selected.length >= MAX_HISTORY_MESSAGES) break;
  }
  return [{ role: 'system', content: systemPrompt }, ...selected.reverse()];
}

export async function buildUserContent(
  context: AiChatServiceContext,
  text: string,
  attachments: any[],
  supportsVision: boolean,
): Promise<string | ModelContentPart[]> {
  if (!attachments.length) return text;
  const parts = await context.attachmentService.toModelParts(attachments, supportsVision);
  const textSections = [text];
  const contentParts: ModelContentPart[] = [];
  for (const part of parts) {
    if (part.text !== undefined) {
      textSections.push(
        `\n\n[附件：${part.attachment.originalName}（附件ID=${part.attachment.id}）]\n${part.text}`,
      );
    } else if (part.imageDataUrl) {
      textSections.push(`\n\n[图片附件：${part.attachment.originalName}]`);
      contentParts.push({ type: 'image_url', image_url: { url: part.imageDataUrl } });
    }
  }
  const combinedText = textSections.join('');
  if (!contentParts.length) return combinedText;
  return [{ type: 'text', text: combinedText }, ...contentParts];
}
