/**
 * Similarity Search
 *
 * Semantic code search with importance weighting.
 */

import type { KuzuConnection } from '../graph/connection.js';
import type { EmbeddingService } from '../embeddings/service.js';
import { GraphQueries } from '../graph/queries.js';
import type {
  SimilarEntity,
  SimilaritySearchOptions,
} from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Default search options
 */
const DEFAULT_OPTIONS: Required<SimilaritySearchOptions> = {
  limit: 20,
  minScore: 0.3,
  entityTypes: ['Function', 'Class', 'File'],
  semanticWeight: 0.5,
  importanceWeight: 0.3,
  pageRankWeight: 0.2,
};

/**
 * Similarity Search
 *
 * Finds similar code entities using:
 * 1. Semantic similarity (embeddings)
 * 2. Importance weighting (file activity, recency)
 * 3. PageRank scores (graph centrality)
 */
export class SimilaritySearch {
  private queries: GraphQueries;
  private logger: Logger;

  constructor(
    conn: KuzuConnection,
    _embeddings?: EmbeddingService,
    logger?: Logger
  ) {
    this.queries = new GraphQueries(conn);
    this.logger = logger ?? createLogger({ name: 'similarity-search' });
  }

  /**
   * Find similar code entities by text search
   *
   * Note: Semantic/embedding search requires direct embedding access.
   * Currently uses text-based matching with importance weighting.
   */
  async findSimilarCode(
    query: string,
    options?: SimilaritySearchOptions
  ): Promise<SimilarEntity[]> {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    this.logger.debug({ query, opts }, 'Finding similar code');

    // Text-based search (semantic search would require embedding access)
    const queryEmbedding: number[] | null = null;

    const results: SimilarEntity[] = [];

    // Search each entity type
    for (const entityType of opts.entityTypes) {
      const entities = await this.searchEntityType(
        entityType,
        query,
        queryEmbedding,
        opts
      );
      results.push(...entities);
    }

    // Sort by combined score and limit
    results.sort((a, b) => b.combinedScore - a.combinedScore);
    return results.slice(0, opts.limit);
  }

  /**
   * Search a specific entity type
   */
  private async searchEntityType(
    entityType: 'Function' | 'Class' | 'File',
    query: string,
    queryEmbedding: number[] | null,
    opts: Required<SimilaritySearchOptions>
  ): Promise<SimilarEntity[]> {
    const results: SimilarEntity[] = [];

    if (entityType === 'Function') {
      // Search functions
      if (queryEmbedding) {
        const similar = await this.queries.searchFunctionsBySimilarity(
          queryEmbedding,
          opts.limit * 2
        );
        for (const { entity, score } of similar) {
          if (score >= opts.minScore) {
            results.push({
              qualifiedName: entity.qualifiedName,
              entityType: 'Function',
              filePath: entity.filePath,
              lineStart: entity.lineStart,
              lineEnd: entity.lineEnd,
              description: entity.docstring,
              similarityScore: score,
              importanceScore: await this.getImportanceScore(entity.filePath),
              combinedScore: this.calculateCombinedScore(score, 0, 0, opts),
            });
          }
        }
      } else {
        // Text-based fallback
        const functions = await this.queries.findFunctionsByName(query);
        for (const fn of functions.slice(0, opts.limit)) {
          const textScore = this.calculateTextScore(query, fn.name, fn.docstring);
          if (textScore >= opts.minScore) {
            results.push({
              qualifiedName: fn.qualifiedName,
              entityType: 'Function',
              filePath: fn.filePath,
              lineStart: fn.lineStart,
              lineEnd: fn.lineEnd,
              description: fn.docstring,
              similarityScore: textScore,
              importanceScore: await this.getImportanceScore(fn.filePath),
              combinedScore: this.calculateCombinedScore(textScore, 0, 0, opts),
            });
          }
        }
      }
    } else if (entityType === 'Class') {
      // Search classes
      if (queryEmbedding) {
        const similar = await this.queries.searchClassesBySimilarity(
          queryEmbedding,
          opts.limit * 2
        );
        for (const { entity, score } of similar) {
          if (score >= opts.minScore) {
            results.push({
              qualifiedName: entity.qualifiedName,
              entityType: 'Class',
              filePath: entity.filePath,
              lineStart: entity.lineStart,
              lineEnd: entity.lineEnd,
              description: entity.docstring,
              similarityScore: score,
              importanceScore: await this.getImportanceScore(entity.filePath),
              combinedScore: this.calculateCombinedScore(score, 0, 0, opts),
            });
          }
        }
      } else {
        // Text-based fallback
        const classes = await this.queries.findClassesByName(query);
        for (const cls of classes.slice(0, opts.limit)) {
          const textScore = this.calculateTextScore(query, cls.name, cls.docstring);
          if (textScore >= opts.minScore) {
            results.push({
              qualifiedName: cls.qualifiedName,
              entityType: 'Class',
              filePath: cls.filePath,
              lineStart: cls.lineStart,
              lineEnd: cls.lineEnd,
              description: cls.docstring,
              similarityScore: textScore,
              importanceScore: await this.getImportanceScore(cls.filePath),
              combinedScore: this.calculateCombinedScore(textScore, 0, 0, opts),
            });
          }
        }
      }
    } else if (entityType === 'File') {
      // Search files by name
      const files = await this.queries.findFilesByName(query);
      for (const file of files.slice(0, opts.limit)) {
        const textScore = this.calculateTextScore(query, file.name, file.path);
        if (textScore >= opts.minScore) {
          results.push({
            qualifiedName: file.path,
            entityType: 'File',
            filePath: file.path,
            similarityScore: textScore,
            importanceScore: await this.getImportanceScore(file.path),
            combinedScore: this.calculateCombinedScore(textScore, 0, 0, opts),
          });
        }
      }
    }

    // Update combined scores with importance
    for (const result of results) {
      result.combinedScore = this.calculateCombinedScore(
        result.similarityScore,
        result.importanceScore,
        0, // PageRank not implemented yet
        opts
      );
    }

    return results;
  }

