// src/lib/search.ts
// Shared search library with caching, rate limiting, and search functions
// Supports both ILIKE (default) and PostgreSQL Full-Text Search (FTS)

import type { Payload, Where } from 'payload'
import type { Post, Category } from '../payload-types'
import { sql } from '@payloadcms/db-postgres/drizzle'

// FTS Feature Flag - aktivieren mit USE_FTS=true in .env
const USE_FTS = process.env.USE_FTS === 'true'

// Type für raw SQL Ergebnisse
interface FtsCountRow {
  total: string
}

interface FtsIdRow {
  id: number
  rank: number
}

// ============================================================================
// Types
// ============================================================================

export interface SearchParams {
  query: string
  tenantId?: number
  categorySlug?: string
  type?: 'blog' | 'news' | 'press' | 'announcement'
  locale?: string
  limit?: number
  offset?: number
}

export interface SearchResult {
  results: SearchResultItem[]
  total: number
  query: string
  filters: {
    category?: string
    type?: string
    locale?: string
  }
  pagination: {
    limit: number
    offset: number
    hasMore: boolean
  }
}

export interface SearchResultItem {
  id: number
  title: string
  slug: string
  excerpt: string | null
  publishedAt: string | null
  type: string
  categories: Array<{
    name: string
    slug: string
  }>
}

export interface SuggestionParams {
  query: string
  tenantId?: number
  categorySlug?: string
  locale?: string
  limit?: number
}

export interface Suggestion {
  title: string
  slug: string
  type: string
}

export interface CategoryFilterParams {
  tenantId?: number
  categorySlug?: string
  type?: 'blog' | 'news' | 'press' | 'announcement'
  locale?: string
  page?: number
  limit?: number
}

export interface PaginatedPosts {
  docs: Post[]
  totalDocs: number
  page: number
  totalPages: number
  hasNextPage: boolean
  hasPrevPage: boolean
}

// ============================================================================
// TTL Cache Implementation
// ============================================================================

interface CacheEntry<T> {
  value: T
  expiresAt: number
}

class TTLCache<K, V> {
  private cache = new Map<K, CacheEntry<V>>()
  private readonly ttlMs: number
  private cleanupInterval: ReturnType<typeof setInterval> | null = null

  constructor(ttlSeconds: number = 60) {
    this.ttlMs = ttlSeconds * 1000
    // Cleanup expired entries every minute
    this.cleanupInterval = setInterval(() => this.cleanup(), 60000)
  }

  get(key: K): V | undefined {
    const entry = this.cache.get(key)
    if (!entry) return undefined

    if (Date.now() > entry.expiresAt) {
      this.cache.delete(key)
      return undefined
    }

    return entry.value
  }

  set(key: K, value: V): void {
    this.cache.set(key, {
      value,
      expiresAt: Date.now() + this.ttlMs,
    })
  }

  delete(key: K): boolean {
    return this.cache.delete(key)
  }

  clear(): void {
    this.cache.clear()
  }

  private cleanup(): void {
    const now = Date.now()
    for (const [key, entry] of this.cache.entries()) {
      if (now > entry.expiresAt) {
        this.cache.delete(key)
      }
    }
  }

  destroy(): void {
    if (this.cleanupInterval) {
      clearInterval(this.cleanupInterval)
      this.cleanupInterval = null
    }
    this.cache.clear()
  }
}

// Cache instances (60 second TTL)
export const searchCache = new TTLCache<string, SearchResult>(60)
export const suggestionCache = new TTLCache<string, Suggestion[]>(60)

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Extract plain text from Lexical JSONB content
 */
export function extractTextFromLexical(content: unknown): string {
  if (!content || typeof content !== 'object') return ''

  const extractFromNode = (node: unknown): string => {
    if (!node || typeof node !== 'object') return ''

    const n = node as Record<string, unknown>
    let text = ''

    // Get text content
    if (typeof n.text === 'string') {
      text += n.text + ' '
    }

    // Recursively process children
    if (Array.isArray(n.children)) {
      for (const child of n.children) {
        text += extractFromNode(child)
      }
    }

    return text
  }

  const c = content as Record<string, unknown>
  if (c.root && typeof c.root === 'object') {
    return extractFromNode(c.root).trim()
  }

  return ''
}

/**
 * Build Payload where clause for search
 */
