/*
 * Runtime Web – Unified Serverless Framework for Full-Stack TypeScript Applications
 * Documentation: https://runtimeweb.com/docs/getting-started
 *
 * @template: ai
 */

/**
 * Search Service
 *
 * OpenSearch-based service for vector similarity search (KNN).
 * Used in RAG (Retrieval-Augmented Generation) to retrieve relevant
 * context documents based on semantic similarity to the user's query.
 *
 * Uses AWS Signature v4 for authentication with OpenSearch Service.
 */

import { Client } from '@opensearch-project/opensearch';
import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import type { RelevantDocument } from '../types/chat.types';

/**
 * OpenSearch hit structure from KNN search response.
 */
interface OpenSearchHit {
    _id: string;
    _score: number;
    _source: {
        content: string;
        metadata?: Record<string, unknown>;
    };
}

/**
 * OpenSearch search response structure.
 */
interface OpenSearchResponse {
    body: {
        hits: {
            hits: OpenSearchHit[];
        };
    };
}

/**
 * SearchService provides vector similarity search using OpenSearch KNN.
 *
 * Configuration via environment variables:
 * - OPENSEARCH_ENDPOINT: OpenSearch domain endpoint URL
 * - OPENSEARCH_INDEX: Index name for knowledge base (default: 'knowledge-base')
 * - AWS_REGION: AWS region for SigV4 signing (default: 'us-east-1')
 *
 * @example
 * ```typescript
 * const search = new SearchService();
 * const docs = await search.knn('How do I reset my password?', 5);
 * // Returns top 5 most relevant documents
 * ```
 */
export class SearchService {
    private client: Client | null = null;
    private indexName: string;
    private endpoint: string | undefined;
    private region: string;

    constructor() {
        this.endpoint = process.env.OPENSEARCH_ENDPOINT;
        this.region = process.env.AWS_REGION || 'us-east-1';
        this.indexName = process.env.OPENSEARCH_INDEX || 'knowledge-base';

        // Initialize client only if endpoint is configured
        if (this.endpoint) {
            this.client = new Client({
                ...AwsSigv4Signer({
                    region: this.region,
                    service: 'es',
                    getCredentials: () => defaultProvider()(),
                }),
                node: this.endpoint,
            });
        }
    }

    /**
     * Performs K-nearest neighbor search to find semantically similar documents.
     *
     * Note: This method expects the query to already be converted to an embedding vector
     * by the caller, or the OpenSearch index to have an ingest pipeline that generates
     * embeddings. For a production implementation, you would typically:
     * 1. Convert the query string to an embedding using Bedrock/Titan
     * 2. Use that embedding vector for the KNN search
     *
     * For simplicity, this implementation uses a text-based search fallback
     * when embeddings are not available.
     *
     * @param query - The search query string
     * @param k - Number of nearest neighbors to return (default: 5)
     * @returns Array of relevant documents sorted by similarity score
     *
     * @example
     * ```typescript
     * const results = await searchService.knn('password reset', 3);
     * // Returns up to 3 most relevant documents
     * ```
     */
    async knn(query: string, k: number = 5): Promise<RelevantDocument[]> {
        // Return empty array if OpenSearch is not configured
        if (!this.client || !this.endpoint) {
            console.warn(
                '[SearchService] OpenSearch endpoint not configured. ' +
                'Set OPENSEARCH_ENDPOINT environment variable to enable vector search.'
            );
            return [];
        }

        try {
            // Perform KNN search using script_score query
            // This approach works with pre-computed embeddings stored in the index
            // In production, you would first convert the query to an embedding vector
            const response = (await this.client.search({
                index: this.indexName,
                body: {
                    size: k,
                    query: {
                        // Use a hybrid approach: text match with KNN boost
                        // For pure KNN, replace with knn query once embedding is available
                        bool: {
                            should: [
                                {
                                    match: {
                                        content: {
                                            query: query,
                                            boost: 1.0,
                                        },
                                    },
                                },
                            ],
                        },
                    },
                    // Sort by score descending
                    sort: [{ _score: { order: 'desc' } }],
                },
            })) as OpenSearchResponse;

            // Transform OpenSearch hits to RelevantDocument format
            const hits = response.body.hits.hits;
            return hits.map((hit: OpenSearchHit) => ({
                id: hit._id,
                content: hit._source.content,
                score: hit._score,
                metadata: hit._source.metadata,
            }));
        } catch (error) {
            // Graceful degradation: log warning and return empty array
            // This ensures the chat handler can continue without search results
            console.warn(
                '[SearchService] OpenSearch query failed. Returning empty results.',
                error instanceof Error ? error.message : String(error)
            );
            return [];
        }
    }

    /**
     * Performs KNN search with a pre-computed embedding vector.
     * Use this method when you have already converted the query to an embedding.
     *
     * @param embedding - The query embedding vector (1536 dimensions for Titan)
     * @param k - Number of nearest neighbors to return (default: 5)
     * @returns Array of relevant documents sorted by similarity score
     *
     * @example
     * ```typescript
     * const embedding = await bedrockService.getEmbedding('password reset');
     * const results = await searchService.knnWithEmbedding(embedding, 3);
     * ```
     */
    async knnWithEmbedding(embedding: number[], k: number = 5): Promise<RelevantDocument[]> {
        // Return empty array if OpenSearch is not configured
        if (!this.client || !this.endpoint) {
            console.warn(
                '[SearchService] OpenSearch endpoint not configured. ' +
                'Set OPENSEARCH_ENDPOINT environment variable to enable vector search.'
            );
            return [];
        }

        try {
            // Perform pure KNN search with the embedding vector
            const response = (await this.client.search({
                index: this.indexName,
                body: {
                    size: k,
                    query: {
                        knn: {
                            embedding: {
                                vector: embedding,
                                k: k,
                            },
                        },
                    },
                },
            })) as OpenSearchResponse;

            // Transform OpenSearch hits to RelevantDocument format
            const hits = response.body.hits.hits;
            return hits.map((hit: OpenSearchHit) => ({
                id: hit._id,
                content: hit._source.content,
                score: hit._score,
                metadata: hit._source.metadata,
            }));
        } catch (error) {
            // Graceful degradation: log warning and return empty array
            console.warn(
                '[SearchService] OpenSearch KNN query failed. Returning empty results.',
                error instanceof Error ? error.message : String(error)
            );
            return [];
        }
    }
}
