/**
 * Query-layer module for deal search.
 *
 * Owns ALL query construction for GET /api/deals/search:
 *   - Input schema (exported for route re-use)
 *   - Slug resolution (category + tags)
 *   - Unified filter path with shared FTS/ILIKE text predicate + real COUNT()
 *   - DealCardDeal mapping
 *
 * Per query-layer exclusivity rule, the API route must call runSearch() and
 * must not inline any DB logic. No db.transaction() — neon-http driver.
 */

import { executeRows } from '../execute-rows.js';
import {
  and,
  eq,
  or,
  inArray,
  isNull,
  isNotNull,
  gt,
  gte,
  lte,
  asc,
  desc,
  sql,
  count,
} from 'drizzle-orm';
import { alias } from 'drizzle-orm/pg-core';
import { z } from 'zod';
import type { DrizzleClient } from '../client.js';
import { deals, vendors, dealImages, dealTranslations } from '../schema.js';
import type { DealCardDeal } from '@/components/ui/domain/DealCard';
import { buildDealTextPredicate } from './deals-search-predicate.js';
import { getLanguageCached } from '@/server/i18n/languages/cache.js';
import { resolveCategorySlug, resolveTagSlugs } from './slug-resolve.js';
import { DEAL_TYPES } from '@/lib/deal-types';
import { searchSort } from '@/lib/enums/search-sort';
import { notSoldOutSqlD } from './sold-out-filter.js';
import { listForDirectory } from './vendorDirectory.js';

// ─── Constants ───────────────────────────────────────────────────────────────

type KnownDealType = (typeof DEAL_TYPES)[number];

// Vendor states visible on public surfaces.
import { PUBLIC_VENDOR_STATES } from '@/server/catalog/_shared/predicates.js';

// ─── Input schema (exported for route) ───────────────────────────────────────

const SLUG = z
  .string()
  .regex(/^[a-z0-9-]+$/)
  .max(80);
const SLUG_CSV = z.string().regex(/^[a-z0-9-]+(,[a-z0-9-]+)*$/);

export const SearchInputSchema = z.object({
  q: z.string().max(200).optional().default(''),
  locale: z.string().max(12).optional(),
  type: z
    .enum(['', ...DEAL_TYPES])
    .optional()
    .default(''),
  cat: SLUG.optional(),
  tagSlugs: SLUG_CSV.optional(),
  minPrice: z.coerce.number().min(0).optional(),
  maxPrice: z.coerce.number().min(0).optional(),
  minDiscount: z.coerce.number().int().min(1).max(100).optional(),
  sort: searchSort.schema.optional().default('relevance'),
  page: z.coerce.number().int().min(1).optional().default(1),
  pageSize: z.coerce.number().int().min(1).max(50).optional().default(12),
});

export type SearchInput = z.infer<typeof SearchInputSchema>;

// ─── Vendor search result ─────────────────────────────────────────────────────

export interface VendorSearchResult {
  _type: 'vendor';
  id: string;
  displayName: string;
  logoUrl: string | null;
  heroImageUrl: string | null;
  city: string;
  reviewsScore: string;
  reviewsCount: number;
  activeDealsCount: number;
  businessTypeName: string | null;
  deals: DealCardDeal[];
}