export function buildSearchWhere(
  query: string,
  filters: {
    tenantId?: number
    categorySlug?: string
    type?: string
  },
): Where {
  const conditions: Where[] = [
    // Only published posts
    { status: { equals: 'published' } },
  ]

  // Tenant filter
  if (filters.tenantId) {
    conditions.push({ tenant: { equals: filters.tenantId } })
  }

  // Category filter (requires lookup or slug field)
  // Note: We'll handle this separately since category is a relationship

  // Type filter
  if (filters.type) {
    conditions.push({ type: { equals: filters.type } })
  }

  // Search in title and excerpt using ILIKE (case-insensitive contains)
  if (query) {
    conditions.push({
      or: [{ title: { contains: query } }, { excerpt: { contains: query } }],
    })
  }

  return { and: conditions }
}

/**
 * Generate cache key from search parameters
 */
function generateCacheKey(prefix: string, params: Record<string, unknown>): string {
  const sortedEntries = Object.entries(params)
    .filter(([, v]) => v !== undefined && v !== null)
    .sort(([a], [b]) => a.localeCompare(b))
  return `${prefix}:${JSON.stringify(sortedEntries)}`
}

// ============================================================================
// Full-Text Search (FTS) Functions
// ============================================================================

/**
 * PostgreSQL FTS-Konfiguration nach Sprache
 */
function getFtsConfig(locale: string): string {
  return locale === 'en' ? 'english' : 'german'
}

/**
 * Bereitet Suchquery für FTS vor
 * Konvertiert "foo bar" zu "foo:* & bar:*" für Prefix-Suche
 */
function prepareFtsQuery(query: string): string {
  return query
    .trim()
    .split(/\s+/)
    .filter(term => term.length >= 2)
    .map(term => `${term}:*`)
    .join(' & ')
}

/**
 * Escapet einen String für sichere Verwendung in SQL
 * Verhindert SQL Injection bei dynamischen Werten
 */
