import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { ElasticsearchService } from '../elasticsearch/elasticsearch.service';
import { EmbeddingService } from '../knowledge-base/embedding.service';
import { ModelConfigService } from '../model-config/model-config.service';
import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service';
import { SearchHistoryService } from '../search-history/search-history.service';
import { ModelConfig, ModelType, ChatStreamChunk, HistoryMessage, SourceReference, StatusMessage } from '../types';
import { RagService } from '../rag/rag.service';

import { DEFAULT_VECTOR_DIMENSIONS, DEFAULT_LANGUAGE } from '../common/constants';
import { I18nService } from '../i18n/i18n.service';
import { TenantService } from '../tenant/tenant.service';
import { UserSettingService } from '../user/user-setting.service';

export interface ChatMessage {
  role: 'user' | 'assistant';
  content: string;
}

// Thinking detection patterns
interface ThinkingPattern {
  start: RegExp;
  end: RegExp;
}

const THINKING_PATTERNS: Record<string, ThinkingPattern> = {
  xml: {
    start: /<thinking>/i,
    end: /<\/thinking>/i,
  },
  codeBlock: {
    start: /```thinking/i,
    end: /```/i,
  },
  bracket: {
    start: /\[THINKING\]/i,
    end: /\[\/THINKING\]/i,
  },
};

// Stream processing state
enum StreamState {
  WAITING,           // Waiting for first chunk
  IN_THINKING,       // Receiving thinking content
  IN_CONTENT,        // Receiving content
}

interface StreamProcessor {
  state: StreamState;
  buffer: string;
  thinkingContent: string;
  contentContent: string;
  thinkingDetected: boolean;
  detectedFormat: string | null;
}

@Injectable()
export class ChatService {
  private readonly logger = new Logger(ChatService.name);
  private readonly defaultDimensions: number;

  constructor(
    @Inject(forwardRef(() => ElasticsearchService))
    private elasticsearchService: ElasticsearchService,
    private embeddingService: EmbeddingService,
    private modelConfigService: ModelConfigService,
    @Inject(forwardRef(() => KnowledgeGroupService))
    private knowledgeGroupService: KnowledgeGroupService,
    private searchHistoryService: SearchHistoryService,
    private configService: ConfigService,
    private ragService: RagService,
    private i18nService: I18nService,
    private tenantService: TenantService,
    private userSettingService: UserSettingService,
  ) {
    this.defaultDimensions = parseInt(
      this.configService.get<string>('DEFAULT_VECTOR_DIMENSIONS', String(DEFAULT_VECTOR_DIMENSIONS)),
    );
  }

  /**
   * Detect thinking format in buffer
   */
  private detectThinkingFormat(buffer: string): string | null {
    for (const [format, pattern] of Object.entries(THINKING_PATTERNS)) {
      if (pattern.start.test(buffer)) {
        return format;
      }
    }
    return null;
  }

  /**
   * Extract thinking and content from response
   */
  private extractThinking(response: string): { thinking: string; content: string } {
    // Format 1: <thinking>...</thinking>
    const thinkingTagMatch = response.match(/<thinking>([\s\S]*?)<\/thinking>/i);
    if (thinkingTagMatch) {
      const thinking = thinkingTagMatch[1].trim();
      const content = response.replace(/<thinking>[\s\S]*?<\/thinking>/i, '').trim();
      return { thinking, content };
    }

    // Format 2: ```thinking\n...\n```
    const codeBlockMatch = response.match(/```thinking\n([\s\S]*?)\n```/i);
    if (codeBlockMatch) {
      const thinking = codeBlockMatch[1].trim();
      const content = response.replace(/```thinking\n[\s\S]*?\n```/i, '').trim();
      return { thinking, content };
    }

    // Format 3: [THINKING]...[/THINKING]
    const bracketMatch = response.match(/\[THINKING\]([\s\S]*?)\[\/THINKING\]/i);
    if (bracketMatch) {
      const thinking = bracketMatch[1].trim();
      const content = response.replace(/\[THINKING\][\s\S]*?\[\/THINKING\]/i, '').trim();
      return { thinking, content };
    }

    // No thinking detected
    return { thinking: '', content: response };
  }

