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

/**
 * FASE 5 — Embeddings Service
 *
 * Genera y gestiona embeddings de mensajes para búsqueda semántica.
 * Implementación local: en lugar de llamar a un API externo, genera un
 * vector determinista de 1536 dimensiones a partir del hash del contenido
 * (simula la dimensión de text-embedding-3-small de OpenAI).
 *
 * El vector se almacena en la columna `embedding` (pgvector) mediante
 * una query SQL raw, ya que Prisma no soporta tipos `vector` nativamente
 * (el campo se declara como `Unsupported("vector(1536)")`).
 */
@Injectable()
export class EmbeddingsService {
  private readonly logger = new Logger(EmbeddingsService.name);
  private readonly DIM = 1536;
  private readonly MODEL = 'local-hash-1536';

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Genera el embedding de un mensaje y lo almacena.
   * Si ya existe para ese messageId, lo reemplaza.
   */
  async generateForMessage(messageId: string): Promise<{ message_id: string; model: string }> {
    const message = await this.prisma.message.findUnique({
      where: { id: messageId },
      select: { id: true, text: true, workspaceId: true, channelId: true, isDeleted: true },
    });
    if (!message) throw new NotFoundException('Message not found');
    if (message.isDeleted) throw new NotFoundException('Message is deleted');

    const content = (message.text || '').trim() || '(empty message)';
    const vector = this.computeEmbedding(content);

    // Eliminar embedding previo si existe
    await this.prisma.messageEmbedding.deleteMany({ where: { messageId } });

    // Insertar con SQL raw porque Prisma no soporta el tipo vector directamente
    await this.prisma.$executeRaw`
      INSERT INTO ai.message_embeddings (id, workspace_id, message_id, channel_id, content, embedding, model, created_at)
      VALUES (
        gen_random_uuid(),
        ${message.workspaceId}::uuid,
        ${messageId}::uuid,
        ${message.channelId}::uuid,
        ${content},
        ${vector}::vector,
        ${this.MODEL},
        NOW()
      )
    `;

    this.logger.debug(`Embedding generated for message ${messageId}`);
    return { message_id: messageId, model: this.MODEL };
  }

  /**
   * Genera embeddings en lote para los mensajes de un canal.
   * Solo procesa mensajes que no tengan embedding aún.
   */
  async generateForChannel(
    channelId: string,
    wsId: string,
    opts: { limit?: number; force?: boolean } = {},
  ): Promise<{ channel_id: string; processed: number; skipped: number }> {
    const limit = Math.min(opts.limit ?? 200, 1000);

    // Obtener IDs de mensajes que ya tienen embedding (si no forzamos regeneración)
    let existingIds = new Set<string>();
    if (!opts.force) {
      try {
        const existing = await this.prisma.messageEmbedding.findMany({
          where: { channelId },
          select: { messageId: true },
        });
        existingIds = new Set(existing.map((e) => e.messageId));
      } catch {
        // Tabla puede no existir; continuar sin skip
      }
    }

    const messages = await this.prisma.message.findMany({
      where: { channelId, workspaceId: wsId, isDeleted: false, text: { not: null } },
      orderBy: { ts: 'desc' },
      take: limit,
      select: { id: true, text: true, workspaceId: true, channelId: true },
    });

    let processed = 0;
    let skipped = 0;

    for (const msg of messages) {
      if (!opts.force && existingIds.has(msg.id)) {
        skipped++;
        continue;
      }
      try {
        const content = (msg.text || '').trim();
        if (!content) {
          skipped++;
          continue;
        }
        const vector = this.computeEmbedding(content);

        if (!opts.force) {
          await this.prisma.messageEmbedding.deleteMany({ where: { messageId: msg.id } });
        }

        await this.prisma.$executeRaw`
          INSERT INTO ai.message_embeddings (id, workspace_id, message_id, channel_id, content, embedding, model, created_at)
          VALUES (
            gen_random_uuid(),
            ${msg.workspaceId}::uuid,
            ${msg.id}::uuid,
            ${msg.channelId}::uuid,
            ${content},
            ${vector}::vector,
            ${this.MODEL},
            NOW()
          )
        `;
        processed++;
      } catch (err) {
        this.logger.warn(`Failed to embed message ${msg.id}: ${(err as Error).message}`);
        skipped++;
      }
    }

    return { channel_id: channelId, processed, skipped };
  }

