import { dot } from "@/lib/matrix";
import { getQueryVector, getVectorIndex, type VectorIndex } from "@/lib/vector-index";
import type { ProductFilters } from "@/types/search";

/**
 * Vector search: the semantic half of the hybrid ranker.
 *
 * Product vectors and the query vector are unit length, so the dot product is
 * the cosine similarity. Latent Semantic Analysis can return negative values
 * (a query may be "opposite" to a document along a latent axis); those are
 * floored to zero so the score stays in 0..1 and stays comparable with the
 * keyword score.
 */

export interface VectorMatch {
  productId: number;
  similarity: number;
}

export function cosineSimilarity(left: readonly number[], right: readonly number[]): number {
  if (left.length !== right.length) return 0;
  return Math.max(0, Math.min(1, dot(left, right)));
}

/** Rank every indexed product against `query`, without applying any filters. */
export async function rankByVector(
  query: string,
  index?: VectorIndex,
): Promise<{ matches: VectorMatch[]; index: VectorIndex }> {
  const activeIndex = index ?? (await getVectorIndex());
  const queryVector = await getQueryVector(activeIndex, query);

  const matches: VectorMatch[] = activeIndex.documents.map((document) => ({
    productId: document.id,
    similarity: cosineSimilarity(queryVector, document.vector),
  }));

  return { matches, index: activeIndex };
}

/** Best similarity per product, used when several vector dimensions are fused. */
export function bestSimilarityPerProduct(matches: VectorMatch[]): Map<number, number> {
  const best = new Map<number, number>();
  for (const match of matches) {
    const current = best.get(match.productId);
    if (current === undefined || match.similarity > current) {
      best.set(match.productId, match.similarity);
    }
  }
  return best;
}

/** Fraction of products that pass the filter, for logging and tuning. */
export function describeFilter(filters: ProductFilters): string {
  const parts = Object.entries(filters)
    .filter(([, value]) => value !== undefined && value !== false)
    .map(([key, value]) => `${key}=${String(value)}`);
  return parts.length > 0 ? parts.join("&") : "none";
}
