import { Injectable, Logger } from "@nestjs/common";
import OpenAI from "openai";
import { PrismaService } from "../../prisma/prisma.service";
import { EMBEDDING_DIMENSION } from "@mosaic/shared";

/**
 * Options for generating embeddings
 */
export interface EmbeddingOptions {
  /**
   * Model to use for embedding generation
   * @default "text-embedding-3-small"
   */
  model?: string;
}

/**
 * Service for generating and managing embeddings using OpenAI API
 */
@Injectable()
export class EmbeddingService {
  private readonly logger = new Logger(EmbeddingService.name);
  private readonly openai: OpenAI | null;
  private readonly defaultModel = "text-embedding-3-small";

  constructor(private readonly prisma: PrismaService) {
    const apiKey = process.env.OPENAI_API_KEY;

    if (!apiKey) {
      this.logger.warn("OPENAI_API_KEY not configured - embedding generation will be disabled");
      this.openai = null;
    } else {
      this.openai = new OpenAI({ apiKey });
    }
  }

  /**
   * Check if the service is properly configured
   */
  isConfigured(): boolean {
    return this.openai !== null;
  }

  /**
   * Generate an embedding vector for the given text
   *
   * @param text - Text to embed
   * @param options - Embedding generation options
   * @returns Embedding vector (array of numbers)
   * @throws Error if OpenAI API key is not configured
   */
  async generateEmbedding(text: string, options: EmbeddingOptions = {}): Promise<number[]> {
    if (!this.openai) {
      throw new Error("OPENAI_API_KEY not configured");
    }

    const model = options.model ?? this.defaultModel;

    try {
      const response = await this.openai.embeddings.create({
        model,
        input: text,
        dimensions: EMBEDDING_DIMENSION,
      });

      const embedding = response.data[0]?.embedding;

      if (!embedding) {
        throw new Error("No embedding returned from OpenAI");
      }

      if (embedding.length !== EMBEDDING_DIMENSION) {
        throw new Error(
          `Unexpected embedding dimension: ${embedding.length.toString()} (expected ${EMBEDDING_DIMENSION.toString()})`
        );
      }

      return embedding;
    } catch (error) {
      this.logger.error("Failed to generate embedding", error);
      throw error;
    }
  }

  /**
   * Generate and store embedding for a knowledge entry
   *
   * @param entryId - ID of the knowledge entry
   * @param content - Content to embed (typically title + content)
   * @param options - Embedding generation options
   * @returns Created/updated embedding record
   */
  async generateAndStoreEmbedding(
    entryId: string,
    content: string,
    options: EmbeddingOptions = {}
  ): Promise<void> {
    if (!this.isConfigured()) {
      this.logger.warn(
        `Skipping embedding generation for entry ${entryId} - OpenAI not configured`
      );
      return;
    }

    const model = options.model ?? this.defaultModel;
    const embedding = await this.generateEmbedding(content, { model });

    // Convert to Prisma-compatible format
    const embeddingString = `[${embedding.join(",")}]`;

    // Upsert the embedding
    await this.prisma.$executeRaw`
      INSERT INTO knowledge_embeddings (id, entry_id, embedding, model, created_at, updated_at)
      VALUES (
        gen_random_uuid(),
        ${entryId}::uuid,
        ${embeddingString}::vector(${EMBEDDING_DIMENSION}),
        ${model},
        NOW(),
        NOW()
      )
      ON CONFLICT (entry_id) DO UPDATE SET
        embedding = ${embeddingString}::vector(${EMBEDDING_DIMENSION}),
        model = ${model},
        updated_at = NOW()
    `;

    this.logger.log(`Generated and stored embedding for entry ${entryId}`);
  }

  /**
   * Batch process embeddings for multiple entries
   *
   * @param entries - Array of {id, content} objects
   * @param options - Embedding generation options
   * @returns Number of embeddings successfully generated
   */
  async batchGenerateEmbeddings(
    entries: { id: string; content: string }[],
    options: EmbeddingOptions = {}
  ): Promise<number> {
    if (!this.isConfigured()) {
      this.logger.warn("Skipping batch embedding generation - OpenAI not configured");
      return 0;
    }

    let successCount = 0;

    for (const entry of entries) {
      try {
        await this.generateAndStoreEmbedding(entry.id, entry.content, options);
        successCount++;
      } catch (error) {
        this.logger.error(`Failed to generate embedding for entry ${entry.id}`, error);
      }
    }

    this.logger.log(
      `Batch generated ${successCount.toString()}/${entries.length.toString()} embeddings`
    );
    return successCount;
  }

  /**
   * Delete embedding for a knowledge entry
   *
   * @param entryId - ID of the knowledge entry
   */
  async deleteEmbedding(entryId: string): Promise<void> {
    await this.prisma.knowledgeEmbedding.deleteMany({
      where: { entryId },
    });

    this.logger.log(`Deleted embedding for entry ${entryId}`);
  }

  /**
   * Prepare content for embedding
   * Combines title and content with appropriate weighting
   *
   * @param title - Entry title
   * @param content - Entry content (markdown)
   * @returns Combined text for embedding
   */
  prepareContentForEmbedding(title: string, content: string): string {
    // Weight title more heavily by repeating it
    // This helps with semantic search matching on titles
    return `${title}\n\n${title}\n\n${content}`.trim();
  }
}
