/**
 * Pinecone Multimodal Service
 *
 * Core service for multimodal knowledge base operations using Pinecone vector DB
 * and Gemini embeddings. Supports text, image, video, and audio content with
 * per-tenant namespace isolation.
 *
 * Architecture:
 * - Singleton Pinecone and Gemini clients (lazy initialized)
 * - Per-tenant namespaces: `tenant-{userId}`
 * - Gemini Embedding exp-03-07 for 3072-dim embeddings
 * - Gemini 2.5 Flash for media description generation
 */

import { Pinecone } from '@pinecone-database/pinecone';
import { GoogleGenAI } from '@google/genai';
import { logger } from '@/lib/logger';
import type {
  KBMediaType,
  RAGQueryResponse,
  RAGCitation,
} from '@/lib/types/knowledge-base.types';
import { getMediaType } from '@/lib/types/knowledge-base.types';

const scopedLogger = logger.scope('PineconeMultimodal');

// --- Singleton clients ---

let pineconeClient: Pinecone | null = null;
let geminiClient: GoogleGenAI | null = null;

function getPineconeClient(): Pinecone {
  if (!pineconeClient) {
    const apiKey = process.env.PINECONE_API_KEY;
    if (!apiKey) {
      throw new Error('PINECONE_API_KEY environment variable is not set');
    }
    pineconeClient = new Pinecone({ apiKey });
  }
  return pineconeClient;
}

function getGeminiClient(): GoogleGenAI {
  if (!geminiClient) {
    const apiKey = process.env.GOOGLE_API_KEY;
    if (!apiKey) {
      throw new Error('GOOGLE_API_KEY environment variable is not set');
    }
    geminiClient = new GoogleGenAI({ apiKey });
  }
  return geminiClient;
}

// --- Constants ---

const INDEX_NAME = process.env.PINECONE_INDEX_NAME || 'mawidi-multimodal-kb';
const EMBEDDING_MODEL = 'gemini-embedding-2-preview';
const GENERATION_MODEL = 'gemini-2.5-flash';
const EMBEDDING_DIMENSION = 3072;
const PINECONE_BATCH_SIZE = 100;

function tenantNamespace(userId: string): string {
  return `tenant-${userId}`;
}

// --- Metadata type stored in Pinecone ---

type PineconeVectorMetadata = Record<string, string | number | boolean | string[]> & {
  title: string;
  category: string;
  mediaType: string;
  documentId: string;
  createdAt: string;
};

export class PineconeMultimodalService {
  /**
   * Check if Pinecone index exists, create if not.
   * Uses serverless spec (AWS us-east-1), 3072 dimensions, cosine metric.
   */
  static async initializeIndex(): Promise<void> {
    const pc = getPineconeClient();

    scopedLogger.info('Checking Pinecone index', { indexName: INDEX_NAME });

    try {
      const indexList = await pc.listIndexes();
      const exists = indexList.indexes?.some((idx) => idx.name === INDEX_NAME);

      if (exists) {
        scopedLogger.info('Pinecone index already exists', { indexName: INDEX_NAME });
        return;
      }

      scopedLogger.info('Creating Pinecone index', {
        indexName: INDEX_NAME,
        dimension: EMBEDDING_DIMENSION,
      });

      await pc.createIndex({
        name: INDEX_NAME,
        dimension: EMBEDDING_DIMENSION,
        metric: 'cosine',
        spec: {
          serverless: {
            cloud: 'aws',
            region: 'us-east-1',
          },
        },
      });

      scopedLogger.info('Pinecone index created successfully', { indexName: INDEX_NAME });
    } catch (error) {
      scopedLogger.error('Failed to initialize Pinecone index', error, {
        indexName: INDEX_NAME,
      });
      throw error;
    }
  }

  /**
   * Generate embedding for content.
   * - For text (string): embeds directly using Gemini Embedding API with RETRIEVAL_DOCUMENT task type
   * - For binary (Buffer): generates a text description via Gemini 2.5 Flash, then embeds that description
   */
  static async generateEmbedding(
    content: string | Buffer,
    mimeType: string
  ): Promise<number[]> {
    const client = getGeminiClient();
    const mediaType = getMediaType(mimeType);

    try {
      let textToEmbed: string;

      if (typeof content === 'string') {
        textToEmbed = content;
      } else {
        // For binary media, generate a text description first
        scopedLogger.debug('Generating AI description for media embedding', { mediaType, mimeType });

        const descriptionResponse = await client.models.generateContent({
          model: GENERATION_MODEL,
          contents: [
            {
              role: 'user',
              parts: [
                {
                  inlineData: {
                    mimeType,
                    data: content.toString('base64'),
                  },
                },
                {
                  text: `Describe this ${mediaType} in detail for search indexing. Include all visible text, objects, people, colors, actions, and any other relevant details that would help someone find this content through a search query.`,
                },
              ],
            },
          ],
        });

        textToEmbed = descriptionResponse.text || `${mediaType} content`;
      }

      // Generate embedding
      const embeddingResponse = await client.models.embedContent({
        model: EMBEDDING_MODEL,
        contents: textToEmbed,
        config: {
          taskType: 'RETRIEVAL_DOCUMENT',
        },
      });

      const values = embeddingResponse.embeddings?.[0]?.values;
      if (!values || values.length === 0) {
        throw new Error('No embedding values returned from Gemini API');
      }

      return values;
    } catch (error) {
      scopedLogger.error('Failed to generate embedding', error, { mediaType, mimeType });
      throw error;
    }
  }