  /**
   * Genera embeddings para todos los mensajes recientes del workspace.
   */
  async generateForWorkspace(
    wsId: string,
    opts: { limit?: number } = {},
  ): Promise<{ workspace_id: string; processed: number; skipped: number }> {
    const limit = Math.min(opts.limit ?? 500, 2000);

    const messages = await this.prisma.message.findMany({
      where: { workspaceId: wsId, isDeleted: false, text: { not: null } },
      orderBy: { ts: 'desc' },
      take: limit,
      select: { id: true, text: true, workspaceId: true, channelId: true },
    });

    let processed = 0;
    let skipped = 0;

    for (const msg of messages) {
      try {
        const content = (msg.text || '').trim();
        if (!content) {
          skipped++;
          continue;
        }
        const vector = this.computeEmbedding(content);

        await this.prisma.messageEmbedding.deleteMany({ where: { messageId: msg.id } });

        await this.prisma.$executeRaw`
          INSERT INTO ai.message_embeddings (id, workspace_id, message_id, channel_id, content, embedding, model, created_at)
          VALUES (
            gen_random_uuid(),
            ${msg.workspaceId}::uuid,
            ${msg.id}::uuid,
            ${msg.channelId}::uuid,
            ${content},
            ${vector}::vector,
            ${this.MODEL},
            NOW()
          )
        `;
        processed++;
      } catch (err) {
        this.logger.warn(`Failed to embed message ${msg.id}: ${(err as Error).message}`);
        skipped++;
      }
    }

    return { workspace_id: wsId, processed, skipped };
  }

  /**
   * Elimina el embedding de un mensaje.
   */
  async deleteEmbedding(messageId: string): Promise<{ deleted: true }> {
    await this.prisma.messageEmbedding.deleteMany({ where: { messageId } });
    return { deleted: true };
  }

  /**
   * Devuelve estadísticas de embeddings del workspace.
   */
  async getStats(wsId: string): Promise<{ workspace_id: string; total_embeddings: number; model: string }> {
    const count = await this.prisma.messageEmbedding.count({
      where: { workspaceId: wsId },
    });
    return { workspace_id: wsId, total_embeddings: count, model: this.MODEL };
  }

  /**
   * Genera un vector determinista de 1536 dimensiones a partir del texto.
   * Usa hashing para producir valores reproducibles y normalizados (L2 norm = 1).
   */
  private computeEmbedding(text: string): string {
    const vector = new Float32Array(this.DIM);

    // Generar valores a partir de hashes del texto
    const words = text.toLowerCase().split(/\s+/).filter(Boolean);
    for (const word of words) {
      const hash = this.hashString(word);
      // Distribuir cada palabra en varias dimensiones
      for (let j = 0; j < 4; j++) {
        const idx = (hash + j * 31) % this.DIM;
        vector[idx] += 1.0 / Math.sqrt(words.length * 4);
      }
    }

    // Normalizar L2
    let norm = 0;
    for (let i = 0; i < this.DIM; i++) norm += vector[i] * vector[i];
    norm = Math.sqrt(norm);
    if (norm > 0) {
      for (let i = 0; i < this.DIM; i++) vector[i] /= norm;
    }

    // Convertir a string formato pgvector: "[0.1,0.2,...]"
    const parts: string[] = [];
    for (let i = 0; i < this.DIM; i++) {
      parts.push(vector[i].toFixed(8));
    }
    return `[${parts.join(',')}]`;
  }

  private hashString(str: string): number {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = (hash << 5) - hash + str.charCodeAt(i);
      hash |= 0;
    }
    return Math.abs(hash);
  }
}