import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import type {
  AiChatServiceContext,
  PublicConversation,
} from './ai-chat.types';
import { DEFAULT_TITLE } from './ai-chat.types';
import { AiConversation, AiMessage } from './entities';
import type { AuthenticatedUser } from '../authorization';

export async function listConversations(
  context: AiChatServiceContext,
  userId: number,
): Promise<PublicConversation[]> {
  return context.conversations.find({
    where: { userId },
    select: ['id', 'title', 'lockedSkillKey', 'createdAt', 'updatedAt', 'lastMessageAt'],
    order: { lastMessageAt: 'DESC', updatedAt: 'DESC' },
  });
}

export async function createConversation(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  title?: string,
  lockedSkillKey?: string | null,
): Promise<PublicConversation> {
  assertSkillAvailable(context, user, lockedSkillKey);
  const entity = context.conversations.create({
    userId: user.id,
    title: normalizeTitle(context, title),
    lockedSkillKey: lockedSkillKey || null,
    lastMessageAt: null,
  });
  return context.conversations.save(entity);
}

export async function updateConversation(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  id: number,
  dto: { title?: string; lockedSkillKey?: string | null },
): Promise<PublicConversation> {
  const conversation = await requireOwnedConversation(context, user.id, id);
  if (dto.title !== undefined) conversation.title = normalizeTitle(context, dto.title);
  if (dto.lockedSkillKey !== undefined) {
    assertSkillAvailable(context, user, dto.lockedSkillKey);
    conversation.lockedSkillKey = dto.lockedSkillKey || null;
  }
  return context.conversations.save(conversation);
}

export async function deleteConversation(
  context: AiChatServiceContext,
  userId: number,
  id: number,
): Promise<void> {
  const conversation = await requireOwnedConversation(context, userId, id);
  if (context.activeConversations.has(id)) throw new ConflictException('该会话正在生成回答');
  const attachmentIds = await context.messages
    .createQueryBuilder('message')
    .innerJoin('message.attachments', 'attachment')
    .where('message.conversation_id = :id', { id })
    .select('attachment.id', 'id')
    .getRawMany<{ id: number }>();
  await context.conversations.remove(conversation);
  await context.attachmentService.removeOrphans(
    userId,
    attachmentIds.map((item) => Number(item.id)),
  );
}

export async function deleteAllConversations(
  context: AiChatServiceContext,
  userId: number,
): Promise<number> {
  const conversations = await context.conversations.find({ where: { userId } });
  if (conversations.some((item) => context.activeConversations.has(item.id))) {
    throw new ConflictException('存在正在生成的会话，请稍后再试');
  }
  if (conversations.length === 0) return 0;

  const attachmentIds = await context.messages
    .createQueryBuilder('message')
    .innerJoin('message.attachments', 'attachment')
    .where('message.conversation_id IN (:...ids)', {
      ids: conversations.map((item) => item.id),
    })
    .select('attachment.id', 'id')
    .getRawMany<{ id: number }>();

  await context.conversations.remove(conversations);
  await context.attachmentService.removeOrphans(
    userId,
    attachmentIds.map((item) => Number(item.id)),
  );
  return conversations.length;
}

export async function getMessages(
  context: AiChatServiceContext,
  userId: number,
  conversationId: number,
  page = 1,
  limit = 50,
) {
  await requireOwnedConversation(context, userId, conversationId);
  const [items, total] = await context.messages.findAndCount({
    where: { conversationId },
    relations: { toolRuns: true, attachments: true },
    order: { createdAt: 'ASC', id: 'ASC' },
    skip: (page - 1) * limit,
    take: limit,
  });
  return {
    items: items.map((message) => context.serializeMessage(message)),
    total,
    page,
    limit,
  };
}