  /**
   * Generate query embedding using RETRIEVAL_QUERY task type.
   */
  static async generateQueryEmbedding(query: string): Promise<number[]> {
    const client = getGeminiClient();

    try {
      const embeddingResponse = await client.models.embedContent({
        model: EMBEDDING_MODEL,
        contents: query,
        config: {
          taskType: 'RETRIEVAL_QUERY',
        },
      });

      const values = embeddingResponse.embeddings?.[0]?.values;
      if (!values || values.length === 0) {
        throw new Error('No embedding values returned from Gemini API');
      }

      return values;
    } catch (error) {
      scopedLogger.error('Failed to generate query embedding', error);
      throw error;
    }
  }

  /**
   * Generate embedding and upsert vector to Pinecone.
   * For images/videos/audio, also generates an AI description if not already provided.
   */
  static async embedAndUpsert(
    userId: string,
    vectorId: string,
    content: string | Buffer,
    mimeType: string,
    metadata: {
      title: string;
      category: string;
      mediaType: KBMediaType;
      mediaUrl?: string;
      thumbnailUrl?: string;
      aiDescription?: string;
      documentId: string;
      chunkIndex?: number;
    }
  ): Promise<{ embeddingId: string; aiDescription?: string }> {
    const pc = getPineconeClient();
    const client = getGeminiClient();
    const namespace = tenantNamespace(userId);

    scopedLogger.info('Embedding and upserting vector', {
      userId,
      vectorId,
      mediaType: metadata.mediaType,
    });

    try {
      let aiDescription = metadata.aiDescription;

      // For any Buffer content without a description, generate AI description
      if (
        !aiDescription &&
        Buffer.isBuffer(content)
      ) {
        scopedLogger.debug('Generating AI description for media', {
          vectorId,
          mediaType: metadata.mediaType,
        });

        const descriptionResponse = await client.models.generateContent({
          model: GENERATION_MODEL,
          contents: [
            {
              role: 'user',
              parts: [
                {
                  inlineData: {
                    mimeType,
                    data: content.toString('base64'),
                  },
                },
                {
                  text: `Describe this ${metadata.mediaType} in detail for search indexing. Include all visible text, objects, people, colors, actions, and any other relevant details.`,
                },
              ],
            },
          ],
        });

        aiDescription = descriptionResponse.text || undefined;
      }

      // Generate embedding
      const embedding = await this.generateEmbedding(content, mimeType);

      // Build Pinecone metadata
      const pineconeMetadata: PineconeVectorMetadata = {
        title: metadata.title,
        category: metadata.category,
        mediaType: metadata.mediaType,
        documentId: metadata.documentId,
        createdAt: new Date().toISOString(),
      };

      if (metadata.mediaUrl) pineconeMetadata.mediaUrl = metadata.mediaUrl;
      if (metadata.thumbnailUrl) pineconeMetadata.thumbnailUrl = metadata.thumbnailUrl;
      if (aiDescription) pineconeMetadata.aiDescription = aiDescription;
      if (metadata.chunkIndex !== undefined) pineconeMetadata.chunkIndex = metadata.chunkIndex;

      // Store text content in metadata for RAG context retrieval (max 10KB for Pinecone limits)
      if (typeof content === 'string') {
        pineconeMetadata.textContent = content.slice(0, 10000);
      } else if (aiDescription) {
        pineconeMetadata.textContent = aiDescription;
      }

      // Get index and upsert
      const index = pc.index(INDEX_NAME);

      await index.upsert({
        records: [
          {
            id: vectorId,
            values: embedding,
            metadata: pineconeMetadata,
          },
        ],
        namespace,
      });

      scopedLogger.info('Vector upserted successfully', {
        userId,
        vectorId,
        namespace,
      });

      return {
        embeddingId: vectorId,
        aiDescription,
      };
    } catch (error) {
      scopedLogger.error('Failed to embed and upsert', error, {
        userId,
        vectorId,
      });
      throw error;
    }
  }