export async function searchVendors(
  db: DrizzleClient,
  opts: { q: string; limit?: number },
): Promise<VendorSearchResult[]> {
  const q = opts.q.trim();
  if (!q) return [];
  try {
    const { vendors: dirVendors } = await listForDirectory(db, {
      query: q,
      limit: opts.limit ?? 8,
    });
    const mapped: VendorSearchResult[] = dirVendors.map((v) => ({
      _type: 'vendor' as const,
      id: v.id,
      displayName: v.displayName,
      logoUrl: v.logoUrl,
      heroImageUrl: v.heroImageUrl,
      city: v.city,
      reviewsScore: v.reviewsScore,
      reviewsCount: v.reviewsCount,
      activeDealsCount: v.activeDealsCount,
      businessTypeName: v.businessTypeNames[0] ?? null,
      deals: [],
    }));

    if (mapped.length === 0) return mapped;

    const vendorIds = mapped.map((v) => v.id);
    const dealsPerVendor = new Map<string, DealCardDeal[]>();

    try {
      type DealRow = {
        id: string;
        title: string;
        vendor_id: string;
        vendor_name: string;
        min_price: string | null;
        max_price: string | null;
        max_discount_percent: number | null;
        stock_remaining: number | null;
        deal_type: string;
        window_end: string | null;
        image_src: string | null;
        he_slug: string | null;
        rn: number;
      };
      const result = await db.execute<DealRow>(sql`
        SELECT
          d.id,
          d.title,
          d.vendor_id,
          v.display_name AS vendor_name,
          d.min_price,
          d.max_price,
          d.max_discount_percent,
          d.stock_remaining,
          d.deal_type,
          d.window_end,
          di.url AS image_src,
          dt.slug AS he_slug,
          ROW_NUMBER() OVER (
            PARTITION BY d.vendor_id
            ORDER BY d.max_discount_percent DESC NULLS LAST
          ) AS rn
        FROM deals d
        JOIN vendors v ON v.id = d.vendor_id
        LEFT JOIN deal_images di ON di.deal_id = d.id AND di.is_primary = true AND di.sku_id IS NULL
        LEFT JOIN deal_translations dt ON dt.deal_id = d.id AND dt.locale = 'he'
        WHERE d.vendor_id = ANY(
          ARRAY[${sql.join(
            vendorIds.map((id) => sql`${id}::uuid`),
            sql`, `,
          )}]
        )
          AND d.deal_state = 'ACTIVE'
          AND (d.window_end IS NULL OR d.window_end > NOW())
          AND (d.stock_remaining IS NULL OR d.stock_remaining > 0)
      `);

      for (const row of executeRows<DealRow>(result)) {
        if (row.rn > 3) continue;
        const deal: DealCardDeal = {
          id: row.id,
          title: row.title,
          vendorId: row.vendor_id,
          vendorName: row.vendor_name,
          city: '',
          originalPrice: parseFloat(row.max_price ?? '0'),
          discountedPrice: parseFloat(row.min_price ?? '0'),
          imageSrc: row.image_src ?? '',
          imageAlt: row.title,
          windowEnd: row.window_end
            ? new Date(row.window_end).toISOString()
            : new Date(Date.now() + 86400000).toISOString(),
          stockRemaining: row.stock_remaining ?? 0,
          stockTotal: row.stock_remaining ?? 0,
          dealType: row.deal_type as DealCardDeal['dealType'],
          discountPercent: row.max_discount_percent ?? 0,
          soldCount: 0,
          heSlug: row.he_slug ?? undefined,
        };
        const existing = dealsPerVendor.get(row.vendor_id) ?? [];
        existing.push(deal);
        dealsPerVendor.set(row.vendor_id, existing);
      }
    } catch (dealErr) {
      console.error('[searchVendors] deals batch failed', dealErr);
      // graceful degradation: return vendors with empty deals[]
    }

    return mapped.map((v) => ({ ...v, deals: dealsPerVendor.get(v.id) ?? [] }));
  } catch (err) {
    console.error('[searchVendors] failed', err);
    return [];
  }
}

// ─── Result type ──────────────────────────────────────────────────────────────

export interface SearchResult {
  results: DealCardDeal[];
  vendors: VendorSearchResult[];
  total: number;
  totalPages: number;
}

// ─── Main query function ──────────────────────────────────────────────────────

/**
 * Run a full deal search with the given validated input.
 *
 * Returns { results, total, totalPages }. Unresolved slugs return the empty-result shape.
 */
