import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  Inject,
  Param,
  Post,
  Put,
  UploadedFile,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express'
import {
  ApiBadRequestResponse,
  ApiBearerAuth,
  ApiBody,
  ApiConsumes,
  ApiForbiddenResponse,
  ApiOperation,
  ApiTags,
  ApiUnauthorizedResponse,
} from '@nestjs/swagger'

import { ApiEnvelopeResponse } from '../../../common/swagger/api-envelope-response.decorator'
import { RequireCapability } from '../../auth/decorators/require-capability.decorator'
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'
import { RoleCapabilitiesGuard } from '../../auth/guards/role-capabilities.guard'
import { ResumeRagSyncService } from '../../resume/resume-rag-sync.service'
import {
  RagAskBodyDto,
  RagAskResultDto,
  RagResumeSyncBodyDto,
  RagResumeSyncResultDto,
  RagCustomBodyDto,
  RagCustomUpdateResultDto,
  RagDocumentDetailDto,
  RagExportUserDocsResultDto,
  RagResetUserDocsResultDto,
  RagSearchBodyDto,
  RagSearchMatchDto,
  RagStatusDto,
  RagUserDocIngestBodyDto,
  RagUserDocIngestResultDto,
} from './dto/rag-swagger.dto'
import { RagService } from './rag.service'
import {
  resolveUserDocChunkingConfig,
  UserDocsIngestionService,
} from './user-docs-ingestion.service'
import { RAG_KNOWLEDGE_DOMAINS, isRagKnowledgeDomain } from './rag-knowledge-domain'

function assertKnowledgeDomains(domains: unknown[] | undefined, endpoint: 'search' | 'ask') {
  if (!domains) return

  const invalidDomain = domains.find((domain) => !isRagKnowledgeDomain(domain))

  if (invalidDomain) {
    throw new BadRequestException(
      `Unsupported ${endpoint} knowledgeDomain: ${String(invalidDomain)}. Supported values: ${RAG_KNOWLEDGE_DOMAINS.join(', ')}`,
    )
  }
}

@Controller('ai/rag')
@UseGuards(JwtAuthGuard)
@ApiTags('AI RAG')
@ApiBearerAuth('bearer')
@ApiUnauthorizedResponse({
  description: '未提供有效 Bearer Token',
})
export class RagController {
  constructor(
    @Inject(RagService)
    private readonly ragService: RagService,
    @Inject(ResumeRagSyncService)
    private readonly resumeRagSyncService: ResumeRagSyncService,
    @Inject(UserDocsIngestionService)
    private readonly userDocsIngestionService: UserDocsIngestionService,
  ) {}

  /**
   * 返回 RAG 索引状态与运行时摘要
   * @returns RAG 状态
   */
  @Get('status')
  @ApiOperation({
    summary: '获取 RAG 索引状态',
    description: '返回索引存在性、是否过期与关键摘要信息',
  })
  @ApiEnvelopeResponse({
    description: '读取 RAG 状态成功',
    type: RagStatusDto,
  })
  getStatus() {
    return this.ragService.getStatus()
  }