  /**
   * Process stream with dynamic thinking detection
   */
  private async *processStreamWithThinking(
    stream: AsyncIterable<{ content: string | any }>,
  ): AsyncGenerator<ChatStreamChunk> {
    const processor: StreamProcessor = {
      state: StreamState.WAITING,
      buffer: '',
      thinkingContent: '',
      contentContent: '',
      thinkingDetected: false,
      detectedFormat: null,
    };

    for await (const chunk of stream) {
      const chunkContent = typeof chunk.content === 'string' 
        ? chunk.content 
        : String(chunk.content || '');
      
      if (!chunkContent) continue;

      processor.buffer += chunkContent;

      switch (processor.state) {
        case StreamState.WAITING:
          // Check for thinking tag at start
          const format = this.detectThinkingFormat(processor.buffer);
          if (format) {
            processor.state = StreamState.IN_THINKING;
            processor.thinkingDetected = true;
            processor.detectedFormat = format;
            
            // Extract content after opening tag
            const pattern = THINKING_PATTERNS[format];
            const parts = processor.buffer.split(pattern.start);
            processor.buffer = parts[1] || '';
            
            // Check if end tag is already in buffer
            if (pattern.end.test(processor.buffer)) {
              const endParts = processor.buffer.split(pattern.end);
              processor.thinkingContent = endParts[0];
              processor.buffer = endParts[1] || '';
              processor.state = StreamState.IN_CONTENT;
              
              yield { type: 'thinking', data: processor.thinkingContent };
              processor.thinkingContent = ''; // Clear to avoid duplicate yield
              
              if (processor.buffer) {
                yield { type: 'content', data: processor.buffer };
                processor.buffer = '';
              }
            }
          } else if (processor.buffer.length > 100) {
            // No thinking detected after 100 chars, switch to content mode
            processor.state = StreamState.IN_CONTENT;
            yield { type: 'content', data: processor.buffer };
            processor.buffer = '';
          }
          break;

        case StreamState.IN_THINKING:
          const currentPattern = THINKING_PATTERNS[processor.detectedFormat!];
          if (currentPattern.end.test(processor.buffer)) {
            // End of thinking section
            const endParts = processor.buffer.split(currentPattern.end);
            const lastThinkingPart = endParts[0];
            processor.buffer = endParts[1] || '';
            processor.state = StreamState.IN_CONTENT;

            // Only yield the last part if it hasn't been yielded yet
            if (lastThinkingPart && lastThinkingPart !== processor.thinkingContent) {
              yield { type: 'thinking', data: lastThinkingPart };
            }
            processor.thinkingContent = '';

            if (processor.buffer) {
              yield { type: 'content', data: processor.buffer };
              processor.buffer = '';
            }
          } else {
            // Continue accumulating thinking content and yield chunk
            processor.thinkingContent += chunkContent;
            yield { type: 'thinking', data: chunkContent };
          }
          break;

        case StreamState.IN_CONTENT:
          // Direct content output
          yield { type: 'content', data: chunkContent };
          break;
      }
    }

    // Handle remaining buffer only if not already yielded
    if (processor.buffer && processor.state !== StreamState.IN_CONTENT) {
      if (processor.state === StreamState.IN_THINKING) {
        yield { type: 'thinking', data: processor.buffer };
      } else {
        yield { type: 'content', data: processor.buffer };
      }
    }
  }

