import type pg from 'pg';
import type { Product, ProductSearchCriteria } from '../domain/product.js';
import type { ProductSearchRepository } from '../domain/ports.js';
import { PRODUCT_COLUMNS, type ProductRow, toProduct } from './pg-product-repository.js';

export class PgProductSearchRepository implements ProductSearchRepository {
  constructor(private readonly pool: pg.Pool) {}

  async search(input: ProductSearchCriteria): Promise<Product[]> {
    const clauses: string[] = [];
    const values: unknown[] = [];
    const q = input.q?.trim();

    if (input.activeOnly) {
      clauses.push(`p.state = 'active'`);
    }
    if (input.brandSlug !== undefined && input.brandSlug.trim() !== '') {
      values.push(input.brandSlug.trim());
      clauses.push(`b.slug = $${values.length}`);
    }
    if (input.categorySlug !== undefined && input.categorySlug.trim() !== '') {
      values.push(input.categorySlug.trim());
      clauses.push(
        `EXISTS (
          SELECT 1 FROM catalog_product_categories filter_pc
          JOIN categories_categories filter_c ON filter_c.id = filter_pc.category_id
          WHERE filter_pc.product_id = p.id AND filter_c.slug = $${values.length}
        )`,
      );
    }

    const baseWhere = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
    const searchWhere: string[] = [];
    let rankSelect = '0::real AS search_rank';
    let orderBy = 'created_at DESC, name ASC, id ASC';

    if (q !== undefined && q !== '') {
      // ILIKE on name: handles partial stems (e.g. 'almen' vs 'almendra').
      // tsquery adds full-word relevance ranking.
      // Word-by-word ILIKE fallback for fuzzy partial matching (FE-048).
      values.push(`%${q}%`);
      const ilikeParam = values.length;
      values.push(q);
      const tsqueryParam = values.length;
      const tsquery = `websearch_to_tsquery('spanish', $${tsqueryParam})`;
      // Word-level fuzzy: split query into words, match each against name/description.
      const words = q.split(/\s+/).filter((w) => w.length >= 2);
      const wordConditions = words
        .map(() => `(name ILIKE $${ilikeParam} OR description ILIKE $${ilikeParam})`)
        .join(' AND ');
      const wordFallback = words.length > 0 ? `(${wordConditions})` : 'TRUE';
      searchWhere.push(
        `(name ILIKE $${ilikeParam} OR description ILIKE $${ilikeParam} OR search_doc @@ ${tsquery} OR ${wordFallback})`,
      );
      // Composite ranking: text relevance + starts-with bonus
      rankSelect = `
        ts_rank_cd(search_doc, ${tsquery}) +
        (CASE WHEN name ILIKE $${ilikeParam} THEN 1.0 ELSE 0 END) +
        (CASE WHEN name ILIKE $${ilikeParam} THEN 0.5 ELSE 0 END)
        AS search_rank`;
      orderBy = 'search_rank DESC, created_at DESC, name ASC, id ASC';
    }

    values.push(input.limit, input.offset);
    const where = searchWhere.length > 0 ? `WHERE ${searchWhere.join(' AND ')}` : '';
    const result = await this.pool.query<ProductRow>(
      `WITH search_basis AS (
         SELECT
           ${PRODUCT_COLUMNS},
           setweight(to_tsvector('spanish', COALESCE(p.name, '')), 'A') ||
           setweight(to_tsvector('spanish', COALESCE(p.description, '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(p.seo_title, '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(p.seo_description, '')), 'C') ||
           setweight(to_tsvector('spanish', COALESCE(b.name, '')), 'A') ||
           setweight(to_tsvector('spanish', COALESCE(b.slug, '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(b.seo_title, '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(b.seo_description, '')), 'C') ||
           setweight(to_tsvector('spanish', COALESCE(string_agg(DISTINCT c.name, ' '), '')), 'A') ||
           setweight(to_tsvector('spanish', COALESCE(string_agg(DISTINCT c.slug, ' '), '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(string_agg(DISTINCT c.seo_title, ' '), '')), 'B') ||
           setweight(to_tsvector('spanish', COALESCE(string_agg(DISTINCT c.seo_description, ' '), '')), 'C')
           AS search_doc
         FROM catalog_products p
         LEFT JOIN brands_brands b ON b.id = p.brand_id
         LEFT JOIN catalog_product_categories pc ON pc.product_id = p.id
         LEFT JOIN categories_categories c ON c.id = pc.category_id
         ${baseWhere}
         GROUP BY p.id, b.id
       )
       SELECT *, ${rankSelect}
       FROM search_basis
       ${where}
       ORDER BY ${orderBy}
       LIMIT $${values.length - 1} OFFSET $${values.length}`,
      values,
    );
    return result.rows.map(toProduct);
  }

  /** FE-048: Suggest term corrections via word-level ILIKE fallback. */
  async suggestCorrections(term: string): Promise<string[]> {
    const q = term.trim();
    if (q.length < 2) return [];

    // Find distinct words from products, brands, categories that share prefix with term
    const result = await this.pool.query<{ suggestion: string }>(
      `SELECT DISTINCT suggestion FROM (
         SELECT DISTINCT LOWER(unnest(string_to_array(name, ' '))) AS suggestion
         FROM catalog_products WHERE state = 'active'
         UNION ALL
         SELECT DISTINCT LOWER(unnest(string_to_array(name, ' '))) AS suggestion
         FROM brands_brands
         UNION ALL
         SELECT DISTINCT LOWER(unnest(string_to_array(name, ' '))) AS suggestion
         FROM categories_categories
       ) t
       WHERE suggestion LIKE $1 AND suggestion <> $2
       ORDER BY suggestion
       LIMIT 5`,
      [`${q}%`, q],
    );
    return result.rows.map((r) => r.suggestion);
  }
}