  /**
   * Calculate text-based similarity score
   */
  private calculateTextScore(
    query: string,
    name: string,
    description?: string
  ): number {
    const queryLower = query.toLowerCase();
    const nameLower = name.toLowerCase();

    // Exact match
    if (nameLower === queryLower) return 1.0;

    // Contains query
    if (nameLower.includes(queryLower)) return 0.8;

    // Query contains name
    if (queryLower.includes(nameLower)) return 0.7;

    // Check description
    if (description) {
      const descLower = description.toLowerCase();
      if (descLower.includes(queryLower)) return 0.6;
    }

    // Word overlap
    const queryWords = new Set(queryLower.split(/\W+/));
    const nameWords = new Set(nameLower.split(/(?=[A-Z])|_|-/).map(w => w.toLowerCase()));

    let overlap = 0;
    for (const word of queryWords) {
      if (word.length > 2 && nameWords.has(word)) {
        overlap++;
      }
    }

    if (overlap > 0) {
      return Math.min(0.5, overlap * 0.2);
    }

    return 0;
  }

  /**
   * Get importance score for a file (based on activity)
   */
  private async getImportanceScore(filePath: string): Promise<number> {
    // Simple heuristic based on file location
    // Core files are more important
    const corePatterns = [
      /\/index\.[tj]sx?$/,
      /\/main\.[tj]sx?$/,
      /\/app\.[tj]sx?$/,
      /\/types\.[tj]sx?$/,
      /\/config\.[tj]sx?$/,
    ];

    for (const pattern of corePatterns) {
      if (pattern.test(filePath)) {
        return 0.8;
      }
    }

    // Test files are less important for planning
    if (/\.(test|spec)\.[tj]sx?$/.test(filePath)) {
      return 0.3;
    }

    // Source files
    if (/\/src\//.test(filePath)) {
      return 0.6;
    }

    return 0.5;
  }

  /**
   * Calculate combined score with weights
   */
  private calculateCombinedScore(
    similarityScore: number,
    importanceScore: number,
    pageRankScore: number,
    opts: Required<SimilaritySearchOptions>
  ): number {
    return (
      similarityScore * opts.semanticWeight +
      importanceScore * opts.importanceWeight +
      pageRankScore * opts.pageRankWeight
    );
  }

  /**
   * Importance-weighted search
   *
   * Combines semantic similarity with file importance and graph centrality.
   */
  async importanceWeightedSearch(
    query: string,
    options?: SimilaritySearchOptions
  ): Promise<SimilarEntity[]> {
    // This is an alias for findSimilarCode with importance weighting
    return this.findSimilarCode(query, {
      ...options,
      importanceWeight: options?.importanceWeight ?? 0.4,
      pageRankWeight: options?.pageRankWeight ?? 0.2,
    });
  }
}

/**
 * Create a similarity search instance
 */
export function createSimilaritySearch(
  conn: KuzuConnection,
  embeddings?: EmbeddingService,
  logger?: Logger
): SimilaritySearch {
  return new SimilaritySearch(conn, embeddings, logger);
}