export async function runSearch(db: DrizzleClient, input: SearchInput): Promise<SearchResult> {
  const {
    q,
    locale,
    type: dealTypeRaw,
    cat: catSlug,
    tagSlugs: tagSlugsRaw,
    minPrice,
    maxPrice,
    minDiscount,
    sort,
    page,
    pageSize,
  } = input;

  const tagSlugArr = tagSlugsRaw ? tagSlugsRaw.split(',').filter((s) => s.trim().length > 0) : [];
  const dealTypeFilter: KnownDealType[] = dealTypeRaw !== '' ? [dealTypeRaw as KnownDealType] : [];

  const empty: SearchResult = { results: [], vendors: [], total: 0, totalPages: 0 };

  // ── Resolve slugs → UUIDs ─────────────────────────────────────────────────
  let resolvedCategoryIds: string[] | undefined;
  let resolvedTagIds: string[] = [];

  if (catSlug) {
    const cat = await resolveCategorySlug(db, catSlug);
    if (!cat) return empty;
    resolvedCategoryIds = [cat.id];
  }

  if (tagSlugArr.length > 0) {
    const tags = await resolveTagSlugs(db, tagSlugArr);
    if (!tags) return empty;
    resolvedTagIds = tags.ids;
  }

  const vendorPromise =
    page === 1 ? searchVendors(db, { q, limit: 8 }) : Promise.resolve([] as VendorSearchResult[]);

  // ── Unified search path (shared text predicate + full filters + real COUNT) ─
  const d = alias(deals, 'd');
  const localeTranslation = alias(dealTranslations, 'locale_tr');
  const heTranslation = alias(dealTranslations, 'he_tr');

  const searchConfig = locale ? (await getLanguageCached(locale))?.searchConfig : undefined;
  const textPred = buildDealTextPredicate({ q, locale, searchConfig });

  const filters = [
    eq(d.dealState, 'ACTIVE'),
    or(isNull(d.windowEnd), gt(d.windowEnd, new Date()))!,
    inArray(vendors.accountState, [...PUBLIC_VENDOR_STATES]),
    isNotNull(d.minPrice),
    notSoldOutSqlD(),
  ];

  if (textPred) filters.push(textPred);

  if (dealTypeFilter.length === 1) {
    filters.push(eq(d.dealType, dealTypeFilter[0]!));
  }

  if (resolvedCategoryIds && resolvedCategoryIds.length === 1) {
    filters.push(eq(d.categoryId, resolvedCategoryIds[0]!));
  } else if (resolvedCategoryIds && resolvedCategoryIds.length > 1) {
    filters.push(inArray(d.categoryId, resolvedCategoryIds));
  }

  if (minPrice != null) filters.push(gte(d.minPrice, String(minPrice)));
  if (maxPrice != null) filters.push(lte(d.minPrice, String(maxPrice)));
  if (minDiscount != null) filters.push(gte(d.maxDiscountPercent, minDiscount));

  if (resolvedTagIds.length > 0) {
    filters.push(
      sql`${d.id} IN (
        SELECT deal_id FROM deal_tag_assignments
        WHERE tag_id = ANY(ARRAY[${sql.join(
          resolvedTagIds.map((id) => sql`${id}::uuid`),
          sql`, `,
        )}])
        GROUP BY deal_id
        HAVING COUNT(DISTINCT tag_id) = ${resolvedTagIds.length}
      )`,
    );
  }

  const whereClause = and(...filters);
  const offset = (page - 1) * pageSize;

  const orderClause =
    sort === 'price_asc'
      ? asc(d.minPrice)
      : sort === 'price_desc'
        ? desc(d.minPrice)
        : sort === 'relevance' && q.trim() && searchConfig && locale
          ? sql`(SELECT ts_rank(
                to_tsvector(${searchConfig}::regconfig, t.title || ' ' || t.description),
                plainto_tsquery(${searchConfig}::regconfig, ${q.trim()}))
              FROM deal_translations t
              WHERE t.deal_id = ${d.id} AND t.locale = ${locale} AND t.status = 'OK'
              ORDER BY 1 DESC LIMIT 1) DESC NULLS LAST`
          : desc(d.maxDiscountPercent);

  const localeJoinCondition = locale
    ? and(eq(localeTranslation.dealId, d.id), eq(localeTranslation.locale, locale))
    : sql`false`;

  const [rows, countResult] = await Promise.all([
    db
      .select({
        deal: d,
        vendorName: vendors.displayName,
        displayTitle: sql<string>`COALESCE(${localeTranslation.title}, ${d.title})`,
        heSlug: heTranslation.slug,
      })
      .from(d)
      .innerJoin(vendors, eq(d.vendorId, vendors.id))
      .leftJoin(localeTranslation, localeJoinCondition)
      .leftJoin(heTranslation, and(eq(heTranslation.dealId, d.id), eq(heTranslation.locale, 'he')))
      .where(whereClause)
      .orderBy(orderClause)
      .limit(pageSize)
      .offset(offset),
    db
      .select({ total: count() })
      .from(d)
      .innerJoin(vendors, eq(d.vendorId, vendors.id))
      .where(whereClause),
  ]);

  const total = countResult[0]?.total ?? 0;
  const totalPages = Math.ceil(total / pageSize);

  const dealIds = rows.map((r) => r.deal.id);
  const imageMap = new Map<string, string>();

  if (dealIds.length > 0) {
    const imgRows = await db
      .select({ dealId: dealImages.dealId, url: dealImages.url })
      .from(dealImages)
      .where(
        and(
          eq(dealImages.isPrimary, true),
          isNull(dealImages.skuId),
          inArray(dealImages.dealId, dealIds),
        ),
      )
      .limit(dealIds.length);
    for (const img of imgRows) imageMap.set(img.dealId, img.url);
  }

  const results: DealCardDeal[] = rows.map(({ deal, vendorName, displayTitle, heSlug }) => ({
    id: deal.id,
    title: displayTitle,
    vendorId: deal.vendorId,
    vendorName,
    city: '',
    originalPrice: parseFloat(deal.maxPrice ?? '0'),
    discountedPrice: parseFloat(deal.minPrice ?? '0'),
    imageSrc: imageMap.get(deal.id) ?? '',
    imageAlt: displayTitle,
    windowEnd: deal.windowEnd?.toISOString() ?? new Date(Date.now() + 86400000).toISOString(),
    stockRemaining: deal.stockRemaining ?? 0,
    stockTotal: deal.stockRemaining ?? 0,
    dealType: deal.dealType,
    discountPercent: deal.maxDiscountPercent ?? 0,
    soldCount: 0,
    heSlug: heSlug ?? undefined,
  }));

  const vendorList = await vendorPromise;
  return { results, vendors: vendorList, total, totalPages };
}
