import { Injectable, NotFoundException, Logger, Optional, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { ContentSafetyService } from '../content-safety/content-safety.service';
import { AiGatewayService } from '../ai/gateway/ai-gateway.service';
import { StorageService } from '../../infrastructure/storage/storage.service';
import { RagChatOutputSchema } from '../ai/prompts/schemas/rag-chat.schema';
import type { StreamChunk } from '../ai/providers/ai-provider.interface';

const MAX_CONTEXT_CHARS = 4000;

const VALID_SCOPE_TYPES = ['knowledge_base', 'folder', 'material', 'knowledge_item', 'global'];

export interface CreateSessionParams {
  scopeType: string;
  scopeId?: string | null;
  parentKnowledgeBaseId?: string | null;
  createdFrom?: string;
  title?: string;
  forceCreate?: boolean;
}

export interface CreateSessionResult {
  session: any;
  isNew: boolean;
}

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

  constructor(
    private readonly prisma: PrismaService,
    @Optional() private readonly safety?: ContentSafetyService,
    @Optional() private readonly aiGateway?: AiGatewayService,
    @Optional() private readonly storage?: StorageService,
  ) {}

  async createSession(userId: string, params: CreateSessionParams): Promise<CreateSessionResult> {
    const { scopeType, scopeId, parentKnowledgeBaseId, createdFrom, title, forceCreate } = params;

    if (!VALID_SCOPE_TYPES.includes(scopeType)) {
      throw new BadRequestException(`scopeType must be one of: ${VALID_SCOPE_TYPES.join(', ')}`);
    }
    if (scopeType !== 'global' && !scopeId) {
      throw new BadRequestException('scopeId is required when scopeType is not "global"');
    }

    const derivedKbId = this.deriveParentKbId(scopeType, scopeId, parentKnowledgeBaseId);

    // open-or-create: for non-global scopes, skip lookup when forceCreate
    if (!forceCreate && scopeType !== 'global' && scopeId) {
      const existing = await this.prisma.chatSession.findFirst({
        where: {
          userId,
          scopeType,
          scopeId,
          isDeleted: false,
          isArchived: false,
        },
        orderBy: { updatedAt: 'desc' },
      });

      if (existing) {
        this.logger.log(`open-or-create: FOUND existing session ${existing.id} for scope ${scopeType}/${scopeId}`);
        return { session: existing, isNew: false };
      }
    }

    // Create new session
    const session = await this.prisma.chatSession.create({
      data: {
        userId,
        knowledgeBaseId: derivedKbId,
        scopeType,
        scopeId: scopeId ?? null,
        parentKnowledgeBaseId: derivedKbId,
        createdFrom: createdFrom ?? 'global_ai_entry',
        title: title || '新对话',
        knowledgeItemIds: [],
      },
    });

    this.logger.log(`open-or-create: CREATED new session ${session.id} for scope ${scopeType}/${scopeId}`);
    return { session, isNew: true };
  }

  async listSessions(
    userId: string,
    opts?: {
      scopeType?: string;
      scopeId?: string;
      parentKnowledgeBaseId?: string;
      kbId?: string;
      isArchived?: boolean;
      page?: number;
      limit?: number;
    },
  ) {
    const where: any = { userId, isDeleted: false };

    if (opts?.scopeType && opts?.scopeId) {
      where.scopeType = opts.scopeType;
      where.scopeId = opts.scopeId;
    } else if (opts?.parentKnowledgeBaseId) {
      where.parentKnowledgeBaseId = opts.parentKnowledgeBaseId;
    } else if (opts?.kbId) {
      where.knowledgeBaseId = opts.kbId;
    }

    if (opts?.isArchived !== undefined) {
      where.isArchived = opts.isArchived;
    }

    const page = opts?.page ?? 1;
    const limit = opts?.limit ?? 20;

    const [data, total] = await Promise.all([
      this.prisma.chatSession.findMany({
        where,
        orderBy: [{ isPinned: 'desc' }, { lastMessageAt: 'desc' }],
        take: limit,
        skip: (page - 1) * limit,
      }),
      this.prisma.chatSession.count({ where }),
    ]);

    return { data, meta: { page, limit, total } };
  }

  async getMessages(userId: string, sessionId: string) {
    const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== userId) throw new NotFoundException('对话不存在');
    return this.prisma.chatMessage.findMany({
      where: { sessionId },
      orderBy: { createdAt: 'asc' },
      include: { citations: true },
    });
  }

  async sendMessage(userId: string, sessionId: string, content: string) {
    const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== userId) throw new NotFoundException('对话不存在');

    // Content safety
    const inputCheck = await this.safety?.check(content, { userId, contentType: 'rag_input' });
    if (inputCheck && !inputCheck.safe) {
      return { blocked: true, message: '输入包含违规内容，请修改后重试' };
    }

    // Save user message with scope snapshot
    const scopeSnapshot = this.buildScopeSnapshot(session);
    await this.prisma.chatMessage.create({
      data: { sessionId, role: 'user', content, scopeSnapshot },
    });

    // Auto-title from first user message
    await this.autoTitle(session, content);

    // Retrieve context by scope
    const kbId = session.parentKnowledgeBaseId || session.knowledgeBaseId;
    this.logger.log(`RAG: scopeType=${session.scopeType}, scopeId=${session.scopeId}, kbId=${kbId}`);
    const context = await this.loadContextByScope(session);
    this.logger.log(`RAG context: isEmpty=${context.isEmpty}, textLen=${context.text.length}, citations=${context.citations.length}, aiGateway=${!!this.aiGateway}`);

    // Generate AI response
    let reply: string;
    let citations: any[] = [];

    if (this.aiGateway && context.text) {
      try {
        this.logger.log(`Calling AI Gateway with ${context.text.length} chars context`);
        const messages = [
          { role: 'system' as const, content: this.buildSystemPrompt(context.text) },
          { role: 'user' as const, content },
        ];
        const resp = await this.aiGateway.generate({
          feature: 'rag-chat',
          userId,
          tier: 'primary',
          promptKey: 'rag-chat',
          promptVersion: 'v1',
          messages,
          maxTokens: 2048,
          outputSchema: RagChatOutputSchema,
        });
        reply = resp.parsed?.answer ?? String(resp.parsed?.content ?? '抱歉，AI 暂时无法生成回答。');
        citations = context.citations;
      } catch (err: any) {
        this.logger.error(`AI Gateway FAILED: ${err?.message}`, err?.stack?.substring(0, 300));
        reply = this.fallbackReply(context.isEmpty);
      }
    } else {
      this.logger.warn(`Falling back: aiGateway=${!!this.aiGateway}, hasText=${!!context.text}`);
      reply = this.fallbackReply(context.isEmpty);
    }

    // Save AI message with scope snapshot
    const aiMsg = await this.prisma.chatMessage.create({
      data: { sessionId, role: 'ai', content: reply, tokens: reply.length, scopeSnapshot },
    });

    // Save citations
    for (const c of citations.slice(0, 5)) {
      await this.prisma.chatCitation.create({
        data: {
          messageId: aiMsg.id,
          chunkId: c.id,
          sourceTitle: c.title ?? null,
          excerptText: c.text.slice(0, 500),
        },
      });
    }

    // Update session timestamp
    await this.prisma.chatSession.update({ where: { id: sessionId }, data: { lastMessageAt: new Date() } });

    return { message: aiMsg, citations };
  }

  async *sendMessageStream(userId: string, sessionId: string, content: string): AsyncGenerator<StreamChunk, void, undefined> {
    const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== userId) {
      yield { type: 'error', content: '对话不存在' };
      return;
    }

    const inputCheck = await this.safety?.check(content, { userId, contentType: 'rag_input' });
    if (inputCheck && !inputCheck.safe) {
      yield { type: 'error', content: '输入包含违规内容' };
      return;
    }

    // Save user message with scope snapshot
    const scopeSnapshot = this.buildScopeSnapshot(session);
    await this.prisma.chatMessage.create({ data: { sessionId, role: 'user', content, scopeSnapshot } });

    // Auto-title from first user message
    await this.autoTitle(session, content);

    // Load context by scope
    const context = await this.loadContextByScope(session);
    if (!this.aiGateway || !context.text) {
      yield { type: 'content', content: this.fallbackReply(!context.text) };
    } else {
      const messages = [
        { role: 'system' as const, content: this.buildSystemPrompt(context.text) },
        { role: 'user' as const, content },
      ];

      let fullContent = '';
      for await (const chunk of this.aiGateway.generateStream({
        feature: 'rag-chat', userId, tier: 'primary',
        promptKey: 'rag-chat', promptVersion: 'v1', messages, maxTokens: 2048,
      })) {
        if (chunk.type === 'content' && chunk.content) {
          fullContent += chunk.content;
        }
        yield chunk;
      }

      // Save AI reply
      if (fullContent) {
        const aiMsg = await this.prisma.chatMessage.create({
          data: { sessionId, role: 'ai', content: fullContent, tokens: fullContent.length, scopeSnapshot },
        });
        for (const c of context.citations.slice(0, 5)) {
          await this.prisma.chatCitation.create({
            data: { messageId: aiMsg.id, chunkId: c.id, sourceTitle: c.title ?? null, excerptText: c.text.slice(0, 500) },
          });
        }
      }
    }

    await this.prisma.chatSession.update({ where: { id: sessionId }, data: { lastMessageAt: new Date() } });
  }

  async deleteSession(userId: string, sessionId: string) {
    const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== userId) throw new NotFoundException('对话不存在');
    await this.prisma.chatSession.update({
      where: { id: sessionId },
      data: { isDeleted: true },
    });
    return { success: true, message: '会话已删除' };
  }

  async updateSession(
    userId: string,
    sessionId: string,
    dto: { title?: string; isPinned?: boolean; isArchived?: boolean; modelMode?: string; modelId?: string | null },
  ) {
    const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
    if (!session || session.userId !== userId) throw new NotFoundException('对话不存在');

    const data: any = {};
    if (dto.title !== undefined) data.title = dto.title;
    if (dto.isPinned !== undefined) data.isPinned = dto.isPinned;
    if (dto.isArchived !== undefined) data.isArchived = dto.isArchived;
    if (dto.modelMode !== undefined) data.modelMode = dto.modelMode;
    if (dto.modelId !== undefined) data.modelId = dto.modelId;

    return this.prisma.chatSession.update({ where: { id: sessionId }, data });
  }

  // ── Private ──

  private async autoTitle(session: any, content: string) {
    // Only auto-title if the session still has the default title
    if (!session.title || session.title === '新对话') {
      const title = this.generateTitle(content);
      await this.prisma.chatSession.update({
        where: { id: session.id },
        data: { title },
      });
    }
  }

  private generateTitle(content: string): string {
    const cleaned = content.trim().replace(/\s+/g, ' ');
    if (cleaned.length <= 40) return cleaned;
    // Truncate at a word boundary if possible, else hard cut
    const truncated = cleaned.slice(0, 40);
    const lastSpace = truncated.lastIndexOf(' ');
    const cut = lastSpace > 20 ? lastSpace : 40;
    return cleaned.slice(0, cut) + '…';
  }

  private buildScopeSnapshot(session: any) {
    return {
      scopeType: session.scopeType ?? 'knowledge_base',
      scopeId: session.scopeId ?? null,
      parentKnowledgeBaseId: session.parentKnowledgeBaseId ?? null,
    };
  }

  private deriveParentKbId(
    scopeType: string,
    scopeId: string | null | undefined,
    parentKnowledgeBaseId: string | null | undefined,
  ): string | null {
    switch (scopeType) {
      case 'knowledge_base':
        return scopeId ?? null;
      case 'material':
      case 'folder':
      case 'knowledge_item':
        return parentKnowledgeBaseId ?? null;
      case 'global':
        return null;
      default:
        return parentKnowledgeBaseId ?? null;
    }
  }

  private async loadContextByScope(session: any) {
    const scopeType = session.scopeType ?? 'knowledge_base';
    const scopeId = session.scopeId as string | null;
    const kbId = session.parentKnowledgeBaseId || session.knowledgeBaseId;

    try {
      let items: any[];

      switch (scopeType) {
        case 'global':
          // No knowledge base context — pure model answer
          return { text: '', citations: [], isEmpty: true };

        case 'material':
          // scopeId 是 KnowledgeSource 的 id（iOS MaterialReaderView 传的 materialId = source.id）
          const materialSource = await this.prisma.knowledgeSource.findFirst({
            where: { id: scopeId!, deletedAt: null },
            select: { id: true, title: true, textLength: true, originalObjectKey: true, parsedObjectKey: true },
          });
          if (!materialSource) return { text: '', citations: [], isEmpty: true };

          // 尝试读取文件的真实文本内容（优先解析后文本，否则原始文件）
          let fileText = '';
          const objectKey = materialSource.parsedObjectKey || materialSource.originalObjectKey;
          if (objectKey && this.storage) {
            try {
              const buf = await this.storage.getObject(objectKey);
              if (buf) {
                fileText = buf.toString('utf-8').slice(0, MAX_CONTEXT_CHARS);
                this.logger.log(`Read file ${objectKey}: ${fileText.length} chars`);
              }
            } catch (err: any) {
              this.logger.warn(`Failed to read file ${objectKey}: ${err?.message}`);
            }
          }

          const displayContent = fileText
            || `资料《${materialSource.title ?? '未命名'}》，共 ${materialSource.textLength ?? 0} 字符。`;
          items = [{
            id: materialSource.id,
            title: materialSource.title ?? '未命名资料',
            content: displayContent,
            summary: null,
          }];
          break;

        case 'knowledge_item':
          // 知识点是 AI 生成的产物，不支持作为 AI 对话的上下文
          this.logger.warn('knowledge_item scope is deprecated for AI chat — knowledge items are AI-generated outputs');
          return { text: '', citations: [], isEmpty: true };


        case 'knowledge_base':
        default:
          if (scopeType !== 'knowledge_base') {
            this.logger.warn(`Unknown scopeType "${scopeType}", falling back to knowledge_base`);
          }
          // 优先使用知识源（KnowledgeSource），知识源是原始资料，知识点是AI生成的产出
          const sources = await this.prisma.knowledgeSource.findMany({
            where: {
              knowledgeBaseId: kbId,
              deletedAt: null,
            },
            select: { id: true, title: true, textLength: true },
            orderBy: { createdAt: 'desc' },
            take: 30,
          });
          this.logger.log(`KnowledgeBase sources count: ${sources.length}`);

          // 无知识源时回退查知识点（兼容已有数据）
          if (sources.length === 0) {
            const fallbackItems = await this.prisma.knowledgeItem.findMany({
              where: {
                knowledgeBaseId: kbId,
                deletedAt: null,
              },
              select: { id: true, title: true, content: true, summary: true },
              orderBy: { updatedAt: 'desc' },
              take: 30,
            });
            if (fallbackItems.length > 0) {
              items = fallbackItems;
              break;
            }
          }

          // 从 chunks 构建上下文（不依赖 textLength，因为历史数据可能没有回填）
          const sourceIds = sources.map(s => s.id);
          const chunks = sourceIds.length > 0 ? await this.prisma.knowledgeChunk.findMany({
            where: { sourceId: { in: sourceIds }, deletedAt: null },
            select: { sourceId: true, content: true },
            orderBy: { chunkIndex: 'asc' },
            take: 200,
          }) : [];

          // 按 source 分组拼接内容
          const contentBySource: Record<string, string> = {};
          for (const c of chunks) {
            if (!contentBySource[c.sourceId]) contentBySource[c.sourceId] = '';
            contentBySource[c.sourceId] += c.content + '\n';
          }

          items = sources.map(s => {
            const chunkContent = contentBySource[s.id] || '';
            const chars = chunkContent.length || (s.textLength ?? 0);
            return {
              id: s.id,
              title: s.title ?? '未命名资料',
              content: chunkContent
                ? chunkContent.slice(0, 1000)
                : `资料《${s.title ?? '未命名'}》，共 ${chars} 字符。`,
              summary: null,
            };
          }).filter(item => (item.content?.length ?? 0) > 0);
          break;
      }

      if (!items || items.length === 0) return { text: '', citations: [], isEmpty: true };

      const parts: string[] = [];
      const citations: any[] = [];

      let total = 0;
      for (const item of items) {
        const t = item.content || item.summary || '';
        if (!t || total >= MAX_CONTEXT_CHARS) break;
        const snippet = t.slice(0, Math.min(t.length, 500));
        parts.push(`【${item.title}】${snippet}`);
        citations.push({ id: item.id, text: snippet, score: 1.0, title: item.title });
        total += snippet.length;
      }

      return { text: parts.join('\n\n'), citations, isEmpty: false };
    } catch {
      return { text: '', citations: [], isEmpty: true };
    }
  }

  private buildSystemPrompt(context: string) {
    return `你是知习 AI 学习助手。基于以下知识库内容回答用户问题，回答应准确、简洁、有依据。

## 知识库内容
${context}

## 回答要求
- 基于提供的知识库内容回答，不要编造信息
- 如果知识库内容不足以回答问题，请诚实告知
- 回答时可以用「根据知识库中的《xxx》...」引用来源`;
  }

  private fallbackReply(isEmpty: boolean) {
    if (isEmpty) {
      return '当前知识库还没有资料内容。请先上传资料，我就可以基于它们回答你的问题了。';
    }
    return '抱歉，AI 服务暂时不可用，请稍后再试。';
  }
}