  /**
   * 重建简历与知识库的向量索引
   * @returns 重建后的状态摘要
   */
  @Post('index/rebuild')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '重建 RAG 索引',
    description: '重建简历与知识库向量索引',
  })
  @ApiEnvelopeResponse({
    description: '重建 RAG 索引成功',
    type: RagStatusDto,
  })
  @ApiForbiddenResponse({
    description: '当前角色没有触发 AI 分析权限',
  })
  rebuildIndex() {
    // 建索引入口：重切块、重向量化，并写回本地索引文件。
    return this.ragService.rebuildIndex()
  }

  /**
   * 人工触发简历检索态同步。
   *
   * @param body 同步范围
   * @returns 同步摘要
   */
  @Post('sync/resume')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '人工触发简历检索态同步',
    description: '按 scope 同步 draft/published 到检索态表',
  })
  @ApiEnvelopeResponse({
    description: '简历检索态同步成功',
    type: RagResumeSyncResultDto,
  })
  @ApiBadRequestResponse({
    description: '同步参数不合法',
  })
  @ApiForbiddenResponse({
    description: '当前角色没有触发 AI 分析权限',
  })
  syncResumeRetrieval(@Body() body: RagResumeSyncBodyDto = {}) {
    const scope = body.scope ?? 'all'

    if (scope !== 'draft' && scope !== 'published' && scope !== 'all') {
      throw new BadRequestException(`Unsupported sync scope: ${scope}`)
    }

    return this.resumeRagSyncService.syncCurrent(scope)
  }

  /**
   * 上传并写入 user_docs 检索态（仅 draft/published）。
   *
   * @param file 上传文件
   * @param body 入库参数
   * @returns 入库结果摘要
   */
  @Post('ingest/user-doc')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @UseInterceptors(FileInterceptor('file'))
  @ApiOperation({
    summary: '上传并入库 user_docs',
    description: '提取文件文本后切块向量化，并写入 user_docs 检索态表',
  })
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      properties: {
        file: {
          description: '待入库的用户资料文件',
          format: 'binary',
          type: 'string',
        },
        scope: {
          description: '入库作用域（仅 draft/published）',
          enum: ['draft', 'published'],
          type: 'string',
        },
        chunkingProfile: {
          description: '切片策略（balanced=500/50，contextual=1000/100）',
          enum: ['balanced', 'contextual'],
          type: 'string',
        },
        chunkSize: {
          description: '自定义切片大小，范围 4-6666，优先级高于 profile',
          maximum: 6666,
          minimum: 4,
          type: 'number',
        },
        chunkOverlap: {
          description: '自定义重叠字符数，范围 0-300，且必须小于 chunkSize',
          maximum: 300,
          minimum: 0,
          type: 'number',
        },
        contentType: {
          description: '内容类型：hobby=兴趣爱好，tech_blog=技术博客，knowledge_column=知识专栏，general=其他通用',
          enum: ['hobby', 'tech_blog', 'knowledge_column', 'general'],
          type: 'string',
        },
      },
    },
  })
  @ApiEnvelopeResponse({
    description: 'user_docs 入库成功',
    type: RagUserDocIngestResultDto,
  })
  @ApiBadRequestResponse({
    description: '文件缺失或入库参数不合法',
  })
  @ApiForbiddenResponse({
    description: '当前角色没有触发 AI 分析权限',
  })
  ingestUserDoc(
    @UploadedFile() file?: Express.Multer.File,
    @Body() body: RagUserDocIngestBodyDto = {},
  ) {
    if (!file) {
      throw new BadRequestException('File is required')
    }

    const sourceScope = body.scope ?? 'draft'

    if (sourceScope !== 'draft' && sourceScope !== 'published') {
      throw new BadRequestException(`Unsupported ingest scope: ${sourceScope}`)
    }

    if (
      body.chunkingProfile &&
      body.chunkingProfile !== 'balanced' &&
      body.chunkingProfile !== 'contextual' &&
      body.chunkingProfile !== 'semantic'
    ) {
      throw new BadRequestException(
        `Unsupported ingest chunkingProfile: ${body.chunkingProfile}`,
      )
    }

    let chunkingConfig: { chunkSize: number; chunkOverlap: number }

    try {
      chunkingConfig = resolveUserDocChunkingConfig({
        profile: body.chunkingProfile,
        chunkSize: body.chunkSize,
        chunkOverlap: body.chunkOverlap,
      })
    } catch (error) {
      throw new BadRequestException(
        error instanceof Error ? error.message : 'Invalid user_docs chunking config',
      )
    }

    return this.userDocsIngestionService.ingest({
      buffer: file.buffer,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size,
      sourceScope,
      title: body.title ?? file.originalname,
      chunkingProfile: body.chunkingProfile,
      chunkSize: chunkingConfig.chunkSize,
      chunkOverlap: chunkingConfig.chunkOverlap,
      contentType: body.contentType,
    })
  }

  @Post('custom')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '添加自定义 RAG 资料（JSON）',
    description: '通过 JSON body 添加自定义文本内容，自动分块向量化写入检索态',
  })
  @ApiBody({ type: RagCustomBodyDto })
  @ApiEnvelopeResponse({ description: '入库成功', type: RagUserDocIngestResultDto })
  ingestCustom(@Body() body: RagCustomBodyDto) {
    if (!body.content?.trim()) {
      throw new BadRequestException('内容不能为空')
    }
    return this.userDocsIngestionService.ingestCustom({
      title: body.title ?? 'RAG 资料',
      content: body.content,
      contentType: body.contentType ?? 'general',
      sourceScope: body.scope ?? 'published',
      linkUrl: body.linkUrl,
      linkUrls: body.linkUrls,
      imageUrls: body.imageUrls,
      summary: body.summary,
    })
  }

  @Put('custom/:documentId')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({ summary: '更新已入库的自定义资料' })
  @ApiEnvelopeResponse({
    description: '更新自定义资料成功',
    type: RagCustomUpdateResultDto,
  })
  updateCustom(
    @Param('documentId') documentId: string,
    @Body() body: RagCustomBodyDto,
  ) {
    if (typeof body.content === 'string' && !body.content.trim()) {
      throw new BadRequestException('内容不能为空')
    }

    return this.userDocsIngestionService.updateCustom(documentId, {
      title: body.title,
      content: body.content,
      contentType: body.contentType,
      sourceScope: body.scope,
      linkUrl: body.linkUrl,
      linkUrls: body.linkUrls,
      imageUrls: body.imageUrls,
      summary: body.summary,
    })
  }

  @Get('documents')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '列出所有 RAG 文档',
    description: '返回 rag_documents 列表，用于管理端查看已入库资料',
  })
  listDocuments() {
    return this.userDocsIngestionService.listDocuments()
  }

  @Get('documents/:documentId')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '读取单个 RAG 文档详情',
    description: '返回已入库资料的完整详情，供管理端查看或编辑自定义资料。',
  })
  @ApiEnvelopeResponse({
    description: '读取 RAG 文档详情成功',
    type: RagDocumentDetailDto,
  })
  getDocumentDetail(@Param('documentId') documentId: string) {
    return this.userDocsIngestionService.getDocumentDetail(documentId)
  }

  @Delete('documents/:documentId')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '删除 RAG 文档及其 chunk',
    description: '删除指定文档及其关联的所有 chunk',
  })
  deleteDocument(@Param('documentId') documentId: string) {
    return this.userDocsIngestionService.deleteDocument(documentId)
  }

  @Post('user-docs/export')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '导出当前 user_docs 快照',
    description: '按当前 SQLite 真源导出 user_docs 内容，用于备份和重建前校验。',
  })
  @ApiEnvelopeResponse({
    description: '导出 user_docs 成功',
    type: RagExportUserDocsResultDto,
  })
  exportUserDocs() {
    return this.userDocsIngestionService.exportUserDocs()
  }

  @Post('user-docs/reset')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '清空当前 user_docs 真源与向量',
    description: '按当前 DB 真源批量删除 user_docs，并同步删除对应向量记录。',
  })
  @ApiEnvelopeResponse({
    description: '清空 user_docs 成功',
    type: RagResetUserDocsResultDto,
  })
  resetUserDocs() {
    return this.userDocsIngestionService.resetUserDocs()
  }

  @Post('user-docs/reconcile')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '对账并重建 user_docs 向量',
    description: '按 rag_documents / rag_chunks 为真源，删除向量残留并重建 user_docs 向量数据。',
  })
  reconcileUserDocsVectors() {
    return this.userDocsIngestionService.reconcileUserDocsVectors()
  }

  /**
   * 在当前索引上执行语义检索
   * @param body 检索请求体
   * @returns 检索结果列表
   */
  @Post('search')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '执行 RAG 检索',
    description: '在当前索引中按语义与关键词执行混合检索',
  })
  @ApiEnvelopeResponse({
    description: '检索成功',
    isArray: true,
    type: RagSearchMatchDto,
  })
  @ApiBadRequestResponse({
    description: '检索参数不合法',
  })
  @ApiForbiddenResponse({
    description: '当前角色没有触发 AI 分析权限',
  })
  search(@Body() body: RagSearchBodyDto) {
    if (
      body.vectorScope &&
      body.vectorScope !== 'draft' &&
      body.vectorScope !== 'published' &&
      body.vectorScope !== 'all'
    ) {
      throw new BadRequestException(`Unsupported search vectorScope: ${body.vectorScope}`)
    }
    assertKnowledgeDomains(body.knowledgeDomains, 'search')

    return this.ragService.search(body.query, body.limit, {
      minScore: body.minScore,
      minScoreGap: body.minScoreGap,
    }, {
      useVectorStore: body.useVectorStore,
      vectorScope: body.vectorScope,
      fallbackToLocal: body.vectorFallbackToLocal,
      knowledgeDomains: body.knowledgeDomains,
    })
  }

  /**
   * 基于检索上下文生成问答结果
   * @param body 问答请求体
   * @returns 问答结果
   */
  @Post('ask')
  @UseGuards(RoleCapabilitiesGuard)
  @RequireCapability('canTriggerAiAnalysis')
  @ApiOperation({
    summary: '执行 RAG 问答',
    description: '先检索上下文，再基于上下文生成回答',
  })
  @ApiEnvelopeResponse({
    description: '问答成功',
    type: RagAskResultDto,
  })
  @ApiBadRequestResponse({
    description: '问答参数不合法',
  })
  @ApiForbiddenResponse({
    description: '当前角色没有触发 AI 分析权限',
  })
  ask(@Body() body: RagAskBodyDto) {
    if (
      body.vectorScope &&
      body.vectorScope !== 'draft' &&
      body.vectorScope !== 'published' &&
      body.vectorScope !== 'all'
    ) {
      throw new BadRequestException(`Unsupported ask vectorScope: ${body.vectorScope}`)
    }
    assertKnowledgeDomains(body.knowledgeDomains, 'ask')

    // ask 先 search，再拼接上下文调用生成接口，不是直接裸问模型。
    return this.ragService.ask(body.question, body.limit, body.locale, {
      useVectorStore: body.useVectorStore,
      vectorScope: body.vectorScope,
      fallbackToLocal: body.vectorFallbackToLocal,
      knowledgeDomains: body.knowledgeDomains,
    })
  }
}
