import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@chatapp/database';

/**
 * FASE 5 — AI Search Service
 *
 * Búsqueda en lenguaje natural sobre los mensajes del workspace.
 * Implementación local (sin LLM externo): tokeniza la query, extrae
 * palabras clave y busca coincidencias por texto + embeddings si
 * existen. Cachea resultados en AiSearchCache.
 */
@Injectable()
export class AiSearchService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Realiza una búsqueda semántica en lenguaje natural.
   * Estrategia:
   *  1. Si existen embeddings para mensajes del workspace, usa similitud
   *     de coseno simulada por coincidencia de términos en el contenido.
   *  2. Si no, hace una búsqueda por texto completo (contains, insensible).
   *  3. Genera una respuesta resumida con los mensajes encontrados.
   */
  async search(
    wsId: string,
    userId: string,
    query: string,
    opts: { channelId?: string; limit?: number } = {},
  ): Promise<{
    query: string;
    answer: string;
    results: any[];
    cached: boolean;
  }> {
    const limit = Math.min(opts.limit ?? 20, 100);

    // 1. Verificar cache
    const cached = await this.prisma.aiSearchCache.findFirst({
      where: { workspaceId: wsId, userId, query },
      orderBy: { createdAt: 'desc' },
    });

    if (cached) {
      const cachedMessages = await this.prisma.message.findMany({
        where: { id: { in: cached.sourceMessageIds } },
        include: {
          user: { select: { id: true, displayName: true } },
          channel: { select: { id: true, name: true } },
        },
      });
      return {
        query,
        answer: cached.answer,
        results: cachedMessages.map((m) => this.formatMessage(m)),
        cached: true,
      };
    }

    // 2. Extraer palabras clave de la query
    const keywords = this.extractKeywords(query);

    // 3. Buscar mensajes por texto
    const where: any = {
      workspaceId: wsId,
      isDeleted: false,
    };
    if (opts.channelId) where.channelId = opts.channelId;

    let messages: any[] = [];
    if (keywords.length > 0) {
      where.OR = keywords.map((kw) => ({
        text: { contains: kw, mode: 'insensitive' as const },
      }));
      messages = await this.prisma.message.findMany({
        where,
        orderBy: { ts: 'desc' },
        take: limit,
        include: {
          user: { select: { id: true, displayName: true } },
          channel: { select: { id: true, name: true } },
        },
      });
    }

    // 4. Si hay embeddings disponibles, enriquecer con ellos
    const embeddingMatches = await this.searchByEmbeddings(wsId, keywords, limit);
    if (embeddingMatches.length > 0) {
      const existingIds = new Set(messages.map((m) => m.id));
      const extra = await this.prisma.message.findMany({
        where: {
          id: { in: embeddingMatches.filter((id) => !existingIds.has(id)) },
          isDeleted: false,
        },
        take: limit - messages.length,
        include: {
          user: { select: { id: true, displayName: true } },
          channel: { select: { id: true, name: true } },
        },
      });
      messages = [...messages, ...extra];
    }

    // 5. Generar respuesta
    if (messages.length === 0) {
      const answer = `No se encontraron mensajes que coincidan con tu búsqueda: "${query}".`;
      await this.cacheResult(wsId, userId, query, answer, []);
      return { query, answer, results: [], cached: false };
    }

    const answer = this.buildAnswer(query, messages);

    // 6. Cachear resultado
    await this.cacheResult(wsId, userId, query, answer, messages.map((m) => m.id));

    return {
      query,
      answer,
      results: messages.map((m) => this.formatMessage(m)),
      cached: false,
    };
  }

  /**
   * Devuelve el historial de búsquedas AI del usuario en el workspace.
   */
  async getHistory(wsId: string, userId: string, limit = 20) {
    const items = await this.prisma.aiSearchCache.findMany({
      where: { workspaceId: wsId, userId },
      orderBy: { createdAt: 'desc' },
      take: Math.min(limit, 100),
    });
    return items.map((c) => ({
      id: c.id,
      query: c.query,
      answer: c.answer,
      source_count: c.sourceMessageIds.length,
      created_at: c.createdAt,
    }));
  }

  // ------------------------------------------------------------------

  private extractKeywords(query: string): string[] {
    return query
      .toLowerCase()
      .replace(/[^\w\sáéíóúñü]/g, ' ')
      .split(/\s+/)
      .filter((w) => w.length > 2 && !this.isStopWord(w));
  }

  private async searchByEmbeddings(
    wsId: string,
    keywords: string[],
    limit: number,
  ): Promise<string[]> {
    if (keywords.length === 0) return [];

    // Buscar embeddings cuyo contenido contiene palabras clave
    // (No usamos pgvector directamente para mantener compatibilidad;
    // hacemos un filtro por contenido en la tabla de embeddings.)
    try {
      const embeddings = await this.prisma.messageEmbedding.findMany({
        where: {
          workspaceId: wsId,
          OR: keywords.map((kw) => ({
            content: { contains: kw, mode: 'insensitive' as const },
          })),
        },
        take: limit,
        orderBy: { createdAt: 'desc' },
        select: { messageId: true },
      });
      return embeddings.map((e) => e.messageId);
    } catch {
      // Si la tabla no existe o hay error de tipos vector, ignoramos
      return [];
    }
  }

  private buildAnswer(query: string, messages: any[]): string {
    const top = messages.slice(0, 5);
    const parts = top.map((m) => {
      const text = m.text ? m.text.substring(0, 200) : '(sin texto)';
      const channel = m.channel?.name ?? 'desconocido';
      const user = m.user?.displayName ?? 'Usuario';
      return `En #${channel}, ${user} dijo: "${text}${m.text && m.text.length > 200 ? '...' : ''}"`;
    });
    return `Se encontraron ${messages.length} mensajes relevantes para: "${query}".\n\n${parts.join('\n\n')}`;
  }

  private async cacheResult(
    wsId: string,
    userId: string,
    query: string,
    answer: string,
    sourceIds: string[],
  ) {
    await this.prisma.aiSearchCache.create({
      data: {
        workspaceId: wsId,
        userId,
        query,
        answer,
        sourceMessageIds: sourceIds,
        model: 'local-keyword-search',
      },
    });
  }

  private formatMessage(m: any) {
    return {
      message_id: m.id,
      channel_id: m.channelId,
      channel_name: m.channel?.name,
      user_id: m.userId,
      user_name: m.user?.displayName,
      text: m.text,
      ts: m.ts,
    };
  }

  private isStopWord(word: string): boolean {
    const stop = new Set([
      'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
      'of', 'with', 'by', 'from', 'is', 'are', 'was', 'were', 'be', 'been',
      'que', 'de', 'la', 'el', 'en', 'y', 'los', 'del', 'las', 'un', 'una',
      'por', 'con', 'para', 'se', 'su', 'es', 'lo', 'como', 'mas', 'pero',
      'sus', 'le', 'ya', 'o', 'este', 'si', 'porque', 'esta', 'entre',
      'cuando', 'muy', 'sin', 'sobre', 'tambien', 'hasta', 'hay',
      'what', 'which', 'who', 'when', 'where', 'why', 'how', 'all',
      'any', 'about', 'this', 'that', 'these', 'those',
    ]);
    return stop.has(word);
  }
}