  async *streamChat(
    message: string,
    history: ChatMessage[],
    userId: string,
    modelConfig: ModelConfig,
    userLanguage: string = DEFAULT_LANGUAGE,
    selectedEmbeddingId?: string,
    selectedGroups?: string[], // New: Selected groups
    selectedFiles?: string[], // New: Selected files
    historyId?: string, // New: Chat history ID
    enableRerank: boolean = false,
    selectedRerankId?: string,
    temperature?: number, // New: temperature parameter
    maxTokens?: number, // New: maxTokens parameter
    topK?: number, // New: topK parameter
    similarityThreshold?: number, // New: similarityThreshold parameter
    rerankSimilarityThreshold?: number, // New: rerankSimilarityThreshold parameter
    enableQueryExpansion?: boolean, // New
    enableHyDE?: boolean, // New
    tenantId?: string // New: tenant isolation
  ): AsyncGenerator<ChatStreamChunk> {
    console.log('=== ChatService.streamChat ===');
    console.log('User ID:', userId);
    console.log('User language:', userLanguage);
    console.log('Selected embedding model ID:', selectedEmbeddingId);
    console.log('Selected groups:', selectedGroups);
    console.log('Selected files:', selectedFiles);
    console.log('History ID:', historyId);
    console.log('Temperature:', temperature);
    console.log('Max Tokens:', maxTokens);
    console.log('Top K:', topK);
    console.log('Similarity threshold:', similarityThreshold);
    console.log('Rerank threshold:', rerankSimilarityThreshold);
    console.log('Query expansion:', enableQueryExpansion);
    console.log('HyDE:', enableHyDE);
    console.log('Model configuration:', {
      name: modelConfig.name,
      modelId: modelConfig.modelId,
      baseUrl: modelConfig.baseUrl,
    });
    console.log('API Key prefix:', modelConfig.apiKey?.substring(0, 10) + '...');
    console.log('API Key length:', modelConfig.apiKey?.length);

    // Get current language setting (keeping LANGUAGE_CONFIG for backward compatibility, now uses i18n service)
    // Use actual language based on user settings
    const effectiveUserLanguage = userLanguage || DEFAULT_LANGUAGE;

    let currentHistoryId = historyId;
    let fullResponse = '';

    try {
      // Create new chat history if no historyId
      if (!currentHistoryId) {
        const searchHistory = await this.searchHistoryService.create(
          userId,
          tenantId || 'default', // New
          message,
          selectedGroups,
        );
        currentHistoryId = searchHistory.id;
        console.log(this.i18nService.getMessage('creatingHistory', effectiveUserLanguage) + currentHistoryId);
        yield { type: 'historyId', data: currentHistoryId };
      }

      // Save user message
      await this.searchHistoryService.addMessage(currentHistoryId, 'user', message);
      
      // Return history (上文) if there are previous messages
      if (history && history.length > 0) {
        yield {
          type: 'history',
          data: {
            messages: history.map(msg => ({
              role: msg.role,
              content: msg.content,
            })),
          },
        };
      }
      
      // 1. Get user's embedding model settings
      let embeddingModel: any;

      if (selectedEmbeddingId) {
        // Find specifically selected model
        embeddingModel = await this.modelConfigService.findOne(selectedEmbeddingId);
      } else {
        // Use organization's default from Index Chat Config (strict)
        embeddingModel = await this.modelConfigService.findDefaultByType(tenantId || 'default', ModelType.EMBEDDING);
      }

      console.log(this.i18nService.getMessage('usingEmbeddingModel', effectiveUserLanguage) + embeddingModel.name + ' ' + embeddingModel.modelId + ' ID:' + embeddingModel.id);

      // 2. Search using user's query directly
      console.log(this.i18nService.getMessage('startingSearch', effectiveUserLanguage));
      yield { 
        type: 'status', 
        data: { 
          stage: 'searching', 
          message: this.i18nService.getMessage('searching', effectiveUserLanguage) 
        } 
      };

      let searchResults: any[] = [];
      let context = '';

      try {
        // 3. If knowledge groups are selected, get file IDs from those groups first
        let effectiveFileIds = selectedFiles; // Prioritize explicitly specified files
        if (!effectiveFileIds && selectedGroups && selectedGroups.length > 0) {
          // Get file IDs from knowledge groups
          effectiveFileIds = await this.knowledgeGroupService.getFileIdsByGroups(selectedGroups, userId, tenantId as string);
        }

        // 3. Use RagService for search (supports hybrid search + Rerank)
        const ragResults = await this.ragService.searchKnowledge(
          message,
          userId,
          topK,
          similarityThreshold,
          embeddingModel.id,
          true, // enableFullTextSearch (Chat defaults to hybrid)
          enableRerank,
          selectedRerankId,
          undefined, // selectedGroups
          effectiveFileIds,
          rerankSimilarityThreshold,
          tenantId,
          enableQueryExpansion,
          enableHyDE
        );

        // Convert RagSearchResult to format needed by ChatService (any[])
        // HybridSearch returns ES hit structure, but RagSearchResult is normalized
        // BuildContext expects {fileName, content}. RagSearchResult has these
        searchResults = ragResults;
        console.log(this.i18nService.getMessage('searchResultsCount', effectiveUserLanguage) + searchResults.length);

        // 4. Build context
        context = this.buildContext(searchResults, effectiveUserLanguage);

        if (searchResults.length === 0) {
          if (selectedGroups && selectedGroups.length > 0) {
            // User selected knowledge groups but no matches found
            const noMatchMsg = this.i18nService.getMessage('noMatchInKnowledgeGroup', effectiveUserLanguage);
            yield { type: 'status', data: { stage: 'search_complete', message: `⚠️ ${noMatchMsg}` } };
          } else {
            yield { type: 'status', data: { stage: 'search_complete', message: this.i18nService.getMessage('noResults', effectiveUserLanguage) } };
          }
          yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('searchScope', effectiveUserLanguage)}: ${selectedFiles ? selectedFiles.length + ' ' + this.i18nService.getMessage('files', effectiveUserLanguage) : selectedGroups ? selectedGroups.length + ' ' + this.i18nService.getMessage('notebooks', effectiveUserLanguage) : this.i18nService.getMessage('all', effectiveUserLanguage)}` } };
          yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('searchResults', effectiveUserLanguage)}: 0 ${this.i18nService.getMessage('items', effectiveUserLanguage)}` } };
        } else {
          yield {
            type: 'status',
            data: { 
              stage: 'search_complete', 
              message: `${searchResults.length} ${this.i18nService.getMessage('relevantInfoFound', effectiveUserLanguage)}。${this.i18nService.getMessage('generatingResponse', effectiveUserLanguage)}...` 
            },
          };
          // Debug info
          const scores = searchResults.map(r => {
            if (r.originalScore !== undefined && r.originalScore !== r.score) {
              return `${r.originalScore.toFixed(2)} → ${r.score.toFixed(2)}`;
            }
            return r.score.toFixed(2);
          }).join(', ');
          const files = [...new Set(searchResults.map(r => r.fileName))].join(', ');
          yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('searchHits', effectiveUserLanguage)}: ${searchResults.length} ${this.i18nService.getMessage('items', effectiveUserLanguage)}` } };
          yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('relevance', effectiveUserLanguage)}: ${scores}` } };
          yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('sourceFiles', effectiveUserLanguage)}: ${files}` } };
        }
      } catch (searchError) {
        console.error(this.i18nService.getMessage('searchFailedLog', effectiveUserLanguage) + ':', searchError);
        yield { type: 'content', data: this.i18nService.getMessage('searchFailed', effectiveUserLanguage) + '\n\n' };
      }

      // 5. Stream response generation
      this.logger.log(this.i18nService.formatMessage('modelCall', {
        type: 'LLM',
        model: `${modelConfig.name} (${modelConfig.modelId})`,
        user: userId
      }, effectiveUserLanguage));
      
      yield { type: 'status', data: { stage: 'generating', message: this.i18nService.getMessage('generatingResponse', effectiveUserLanguage) } };
      
      const llm = new ChatOpenAI({
        apiKey: modelConfig.apiKey || 'ollama',
        streaming: true,
        temperature: temperature !== undefined ? temperature : 0.3,
        maxTokens: maxTokens !== undefined ? maxTokens : undefined,
        modelName: modelConfig.modelId,
        configuration: {
          baseURL: modelConfig.baseUrl || 'http://localhost:11434/v1',
        },
      });

      const promptTemplate =
        context.length > 0
          ? this.i18nService.getPrompt(
            effectiveUserLanguage,
            'withContext',
            selectedGroups && selectedGroups.length > 0
          )
          : this.i18nService.getPrompt(effectiveUserLanguage, 'withoutContext');

      const prompt = PromptTemplate.fromTemplate(promptTemplate);

      const chain = prompt.pipe(llm);

      const stream = await chain.stream({
        context,
        history: this.formatHistory(history, userLanguage),
        question: message,
      });

      // Process stream with dynamic thinking detection
      for await (const chunk of this.processStreamWithThinking(stream)) {
        if (chunk.type === 'content' || chunk.type === 'thinking') {
          fullResponse += chunk.data;
        }
        yield chunk;
      }

      // Save AI response
      await this.searchHistoryService.addMessage(
        currentHistoryId,
        'assistant',
        fullResponse,
        searchResults.map((result) => ({
          fileName: result.fileName,
          title: result.metadata?.title || result.metadata?.originalName, // ES metadata contains these
          content: String(result.content).substring(0, 200) + '...',
          score: result.score,
          chunkIndex: result.chunkIndex,
          fileId: result.fileId,
        })),
      );

      // 7. Auto-generate chat title (executed after first exchange)
      const messagesInHistory = await this.searchHistoryService.findOne(currentHistoryId, userId, tenantId);
      if (messagesInHistory.messages.length === 2) {
        this.generateChatTitle(currentHistoryId, userId, tenantId).catch((err) => {
          this.logger.error(`Failed to generate chat title for ${currentHistoryId}`, err);
        });
      }

      // 6. Return sources
      yield {
        type: 'sources',
        data: searchResults.map((result) => ({
          fileName: result.fileName,
          content: String(result.content).substring(0, 200) + '...',
          score: result.score,
          chunkIndex: result.chunkIndex,
          fileId: result.fileId,
        })),
      };
    } catch (error) {
      this.logger.error(this.i18nService.getMessage('chatStreamError', effectiveUserLanguage), error);
      yield { type: 'status', data: { stage: 'debug', message: `${this.i18nService.getMessage('error', effectiveUserLanguage)}: ${error.message}` } };
    }
  }

  async *streamAssist(
    instruction: string,
    context: string,
    modelConfig: ModelConfig,
    userLanguage: string = DEFAULT_LANGUAGE,
  ): AsyncGenerator<{ type: 'content'; data: any }> {
    try {
      this.logger.log(this.i18nService.formatMessage('modelCall', {
        type: 'LLM (Assist)',
        model: `${modelConfig.name} (${modelConfig.modelId})`,
        user: 'N/A'
      }, userLanguage));
      const llm = new ChatOpenAI({
        apiKey: modelConfig.apiKey || 'ollama',
        streaming: true,
        temperature: 0.7,
        modelName: modelConfig.modelId,
        configuration: {
          baseURL: modelConfig.baseUrl || 'http://localhost:11434/v1',
        },
      });

      const systemPrompt = `${this.i18nService.getMessage('intelligentAssistant', userLanguage)}