export async function deleteMessage(
  context: AiChatServiceContext,
  userId: number,
  conversationId: number,
  messageId: number,
): Promise<{ deletedIds: number[] }> {
  await requireOwnedConversation(context, userId, conversationId);
  if (context.activeConversations.has(conversationId)) {
    throw new ConflictException('该会话正在生成回答');
  }
  const target = await context.messages.findOne({
    where: { id: messageId, conversationId },
  });
  if (!target) throw new NotFoundException('消息不存在');

  const deletedIds =
    target.role === 'assistant'
      ? [target.id]
      : [
          target.id,
          ...(
            await context.messages.find({
              where: { conversationId, replyToMessageId: target.id },
              select: { id: true },
            })
          ).map((item) => item.id),
        ];

  const attachmentRows = await context.messages
    .createQueryBuilder('message')
    .innerJoin('message.attachments', 'attachment')
    .where('message.id IN (:...ids)', { ids: deletedIds })
    .select('attachment.id', 'id')
    .getRawMany<{ id: number }>();

  await context.messages
    .createQueryBuilder()
    .delete()
    .from('ai_message_attachments')
    .where('message_id IN (:...ids)', { ids: deletedIds })
    .execute();
  await context.messages.delete(deletedIds);
  await context.attachmentService.removeOrphans(
    userId,
    attachmentRows.map((item) => Number(item.id)),
  );

  const last = await context.messages.findOne({
    where: { conversationId },
    order: { createdAt: 'DESC', id: 'DESC' },
  });
  await context.conversations.update(
    { id: conversationId, userId },
    { lastMessageAt: last?.createdAt ?? null },
  );
  return { deletedIds };
}

export async function requireOwnedConversation(
  context: AiChatServiceContext,
  userId: number,
  id: number,
): Promise<AiConversation> {
  const conversation = await context.conversations.findOne({ where: { id, userId } });
  if (!conversation) throw new NotFoundException('会话不存在');
  return conversation;
}

export async function acquireConversation(
  context: AiChatServiceContext,
  conversationId: number,
): Promise<void> {
  if (context.activeConversations.has(conversationId)) {
    throw new ConflictException('该会话正在生成回答');
  }
  context.activeConversations.add(conversationId);
  try {
    const pending = await context.messages.exists({
      where: { conversationId, role: 'assistant', status: 'pending' },
    });
    if (pending) throw new ConflictException('该会话正在生成回答');
  } catch (error) {
    context.activeConversations.delete(conversationId);
    throw error;
  }
}

export function normalizeTitle(context: AiChatServiceContext, title?: string): string {
  const normalized = title?.trim();
  return normalized ? normalized.slice(0, 100) : DEFAULT_TITLE;
}

export function titleFromMessage(context: AiChatServiceContext, message: string): string {
  return message.replace(/\s+/g, ' ').trim().slice(0, 30) || DEFAULT_TITLE;
}

export function metadataSkillKey(
  context: AiChatServiceContext,
  metadata: Record<string, unknown> | null,
): string | null {
  return typeof metadata?.skillKey === 'string' ? metadata.skillKey : null;
}

export function assertSkillAvailable(
  context: AiChatServiceContext,
  user: AuthenticatedUser,
  skillKey?: string | null,
): void {
  if (!skillKey) return;
  const available = context.listSkills(user).some((skill) => skill.key === skillKey);
  if (!available) throw new BadRequestException('技能不存在或无权使用');
}

export function truncateText(context: AiChatServiceContext, value: string, max: number): string {
  if (value.length <= max) return value;
  return `${value.slice(0, max)}\n\n[内容过长，已截断为前 ${max} 字]`;
}

export function serializeMessage(
  context: AiChatServiceContext,
  message: AiMessage,
): Record<string, unknown> {
  return {
    id: message.id,
    conversationId: message.conversationId,
    role: message.role,
    content: message.content,
    reasoningContent: message.reasoningContent,
    status: message.status,
    errorCode: message.errorCode,
    replyToMessageId: message.replyToMessageId,
    metadata: message.metadata,
    attachments: (message.attachments ?? []).map((attachment) =>
      context.attachmentService.serialize(attachment),
    ),
    toolRuns: [...(message.toolRuns ?? [])]
      .sort((a, b) => a.id - b.id)
      .map((run) => ({
        id: run.id,
        toolCallId: run.toolCallId,
        toolName: run.toolName,
        skillKey: run.skillKey,
        argumentsSummary: run.argumentsSummary,
        resultSummary: run.resultSummary,
        status: run.status,
        durationMs: run.durationMs,
      })),
    createdAt: message.createdAt,
    updatedAt: message.updatedAt,
  };
}