  /**
   * Query Pinecone for similar multimodal content.
   * Returns ranked matches with scores and metadata.
   */
  static async queryMultimodal(
    userId: string,
    query: string,
    options?: {
      topK?: number;
      mediaTypeFilter?: KBMediaType;
      minScore?: number;
    }
  ): Promise<{
    matches: Array<{
      id: string;
      score: number;
      metadata: Record<string, unknown>;
    }>;
  }> {
    const pc = getPineconeClient();
    const namespace = tenantNamespace(userId);
    const topK = options?.topK ?? 10;
    const minScore = options?.minScore ?? 0.3;

    scopedLogger.info('Querying multimodal KB', {
      userId,
      queryLength: query.length,
      topK,
      mediaTypeFilter: options?.mediaTypeFilter,
    });

    try {
      const queryEmbedding = await this.generateQueryEmbedding(query);

      const index = pc.index(INDEX_NAME);

      // Build filter if mediaTypeFilter is provided
      const filter = options?.mediaTypeFilter
        ? { mediaType: { $eq: options.mediaTypeFilter } }
        : undefined;

      const queryResponse = await index.query({
        vector: queryEmbedding,
        topK,
        includeMetadata: true,
        namespace,
        filter,
      });

      // Filter by minimum score
      const filteredMatches = (queryResponse.matches || [])
        .filter((match) => (match.score ?? 0) >= minScore)
        .map((match) => ({
          id: match.id,
          score: match.score ?? 0,
          metadata: (match.metadata ?? {}) as Record<string, unknown>,
        }));

      scopedLogger.info('Query completed', {
        userId,
        totalMatches: queryResponse.matches?.length ?? 0,
        filteredMatches: filteredMatches.length,
      });

      return { matches: filteredMatches };
    } catch (error) {
      scopedLogger.error('Failed to query multimodal KB', error, { userId });
      throw error;
    }
  }

  /**
   * Query the multimodal KB and generate an answer using Gemini 2.5 Flash.
   * Builds context from matched chunks and generates a response with citations.
   */
  static async queryAndGenerate(
    userId: string,
    query: string,
    options?: {
      language?: 'en' | 'ar';
      topK?: number;
      mediaTypeFilter?: KBMediaType;
      customInstructions?: string;
    }
  ): Promise<RAGQueryResponse> {
    const client = getGeminiClient();
    const startTime = Date.now();
    const language = options?.language ?? 'en';

    scopedLogger.info('Query and generate', {
      userId,
      queryLength: query.length,
      language,
    });

    try {
      // Get relevant matches
      const { matches } = await this.queryMultimodal(userId, query, {
        topK: options?.topK ?? 10,
        mediaTypeFilter: options?.mediaTypeFilter,
      });

      if (matches.length === 0) {
        return {
          success: true,
          answer:
            language === 'ar'
              ? 'لم يتم العثور على معلومات ذات صلة في قاعدة المعرفة.'
              : 'No relevant information found in the knowledge base.',
          citations: [],
          metadata: {
            queryTimeMs: Date.now() - startTime,
            model: GENERATION_MODEL,
            totalMatches: 0,
          },
        };
      }

      // Build context from matches
      const contextParts: string[] = [];
      const citations: RAGCitation[] = [];
      const mediaResults: RAGQueryResponse['mediaResults'] = [];

      for (const match of matches) {
        const meta = match.metadata as Partial<PineconeVectorMetadata>;
        const title = (meta.title as string) || 'Untitled';
        const description = (meta.aiDescription as string) || '';
        const mediaType = (meta.mediaType as KBMediaType) || 'document';
        const mediaUrl = meta.mediaUrl as string | undefined;

        // Add to context — use textContent (stored text) or aiDescription (media description)
        const textContent = (meta.textContent as string) || description || '';
        contextParts.push(
          `[Source: ${title} (${mediaType}, relevance: ${match.score.toFixed(2)})]`
        );
        if (textContent) {
          contextParts.push(textContent);
        }
        contextParts.push('---');

        // Build citation
        citations.push({
          documentId: (meta.documentId as string) || match.id,
          documentTitle: title,
          excerpt: textContent.slice(0, 500) || title,
          relevanceScore: match.score,
          mediaType,
          mediaUrl,
        });

        // Build media result if applicable
        if (mediaType !== 'document' && mediaUrl) {
          mediaResults.push({
            type: mediaType,
            url: mediaUrl,
            title,
            description: description.slice(0, 200) || undefined,
            relevanceScore: match.score,
            thumbnailUrl: meta.thumbnailUrl as string | undefined,
          });
        }
      }

      const context = contextParts.join('\n');

      // Build prompt
      let prompt = '';

      if (options?.customInstructions) {
        prompt += `${options.customInstructions}\n\n`;
      }

      if (language === 'ar') {
        prompt += 'Please respond in Arabic.\n\n';
      }

      prompt += `Based on the following knowledge base context, answer the user's question. If the context does not contain enough information, say so clearly.\n\n`;
      prompt += `Context:\n${context}\n\n`;
      prompt += `Question: ${query}`;

      // Generate answer
      const response = await client.models.generateContent({
        model: GENERATION_MODEL,
        contents: prompt,
      });

      const queryTimeMs = Date.now() - startTime;

      scopedLogger.info('Query and generate completed', {
        userId,
        queryTimeMs,
        citationsCount: citations.length,
        mediaResultsCount: mediaResults.length,
      });

      return {
        success: true,
        answer: response.text || 'No response generated.',
        citations,
        mediaResults: mediaResults.length > 0 ? mediaResults : undefined,
        metadata: {
          queryTimeMs,
          model: GENERATION_MODEL,
          totalMatches: matches.length,
        },
      };
    } catch (error) {
      scopedLogger.error('Query and generate failed', error, { userId });

      return {
        success: false,
        answer:
          language === 'ar'
            ? 'فشل في معالجة استفسارك. يرجى المحاولة مرة أخرى.'
            : 'Failed to process your query. Please try again.',
        citations: [],
        metadata: {
          queryTimeMs: Date.now() - startTime,
          model: GENERATION_MODEL,
        },
      };
    }
  }