function escapeSqlString(str: string): string {
  return str.replace(/'/g, "''")
}

/**
 * FTS-basierte Suche mit PostgreSQL to_tsvector
 * Verwendet die GIN-Indexes auf posts_locales
 */
async function searchWithFts(
  payload: Payload,
  params: SearchParams,
): Promise<{ postIds: number[]; totalCount: number }> {
  const { query, tenantId, type, locale = 'de', limit = 10, offset = 0 } = params

  if (!query || query.length < 2) {
    return { postIds: [], totalCount: 0 }
  }

  const ftsConfig = getFtsConfig(locale)
  const ftsQuery = prepareFtsQuery(query)

  if (!ftsQuery) {
    return { postIds: [], totalCount: 0 }
  }

  // Guard: Check if payload.db exists (may be missing in test mocks)
  if (!payload.db || !payload.db.drizzle) {
    console.warn('[FTS Search] payload.db.drizzle not available, returning empty results')
    return { postIds: [], totalCount: 0 }
  }

  // Drizzle-ORM für raw SQL Query
  const db = payload.db.drizzle

  // Escape Werte für sichere SQL-Queries
  const safeFtsQuery = escapeSqlString(ftsQuery)
  const safeLocale = escapeSqlString(locale)

  // Baue die SQL Query für FTS
  const conditions: string[] = []

  // FTS-Bedingung (Title + Excerpt)
  conditions.push(`to_tsvector('${ftsConfig}', COALESCE(pl.title, '') || ' ' || COALESCE(pl.excerpt, '')) @@ to_tsquery('${ftsConfig}', '${safeFtsQuery}')`)

  // Locale-Filter
  conditions.push(`pl._locale = '${safeLocale}'`)

  // Status-Filter (nur published)
  conditions.push(`p.status = 'published'`)

  // Tenant-Filter
  if (tenantId && Number.isInteger(tenantId)) {
    conditions.push(`p.tenant_id = ${tenantId}`)
  }

  // Type-Filter
  if (type) {
    const safeType = escapeSqlString(type)
    conditions.push(`p.type = '${safeType}'`)
  }

  const whereClause = conditions.join(' AND ')

  // Count Query für Pagination
  const countQuery = `
    SELECT COUNT(DISTINCT p.id) as total
    FROM posts p
    INNER JOIN posts_locales pl ON p.id = pl._parent_id
    WHERE ${whereClause}
  `

  // Main Query mit Relevanz-Sortierung
  // Note: SELECT DISTINCT requires all ORDER BY columns to be in SELECT list
  const mainQuery = `
    SELECT DISTINCT p.id,
      ts_rank(
        to_tsvector('${ftsConfig}', COALESCE(pl.title, '') || ' ' || COALESCE(pl.excerpt, '')),
        to_tsquery('${ftsConfig}', '${safeFtsQuery}')
      ) as rank,
      p.published_at
    FROM posts p
    INNER JOIN posts_locales pl ON p.id = pl._parent_id
    WHERE ${whereClause}
    ORDER BY rank DESC, p.published_at DESC NULLS LAST
    LIMIT ${Math.min(limit, 100)} OFFSET ${Math.max(0, offset)}
  `

  try {
    // Execute queries mit drizzle-orm sql template
    const [countResult, mainResult] = await Promise.all([
      db.execute(sql.raw(countQuery)),
      db.execute(sql.raw(mainQuery)),
    ])

    // Extrahiere die Ergebnisse - node-postgres gibt { rows: [...] } zurück
    const countRows = (countResult as unknown as { rows: FtsCountRow[] }).rows || []
    const totalCount = countRows.length > 0 ? parseInt(countRows[0].total, 10) : 0

    const mainRows = (mainResult as unknown as { rows: FtsIdRow[] }).rows || []
    const postIds = mainRows.map((row) => row.id)

    return { postIds, totalCount }
  } catch (error) {
    console.error('[FTS Search] SQL Error:', error)
    // Fallback zu leeren Ergebnissen bei Fehler
    return { postIds: [], totalCount: 0 }
  }
}

// ============================================================================
// Search Functions
// ============================================================================

/**
 * Search posts with ILIKE or FTS (when USE_FTS=true)
 * FTS uses PostgreSQL to_tsvector for better performance and relevance
 */
export async function searchPosts(
  payload: Payload,
  params: SearchParams,
): Promise<SearchResult> {
  const { query, tenantId, categorySlug, type, locale = 'de', limit = 10, offset = 0 } = params

  // Check cache
  const cacheKey = generateCacheKey('search', { query, tenantId, categorySlug, type, locale, limit, offset, fts: USE_FTS })
  const cached = searchCache.get(cacheKey)
  if (cached) {
    return cached
  }

  // Category lookup (benötigt für beide Methoden)
  let categoryId: number | undefined
  if (categorySlug) {
    const categoryResult = await payload.find({
      collection: 'categories',
      where: {
        slug: { equals: categorySlug },
        ...(tenantId ? { tenant: { equals: tenantId } } : {}),
      },
      locale: locale as 'de' | 'en',
      fallbackLocale: 'de',
      limit: 1,
    })
    if (categoryResult.docs.length > 0) {
      categoryId = categoryResult.docs[0].id
    }
  }

  let result: Awaited<ReturnType<typeof payload.find>>
  let totalDocs: number

  // Verwende FTS wenn aktiviert und Query vorhanden
  if (USE_FTS && query && query.length >= 2) {
    // FTS-basierte Suche
    const ftsResult = await searchWithFts(payload, { ...params, categorySlug: undefined })

    if (ftsResult.postIds.length === 0) {
      // Keine FTS-Ergebnisse
      result = { docs: [], totalDocs: 0, page: 1, totalPages: 0, hasNextPage: false, hasPrevPage: false, limit, pagingCounter: 1 }
      totalDocs = 0
    } else {
      // Lade Posts nach FTS-IDs (mit Category-Filter wenn nötig)
      const whereConditions: Where[] = [
        { id: { in: ftsResult.postIds } },
      ]
      if (categoryId) {
        whereConditions.push({ categories: { contains: categoryId } })
      }

      result = await payload.find({
        collection: 'posts',
        where: whereConditions.length > 1 ? { and: whereConditions } : whereConditions[0],
        locale: locale as 'de' | 'en',
        fallbackLocale: 'de',
        limit: ftsResult.postIds.length, // Alle gefundenen IDs laden
        sort: '-publishedAt',
        depth: 1,
      })

      // Sortiere nach FTS-Reihenfolge (Relevanz)
      const orderedDocs = ftsResult.postIds
        .map(id => result.docs.find(doc => doc.id === id))
        .filter((doc): doc is Post => doc !== undefined)

      result = { ...result, docs: orderedDocs }
      totalDocs = categoryId ? result.totalDocs : ftsResult.totalCount
    }
  } else {
    // Standard ILIKE-basierte Suche
    const whereConditions: Where[] = [{ status: { equals: 'published' } }]

    if (tenantId) {
      whereConditions.push({ tenant: { equals: tenantId } })
    }

    if (type) {
      whereConditions.push({ type: { equals: type } })
    }

    // Search in title and excerpt mit ILIKE
    if (query && query.length >= 2) {
      whereConditions.push({
        or: [{ title: { contains: query } }, { excerpt: { contains: query } }],
      })
    }

    if (categoryId) {
      whereConditions.push({ categories: { contains: categoryId } })
    }

    const where: Where = whereConditions.length > 1 ? { and: whereConditions } : whereConditions[0]

    result = await payload.find({
      collection: 'posts',
      where,
      locale: locale as 'de' | 'en',
      fallbackLocale: 'de',
      limit,
      page: Math.floor(offset / limit) + 1,
      sort: '-publishedAt',
      depth: 1,
    })
    totalDocs = result.totalDocs
  }

  // Transform results - Cast docs to Post[] for proper typing
  const posts = result.docs as Post[]
  const searchResult: SearchResult = {
    results: posts.map((post) => ({
      id: post.id,
      title: post.title,
      slug: post.slug,
      excerpt: post.excerpt || null,
      publishedAt: post.publishedAt || null,
      type: (post as Post & { type?: string }).type || 'blog',
      categories: Array.isArray(post.categories)
        ? post.categories
            .filter((cat): cat is Category => cat !== null && typeof cat === 'object' && 'name' in cat)
            .map((cat) => ({ name: cat.name, slug: cat.slug }))
        : [],
    })),
    total: totalDocs,
    query,
    filters: {
      category: categorySlug,
      type,
      locale,
    },
    pagination: {
      limit,
      offset,
      hasMore: result.hasNextPage,
    },
  }

  // Cache result
  searchCache.set(cacheKey, searchResult)

  return searchResult
}

/**
 * Get search suggestions (auto-complete)
 */
export async function getSearchSuggestions(
  payload: Payload,
  params: SuggestionParams,
): Promise<Suggestion[]> {
  const { query, tenantId, categorySlug, locale = 'de', limit = 5 } = params

  if (!query || query.length < 2) {
    return []
  }

  // Check cache
  const cacheKey = generateCacheKey('suggestions', { query, tenantId, categorySlug, locale, limit })
  const cached = suggestionCache.get(cacheKey)
  if (cached) {
    return cached
  }

  // Build where clause
  const whereConditions: Where[] = [
    { status: { equals: 'published' } },
    // Prefix search on title
    { title: { contains: query } },
  ]

  if (tenantId) {
    whereConditions.push({ tenant: { equals: tenantId } })
  }

  // Category filter
  if (categorySlug) {
    const categoryResult = await payload.find({
      collection: 'categories',
      where: {
        slug: { equals: categorySlug },
        ...(tenantId ? { tenant: { equals: tenantId } } : {}),
      },
      locale: locale as 'de' | 'en',
      fallbackLocale: 'de',
      limit: 1,
    })
    if (categoryResult.docs.length > 0) {
      whereConditions.push({ categories: { contains: categoryResult.docs[0].id } })
    }
  }

  const where: Where = { and: whereConditions }

  // Execute query
  const result = await payload.find({
    collection: 'posts',
    where,
    locale: locale as 'de' | 'en',
    fallbackLocale: 'de',
    limit,
    sort: '-publishedAt',
    depth: 0,
  })

  // Transform to suggestions
  const suggestions: Suggestion[] = result.docs.map((post) => ({
    title: post.title,
    slug: post.slug,
    type: (post as Post & { type?: string }).type || 'blog',
  }))

  // Cache result
  suggestionCache.set(cacheKey, suggestions)

  return suggestions
}

/**
 * Get posts by category with pagination
 */
export async function getPostsByCategory(
  payload: Payload,
  params: CategoryFilterParams,
): Promise<PaginatedPosts> {
  const { tenantId, categorySlug, type, locale = 'de', page = 1, limit = 10 } = params

  // Build where clause
  const whereConditions: Where[] = [{ status: { equals: 'published' } }]

  if (tenantId) {
    whereConditions.push({ tenant: { equals: tenantId } })
  }

  if (type) {
    whereConditions.push({ type: { equals: type } })
  }

  // Category filter
  if (categorySlug) {
    const categoryResult = await payload.find({
      collection: 'categories',
      where: {
        slug: { equals: categorySlug },
        ...(tenantId ? { tenant: { equals: tenantId } } : {}),
      },
      locale: locale as 'de' | 'en',
      fallbackLocale: 'de',
      limit: 1,
    })
    if (categoryResult.docs.length > 0) {
      whereConditions.push({ categories: { contains: categoryResult.docs[0].id } })
    }
  }

  const where: Where = whereConditions.length > 1 ? { and: whereConditions } : whereConditions[0]

  // Execute query
  const result = await payload.find({
    collection: 'posts',
    where,
    locale: locale as 'de' | 'en',
    fallbackLocale: 'de',
    page,
    limit,
    sort: '-publishedAt',
    depth: 1,
  })

  return {
    docs: result.docs,
    totalDocs: result.totalDocs,
    page: result.page || page,
    totalPages: result.totalPages,
    hasNextPage: result.hasNextPage,
    hasPrevPage: result.hasPrevPage,
  }
}
