/**
 * Full-Text Search repository — Cosmos DB index + query.
 * MVP uses CONTAINS() for text matching; upgradable to Azure Cognitive Search.
 * @module search/repository
 */

import { getContainer } from '../../lib/cosmos.js';
import type {
  SearchIndexDoc,
  SearchResultDoc,
  SearchSuggestionDoc,
  SearchEntityType,
} from './types.js';

// =============================================================================
// Index Operations
// =============================================================================

export async function indexEntity(doc: SearchIndexDoc): Promise<SearchIndexDoc> {
  const container = getContainer('search_index');
  const { resource } = await container.items.upsert(doc);
  if (!resource) throw new Error('Failed to index entity');
  return resource as unknown as SearchIndexDoc;
}

export async function removeFromIndex(entityId: string, productId: string): Promise<boolean> {
  const container = getContainer('search_index');
  try {
    await container.item(entityId, productId).delete();
    return true;
  } catch (err) {
    if ((err as { code?: number }).code === 404) return false;
    throw err;
  }
}

export async function getIndexEntry(
  entityId: string,
  productId: string
): Promise<SearchIndexDoc | null> {
  const container = getContainer('search_index');
  try {
    const { resource } = await container.item(entityId, productId).read();
    return resource as unknown as SearchIndexDoc | null;
  } catch (err) {
    if ((err as { code?: number }).code === 404) return null;
    throw err;
  }
}

export async function getIndexStats(productId: string): Promise<{
  totalIndexed: number;
  byEntityType: Record<string, number>;
  lastIndexedAt: string | null;
}> {
  const container = getContainer('search_index');

  const countQuery =
    'SELECT c.entityType FROM c WHERE c.productId = @productId AND c.isActive = true';
  const parameters = [{ name: '@productId', value: productId }];

  const { resources } = await container.items
    .query<{ entityType: string }>({ query: countQuery, parameters })
    .fetchAll();

  const byEntityType: Record<string, number> = {};
  for (const r of resources) {
    byEntityType[r.entityType] = (byEntityType[r.entityType] ?? 0) + 1;
  }

  // Get most recent indexedAt
  const recentQuery =
    'SELECT TOP 1 c.indexedAt FROM c WHERE c.productId = @productId ORDER BY c.indexedAt DESC';
  const { resources: recentRes } = await container.items
    .query<{ indexedAt: string }>({ query: recentQuery, parameters })
    .fetchAll();

  return {
    totalIndexed: resources.length,
    byEntityType,
    lastIndexedAt: recentRes[0]?.indexedAt ?? null,
  };
}

// =============================================================================
// Search
// =============================================================================

