import {
  Body,
  Controller,
  Delete,
  Get,
  HttpException,
  Param,
  ParseIntPipe,
  Patch,
  Post,
  Query,
  Req,
  Res,
  UploadedFile,
  UseInterceptors,
  UsePipes,
  ValidationPipe,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Throttle, ThrottlerException } from '@nestjs/throttler';
import type { Request, Response } from 'express';
import { RequirePermission } from '../auth/decorators/permission.decorator';
import type { AuthenticatedUser } from '../authorization';
import { AiAttachmentService } from './ai-attachment.service';
import { AiChatService } from './ai-chat.service';
import type { AiSseEventName } from './ai-chat.types';
import type { AiReviewSectionType } from './entities';
import {
  CreateConversationDto,
  EditMessageDto,
  MessagePageQueryDto,
  RegenerateMessageDto,
  ResolveImportPreflightDto,
  SendMessageDto,
  SubmitFormDto,
  SubmitReviewDto,
  UpdateConversationDto,
} from './dto/ai-chat.dto';

interface AuthenticatedRequest extends Request {
  user: AuthenticatedUser;
}

@Controller('ai/chat')
@RequirePermission('ai:chat:use')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
export class AiChatController {
  constructor(
    private readonly service: AiChatService,
    private readonly attachmentService: AiAttachmentService,
  ) {}

  @Get('skills')
  skills(@Req() req: AuthenticatedRequest) {
    return { success: true, data: this.service.listSkills(req.user) };
  }

  @Get('conversations')
  async list(@Req() req: AuthenticatedRequest) {
    return { success: true, data: await this.service.listConversations(req.user.id) };
  }