${this.i18nService.getMessage('assistSystemPrompt', userLanguage)}

${this.i18nService.getMessage('contextLabel', userLanguage)}:
${context}

${this.i18nService.getMessage('userInstructionLabel', userLanguage)}:
${instruction}`;

      const stream = await llm.stream(systemPrompt);

      for await (const chunk of stream) {
        if (chunk.content) {
          yield { type: 'content', data: chunk.content };
        }
      }
    } catch (error) {
      this.logger.error(this.i18nService.getMessage('assistStreamError', userLanguage), error);
      yield { type: 'content', data: `${this.i18nService.getMessage('error', userLanguage)}: ${error.message}` };
    }
  }

  private async hybridSearch(
    keywords: string[],
    userId: string,
    embeddingModelId?: string,
    selectedGroups?: string[], // New parameter
    explicitFileIds?: string[], // New parameter
    tenantId?: string, // Added
    userLanguage: string = DEFAULT_LANGUAGE,
  ): Promise<any[]> {
    try {
      // Join keywords into search string
      const combinedQuery = keywords.join(' ');
      console.log(this.i18nService.getMessage('searchString', userLanguage) + combinedQuery);

      // Check if embedding model ID is provided
      if (!embeddingModelId) {
        console.log(this.i18nService.getMessage('embeddingModelIdNotProvided', userLanguage));
        return [];
      }

      // Use actual embedding vector
      console.log(this.i18nService.getMessage('generatingEmbeddings', userLanguage));
      const queryEmbedding = await this.embeddingService.getEmbeddings(
        [combinedQuery],
        userId,
        embeddingModelId,
      );
      const queryVector = queryEmbedding[0];
      console.log(this.i18nService.getMessage('embeddingsGenerated', userLanguage) + this.i18nService.getMessage('dimensions', userLanguage) + ':', queryVector.length);

      // Hybrid search
      console.log(this.i18nService.getMessage('performingHybridSearch', userLanguage));
      const results = await this.elasticsearchService.hybridSearch(
        queryVector,
        combinedQuery,
        userId,
        10,
        0.6,
        selectedGroups, // Pass selected groups
        explicitFileIds, // Pass explicit file IDs
        tenantId, // Pass tenant ID
      );
      console.log(this.i18nService.getMessage('esSearchCompleted', userLanguage) + this.i18nService.getMessage('resultsCount', userLanguage) + ':', results.length);

      return results.slice(0, 10);
    } catch (error) {
      console.error(this.i18nService.getMessage('hybridSearchFailed', userLanguage) + ':', error);
      return [];
    }
  }

  private buildContext(results: any[], language: string = 'ja'): string {
    return results
      .map(
        (result, index) =>
          `[${index + 1}] ${this.i18nService.getMessage('file', language)}：${result.fileName}\n${this.i18nService.getMessage('content', language)}：${result.content}\n`,
      )
      .join('\n');
  }

  private formatHistory(
    history: ChatMessage[],
    userLanguage: string = 'ja',
  ): string {
    const userLabel = this.i18nService.getMessage('userLabel', userLanguage);
    const assistantLabel = this.i18nService.getMessage('assistantLabel', userLanguage);

    return history
      .slice(-6)
      .map(
        (msg) =>
          `${msg.role === 'user' ? userLabel : assistantLabel}：${msg.content}`,
      )
      .join('\n');
  }
  async getContextForTopic(topic: string, userId: string, tenantId?: string, groupId?: string, fileIds?: string[], userLanguage: string = DEFAULT_LANGUAGE): Promise<string> {
    try {
      // Use organization's default embedding from Index Chat Config (strict)
      const embeddingModel = await this.modelConfigService.findDefaultByType(tenantId || 'default', ModelType.EMBEDDING);

      const results = await this.hybridSearch(
        [topic],
        userId,
        embeddingModel.id,
        groupId ? [groupId] : undefined,
        fileIds,
        tenantId,
        userLanguage,
      );

      return this.buildContext(results);
    } catch (err) {
      this.logger.error(`${this.i18nService.getMessage('getContextForTopicFailed', userLanguage)}: ${err.message}`);
      return '';
    }
  }

  async generateSimpleChat(
    messages: ChatMessage[],
    userId: string,
    tenantId?: string,
    modelConfig?: ModelConfig, // Optional, looks up if not provided
    userLanguage: string = DEFAULT_LANGUAGE,
  ): Promise<string> {
    try {
      let config = modelConfig;
      if (!config) {
        // Use organization's default LLM from Index Chat Config (strict)
        const found = await this.modelConfigService.findDefaultByType(tenantId || 'default', ModelType.LLM);
        config = found as unknown as ModelConfig;
      }

      this.logger.log(this.i18nService.formatMessage('modelCall', {
        type: 'LLM (Simple)',
        model: `${config.name} (${config.modelId})`,
        user: userId
      }, 'ja'));
      const settings = await this.tenantService.getSettings(tenantId || 'default');
      const llm = new ChatOpenAI({
        apiKey: config.apiKey || 'ollama',
        temperature: settings?.temperature ?? 0.7,
        modelName: config.modelId,
        configuration: {
          baseURL: config.baseUrl || 'http://localhost:11434/v1',
        },
      });

      const response = await llm.invoke(
        messages.map(m => [m.role, m.content])
      );

      return String(response.content);
    } catch (error) {
      this.logger.error(this.i18nService.getMessage('simpleChatGenerationError', userLanguage), error);
      throw error;
    }
  }

  /**
   * Automatically generate chat title based on conversation content
   */
  async generateChatTitle(historyId: string, userId: string, tenantId?: string): Promise<string | null> {
    this.logger.log(`Generating automatic title for chat session ${historyId}`);

    try {
      const history = await this.searchHistoryService.findOne(historyId, userId, tenantId || 'default');
      if (!history || history.messages.length < 2) {
        return null;
      }

      const userMessage = history.messages.find(m => m.role === 'user')?.content || '';
      const aiResponse = history.messages.find(m => m.role === 'assistant')?.content || '';

      if (!userMessage || !aiResponse) {
        return null;
      }

      // Get language from user settings
      const userSettings = await this.userSettingService.getByUser(userId);
      const language = userSettings?.language || 'ja';

      // Build prompt
      const prompt = this.i18nService.getChatTitlePrompt(language, userMessage, aiResponse);

      // Call LLM to generate title
      const generatedTitle = await this.generateSimpleChat(
        [{ role: 'user', content: prompt }],
        userId,
        tenantId || 'default'
      );

      if (generatedTitle && generatedTitle.trim().length > 0) {
        // Remove extra quotes
        const cleanedTitle = generatedTitle.trim().replace(/^["']|["']$/g, '').substring(0, 50);
        await this.searchHistoryService.updateTitle(historyId, cleanedTitle);
        this.logger.log(`Successfully generated title for chat ${historyId}: ${cleanedTitle}`);
        return cleanedTitle;
      }
    } catch (error) {
      this.logger.error(`Failed to generate chat title for ${historyId}`, error);
    }

    return null;
  }
}