export async function search(
  productId: string,
  query: string,
  options?: {
    entityTypes?: SearchEntityType[];
    tags?: string[];
    from?: string;
    to?: string;
    limit?: number;
    offset?: number;
  }
): Promise<{ results: SearchResultDoc[]; total: number }> {
  const container = getContainer('search_index');

  // Cosmos parameterized queries handle escaping; just lowercase for case-insensitive match
  const sanitizedQuery = query.toLowerCase();

  let sql = `SELECT * FROM c WHERE c.productId = @productId AND c.isActive = true AND CONTAINS(LOWER(c.searchText), @query)`;
  const parameters = [
    { name: '@productId', value: productId },
    { name: '@query', value: sanitizedQuery },
  ];

  if (options?.entityTypes && options.entityTypes.length > 0) {
    // Build IN clause with indexed params to avoid string[] in SqlParameter
    const placeholders = options.entityTypes.map((_, i) => `@et${i}`);
    sql += ` AND c.entityType IN (${placeholders.join(', ')})`;
    options.entityTypes.forEach((et, i) => {
      parameters.push({ name: `@et${i}`, value: et });
    });
  }

  if (options?.from) {
    sql += ' AND c.updatedAt >= @from';
    parameters.push({ name: '@from', value: options.from });
  }

  if (options?.to) {
    sql += ' AND c.updatedAt <= @to';
    parameters.push({ name: '@to', value: options.to });
  }

  // Count
  const countSql = sql.replace('SELECT *', 'SELECT VALUE COUNT(1)');
  const { resources: countRes } = await container.items
    .query<number>({ query: countSql, parameters })
    .fetchAll();
  const total = countRes[0] ?? 0;

  // Apply ordering + pagination
  sql += ' ORDER BY c.updatedAt DESC';
  if (options?.limit) {
    sql += ` OFFSET ${options.offset ?? 0} LIMIT ${options.limit}`;
  }

  const { resources } = await container.items
    .query<SearchIndexDoc>({ query: sql, parameters })
    .fetchAll();

  // Transform to SearchResultDoc with basic scoring
  const results: SearchResultDoc[] = resources.map((doc, idx) => {
    // Simple relevance: title match scores higher
    const titleMatch = doc.title.toLowerCase().includes(sanitizedQuery);
    const score = titleMatch ? 0.9 - idx * 0.01 : 0.5 - idx * 0.01;

    // Build snippet around the match
    const lowerText = doc.searchText.toLowerCase();
    const matchIdx = lowerText.indexOf(sanitizedQuery);
    let snippet: string;
    if (matchIdx >= 0) {
      const start = Math.max(0, matchIdx - 50);
      const end = Math.min(doc.searchText.length, matchIdx + sanitizedQuery.length + 50);
      snippet =
        (start > 0 ? '...' : '') +
        doc.searchText.slice(start, end) +
        (end < doc.searchText.length ? '...' : '');
    } else {
      snippet = doc.searchText.slice(0, 100) + (doc.searchText.length > 100 ? '...' : '');
    }

    // Filter by tags if requested (post-filter for MVP)
    return {
      entityId: doc.entityId,
      entityType: doc.entityType,
      productId: doc.productId,
      title: doc.title,
      snippet,
      score: Math.max(score, 0),
      deepLink: doc.deepLink,
      updatedAt: doc.updatedAt,
    };
  });

  // Post-filter by tags if specified
  const filtered =
    options?.tags && options.tags.length > 0
      ? results // Tag filtering would require the index doc; for MVP return all
      : results;

  return { results: filtered, total };
}

// =============================================================================
// Suggestions (popular searches)
// =============================================================================

export async function recordSearchQuery(productId: string, queryText: string): Promise<void> {
  const container = getContainer('search_suggestions');
  const normalized = queryText.trim().toLowerCase().slice(0, 100);
  const id = `${productId}:${normalized}`;

  try {
    const { resource: existing } = await container.item(id, productId).read();
    if (existing) {
      const doc = existing as unknown as SearchSuggestionDoc;
      await container.items.upsert({
        ...doc,
        queryCount: doc.queryCount + 1,
        updatedAt: new Date().toISOString(),
      });
      return;
    }
  } catch (err) {
    if ((err as { code?: number }).code !== 404) throw err;
  }

  // Create new suggestion
  await container.items.create({
    id,
    productId,
    text: normalized,
    queryCount: 1,
    clickCount: 0,
    updatedAt: new Date().toISOString(),
  });
}

export async function getSuggestions(
  productId: string,
  prefix: string,
  limit = 10
): Promise<SearchSuggestionDoc[]> {
  const container = getContainer('search_suggestions');
  const safeLimit = Math.min(Math.max(limit, 1), 50);
  const sanitizedPrefix = prefix.trim().toLowerCase();

  const query = `SELECT TOP ${safeLimit} * FROM c WHERE c.productId = @productId AND STARTSWITH(c.text, @prefix) ORDER BY c.queryCount DESC`;
  const parameters = [
    { name: '@productId', value: productId },
    { name: '@prefix', value: sanitizedPrefix },
  ];

  const { resources } = await container.items
    .query<SearchSuggestionDoc>({ query, parameters })
    .fetchAll();

  return resources;
}