  @Post('conversations')
  async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateConversationDto) {
    return {
      success: true,
      data: await this.service.createConversation(req.user, dto.title, dto.lockedSkillKey),
    };
  }

  @Patch('conversations/:id')
  async update(
    @Req() req: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: UpdateConversationDto,
  ) {
    return { success: true, data: await this.service.updateConversation(req.user, id, dto) };
  }

  @Delete('conversations/:id')
  async remove(@Req() req: AuthenticatedRequest, @Param('id', ParseIntPipe) id: number) {
    await this.service.deleteConversation(req.user.id, id);
    return { success: true };
  }

  @Delete('conversations')
  async removeAll(@Req() req: AuthenticatedRequest) {
    return {
      success: true,
      data: { deleted: await this.service.deleteAllConversations(req.user.id) },
    };
  }

  @Delete('conversations/:id/messages/:messageId')
  async removeMessage(
    @Req() req: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Param('messageId', ParseIntPipe) messageId: number,
  ) {
    return {
      success: true,
      data: await this.service.deleteMessage(req.user.id, id, messageId),
    };
  }

  @Post('attachments')
  @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 10 * 1024 * 1024 } }))
  async uploadAttachment(
    @Req() req: AuthenticatedRequest,
    @UploadedFile() file: Express.Multer.File,
  ) {
    const attachment = await this.attachmentService.upload(req.user.id, file);
    return { success: true, data: this.attachmentService.serialize(attachment) };
  }

  @Get('attachments/:id')
  async downloadAttachment(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('id', ParseIntPipe) id: number,
  ): Promise<void> {
    const { attachment, stream } = await this.attachmentService.open(req.user.id, id);
    res.setHeader('Content-Type', attachment.mimeType);
    res.setHeader('Content-Length', String(attachment.size));
    res.setHeader('Cache-Control', 'private, no-store');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader(
      'Content-Disposition',
      `inline; filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
    );
    stream.pipe(res);
  }

  @Delete('attachments/:id')
  async deleteAttachment(
    @Req() req: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
  ) {
    await this.attachmentService.removeUnbound(req.user.id, id);
    return { success: true };
  }

  @Get('conversations/:id/messages')
  async messages(
    @Req() req: AuthenticatedRequest,
    @Param('id', ParseIntPipe) id: number,
    @Query() query: MessagePageQueryDto,
  ) {
    return {
      success: true,
      data: await this.service.getMessages(req.user.id, id, query.page ?? 1, query.limit ?? 50),
    };
  }

  @Post('conversations/:id/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async stream(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: SendMessageDto,
  ): Promise<void> {
    return this.handleStream(res, dto.clientRequestId, id, (signal, emit, onReady) =>
      this.service.streamMessage(req.user, id, dto, signal, emit, onReady),
    );
  }

  @Post('conversations/:id/messages/:messageId/regenerate/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async regenerate(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('id', ParseIntPipe) id: number,
    @Param('messageId', ParseIntPipe) messageId: number,
    @Body() dto: RegenerateMessageDto,
  ): Promise<void> {
    return this.handleStream(res, dto.clientRequestId, id, (signal, emit, onReady) =>
      this.service.regenerateMessage(
        req.user,
        id,
        messageId,
        dto.clientRequestId,
        dto.reasoningEffort,
        signal,
        emit,
        onReady,
      ),
    );
  }

  @Post('conversations/:id/messages/:messageId/edit/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async editMessage(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('id', ParseIntPipe) id: number,
    @Param('messageId', ParseIntPipe) messageId: number,
    @Body() dto: EditMessageDto,
  ): Promise<void> {
    return this.handleStream(res, dto.clientRequestId, id, (signal, emit, onReady) =>
      this.service.editMessage(
        req.user,
        id,
        messageId,
        dto,
        signal,
        emit,
        onReady,
      ),
    );
  }

  @Post('forms/:formId/submit/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async submitForm(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('formId') formId: string,
    @Body() dto: SubmitFormDto,
  ): Promise<void> {
    const conversationId = await this.service.resolveFormConversationId(req.user.id, formId);
    return this.handleStream(res, dto.clientRequestId, conversationId, (signal, emit, onReady) =>
      this.service.submitForm(req.user, formId, dto, signal, emit, onReady),
    );
  }

  @Post('reviews/:reviewId/submit/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async submitReview(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('reviewId') reviewId: string,
    @Body() dto: SubmitReviewDto,
  ): Promise<void> {
    const conversationId = await this.service.resolveReviewConversationId(req.user.id, reviewId);
    return this.handleStream(res, dto.clientRequestId, conversationId, (signal, emit, onReady) =>
      this.service.submitReview(req.user, reviewId, dto, signal, emit, onReady),
    );
  }

  @Post('import/preflight/:messageId/resolve/stream')
  @Throttle({ default: { ttl: 60000, limit: 10 } })
  async resolveImportPreflight(
    @Req() req: AuthenticatedRequest,
    @Res() res: Response,
    @Param('messageId', ParseIntPipe) messageId: number,
    @Body() dto: ResolveImportPreflightDto,
  ): Promise<void> {
    const conversationId = await this.service.resolvePreflightConversationId(
      req.user.id,
      messageId,
    );
    return this.handleStream(res, dto.clientRequestId, conversationId, (signal, emit, onReady) =>
      this.service.resolveImportPreflight(req.user, messageId, dto, signal, emit, onReady),
    );
  }

  @Post('reviews/:reviewId/steps/:sectionKey/confirm')
  async confirmReviewStep(
    @Req() req: AuthenticatedRequest,
    @Param('reviewId') reviewId: string,
    @Param('sectionKey') sectionKey: string,
  ) {
    return {
      success: true,
      data: await this.service.confirmReviewStep(
        req.user,
        reviewId,
        sectionKey,
      ),
    };
  }

  @Post('reviews/:reviewId/types/:type/confirm')
  async confirmReviewGroup(
    @Req() req: AuthenticatedRequest,
    @Param('reviewId') reviewId: string,
    @Param('type') type: string,
  ) {
    return {
      success: true,
      data: await this.service.confirmReviewGroup(
        req.user,
        reviewId,
        type as AiReviewSectionType,
      ),
    };
  }

  private async handleStream(
    res: Response,
    requestId: string,
    conversationId: number,
    execute: (
      signal: AbortSignal,
      emit: (event: AiSseEventName, data: Record<string, unknown>) => void,
      onReady: () => void,
    ) => Promise<void>,
  ): Promise<void> {
    const abortController = new AbortController();
    const onClose = () => {
      if (!res.writableEnded) abortController.abort(new Error('client disconnected'));
    };
    res.once('close', onClose);
    let lastMessageId: number | null = null;
    const emit = (event: AiSseEventName, data: Record<string, unknown>) => {
      const nestedMessage =
        data.message && typeof data.message === 'object'
          ? (data.message as { id?: unknown })
          : undefined;
      const eventMessageId =
        typeof data.messageId === 'number'
          ? data.messageId
          : typeof nestedMessage?.id === 'number'
            ? nestedMessage.id
            : null;
      if (eventMessageId !== null) lastMessageId = eventMessageId;
      if (!res.writableEnded && !res.destroyed) {
        res.write(
          `event: ${event}\ndata: ${JSON.stringify({
            ...data,
            requestId,
            conversationId,
            messageId: eventMessageId ?? lastMessageId,
          })}\n\n`,
        );
      }
    };
    const onReady = () => {
      if (res.headersSent) return;
      res.status(200);
      res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
      res.setHeader('Cache-Control', 'no-cache, no-transform');
      res.setHeader('Connection', 'keep-alive');
      res.setHeader('X-Accel-Buffering', 'no');
      res.flushHeaders();
    };
    try {
      await execute(abortController.signal, emit, onReady);
    } catch (error) {
      if (!res.headersSent) throw error;
      if (!abortController.signal.aborted) {
        const { code, message } = this.safeError(error);
        emit('error', { code, message });
      }
    } finally {
      res.off('close', onClose);
      if (res.headersSent) {
        emit('done', {});
        if (!res.writableEnded) res.end();
      }
    }
  }

  private safeError(error: unknown): { code: string; message: string } {
    if (error instanceof ThrottlerException) return { code: 'RATE_LIMITED', message: '请求过于频繁' };
    if (error instanceof HttpException) {
      const status = error.getStatus();
      if (status === 404) return { code: 'NOT_FOUND', message: '会话不存在' };
      if (status === 409) return { code: 'CONVERSATION_BUSY', message: '该会话正在生成回答' };
      if (status === 408) return { code: 'UPSTREAM_TIMEOUT', message: 'AI 服务响应超时' };
      if (status === 400) return { code: 'BAD_REQUEST', message: error.message };
      if (status === 429) return { code: 'RATE_LIMITED', message: 'AI 服务请求过于频繁' };
      if (status >= 500) return { code: 'UPSTREAM_ERROR', message: error.message };
    }
    return { code: 'UPSTREAM_ERROR', message: 'AI 服务暂时不可用' };
  }
}