  /**
   * Delete specific vectors from a tenant's namespace.
   */
  static async deleteVectors(userId: string, vectorIds: string[]): Promise<void> {
    const pc = getPineconeClient();
    const namespace = tenantNamespace(userId);

    scopedLogger.info('Deleting vectors', {
      userId,
      count: vectorIds.length,
    });

    try {
      const index = pc.index(INDEX_NAME);

      // Batch deletes if >100 IDs
      for (let i = 0; i < vectorIds.length; i += PINECONE_BATCH_SIZE) {
        const batch = vectorIds.slice(i, i + PINECONE_BATCH_SIZE);
        await index.deleteMany({ ids: batch, namespace });
      }

      scopedLogger.info('Vectors deleted', { userId, count: vectorIds.length });
    } catch (error) {
      scopedLogger.error('Failed to delete vectors', error, { userId });
      throw error;
    }
  }

  /**
   * Delete all vectors in a tenant's namespace.
   */
  static async deleteNamespace(userId: string): Promise<void> {
    const pc = getPineconeClient();
    const namespace = tenantNamespace(userId);

    scopedLogger.info('Deleting namespace', { userId, namespace });

    try {
      const index = pc.index(INDEX_NAME);
      await index.deleteAll({ namespace });

      scopedLogger.info('Namespace deleted', { userId, namespace });
    } catch (error) {
      scopedLogger.error('Failed to delete namespace', error, { userId, namespace });
      throw error;
    }
  }

  /**
   * Get vector count for a tenant's namespace.
   */
  static async getNamespaceStats(
    userId: string
  ): Promise<{ vectorCount: number }> {
    const pc = getPineconeClient();
    const namespace = tenantNamespace(userId);

    try {
      const index = pc.index(INDEX_NAME);
      const stats = await index.describeIndexStats();
      const nsStats = stats.namespaces?.[namespace];

      return {
        vectorCount: nsStats?.recordCount ?? 0,
      };
    } catch (error) {
      scopedLogger.error('Failed to get namespace stats', error, { userId });
      throw error;
    }
  }

  /**
   * Check if both Pinecone and Gemini API keys are configured.
   */
  static isConfigured(): boolean {
    return !!process.env.PINECONE_API_KEY && !!process.env.GOOGLE_API_KEY;
  }
}

// Export convenience object with bound static methods
export const pineconeMultimodalService = {
  initializeIndex: PineconeMultimodalService.initializeIndex.bind(PineconeMultimodalService),
  generateEmbedding: PineconeMultimodalService.generateEmbedding.bind(PineconeMultimodalService),
  generateQueryEmbedding: PineconeMultimodalService.generateQueryEmbedding.bind(PineconeMultimodalService),
  embedAndUpsert: PineconeMultimodalService.embedAndUpsert.bind(PineconeMultimodalService),
  queryMultimodal: PineconeMultimodalService.queryMultimodal.bind(PineconeMultimodalService),
  queryAndGenerate: PineconeMultimodalService.queryAndGenerate.bind(PineconeMultimodalService),
  deleteVectors: PineconeMultimodalService.deleteVectors.bind(PineconeMultimodalService),
  deleteNamespace: PineconeMultimodalService.deleteNamespace.bind(PineconeMultimodalService),
  getNamespaceStats: PineconeMultimodalService.getNamespaceStats.bind(PineconeMultimodalService),
  isConfigured: PineconeMultimodalService.isConfigured.bind(PineconeMultimodalService),
};
