/**
 * موتور جست‌وجو (لایه پرس‌وجو).
 *
 * پشتیبانی از دو حالت:
 *  - ساده: یک عبارت + تحمل غلط تایپی + مترادف + پیشنهاد
 *  - پیشرفته: عبارت دقیق / همه کلمات / هر کدام از کلمات / بدون کلمه + فیلترهای
 *    ساختاری (سازمان، صنف، دسته، تاریخ، نوع محتوا، دارای تصویر و ...)
 *
 * رتبه‌بندی قابل تنظیم است (RANKING) تا تیم محتوا بتواند وزن‌ها را تغییر دهد.
 */
import { and, eq, gte, inArray, lte, sql, desc } from 'drizzle-orm';
import { getDb } from '../../database/client.js';
import { searchDocuments, searchQueries, searchSynonyms, analyticsEvents } from '../../database/schema/index.js';
import { normalizePersian, tokenize, fuzzyMatches } from '../../common/persian.js';
import type { EntityType } from './indexer.js';

export const RANKING = {
  titleExact: 120,
  titleToken: 45,
  bodyToken: 12,
  fuzzyPenalty: 0.45,
  synonymFactor: 0.6,
  freshnessDays: 30,
  freshnessWeight: 25,
  popularityWeight: 0.05,
  entityBoost: { article: 10, case: 6, dataset: 6, organization: 4, business: 3, shop: 1, guild: 1, report: 0 } as Record<string, number>,
};

export type SearchQuery = {
  q?: string;
  mode?: 'simple' | 'advanced';
  exactPhrase?: string;
  allWords?: string;
  anyWords?: string;
  noneWords?: string;
  types?: EntityType[];
  categoryKey?: string;
  contentType?: string;
  organizationId?: string;
  businessId?: string;
  guildId?: string;
  regionId?: string;
  authorId?: string;
  topicKey?: string;
  tagKey?: string;
  status?: string;
  from?: string;
  to?: string;
  hasImage?: boolean;
  hasVideo?: boolean;
  hasDocument?: boolean;
  sort?: 'relevance' | 'newest' | 'oldest' | 'most_viewed' | 'most_discussed' | 'most_saved';
  page?: number;
  pageSize?: number;
};

export type SearchHit = {
  entityType: string;
  entityId: string;
  title: string;
  summary: string | null;
  url: string;
  publishedAt: Date | null;
  score: number;
  facets: Record<string, unknown>;
  highlight?: string;
};

async function expandSynonyms(tokens: string[]): Promise<Map<string, string[]>> {
  if (tokens.length === 0) return new Map();
  const db = getDb();
  const rows = await db.select().from(searchSynonyms).where(inArray(searchSynonyms.term, tokens));
  const map = new Map<string, string[]>();
  for (const row of rows) map.set(row.term, row.synonyms.map(normalizePersian));
  return map;
}

function buildFilters(query: SearchQuery) {
  const conditions = [];
  if (query.types?.length) conditions.push(inArray(searchDocuments.entityType, query.types));
  if (query.hasImage) conditions.push(eq(searchDocuments.hasImage, true));
  if (query.hasVideo) conditions.push(eq(searchDocuments.hasVideo, true));
  if (query.hasDocument) conditions.push(eq(searchDocuments.hasDocument, true));
  if (query.from) conditions.push(gte(searchDocuments.publishedAt, new Date(query.from)));
  if (query.to) conditions.push(lte(searchDocuments.publishedAt, new Date(query.to)));

  // فیلترهای ساختاری روی facets (JSONB)
  const facetFilters: [string, string | undefined][] = [
    ['categoryKey', query.categoryKey],
    ['contentType', query.contentType],
    ['organizationId', query.organizationId],
    ['businessId', query.businessId],
    ['guildId', query.guildId],
    ['regionId', query.regionId],
    ['authorId', query.authorId],
    ['status', query.status],
  ];
  for (const [key, value] of facetFilters) {
    if (value) conditions.push(sql`${searchDocuments.facets}->>${key} = ${value}`);
  }
  if (query.topicKey) conditions.push(sql`${searchDocuments.facets}->'topicKeys' @> ${JSON.stringify([query.topicKey])}::jsonb`);
  if (query.tagKey) conditions.push(sql`${searchDocuments.facets}->'tagKeys' @> ${JSON.stringify([query.tagKey])}::jsonb`);
  return conditions;
}

function scoreDocument(
  doc: { title: string; normalizedText: string; publishedAt: Date | null; popularity: number; entityType: string },
  parsed: { tokens: string[]; phrase: string | null; synonyms: Map<string, string[]> },
): { total: number; textScore: number } {
  const normTitle = normalizePersian(doc.title);
  /**
   * امتیاز به دو بخش تفکیک می‌شود: «امتیاز متنی» (میزان تطابق واقعی با عبارت
   * جست‌وجو) و امتیازهای کمکی (تازگی، محبوبیت، نوع موجودیت). سندی که امتیاز متنی
   * صفر دارد هرگز در نتایج ظاهر نمی‌شود؛ در غیر این صورت تازگی و محبوبیت باعث
   * می‌شد همه اسناد در هر جست‌وجویی برگردند.
   */
  let textScore = 0;

  if (parsed.phrase) {
    if (normTitle.includes(parsed.phrase)) textScore += RANKING.titleExact;
    else if (doc.normalizedText.includes(parsed.phrase)) textScore += RANKING.titleExact * 0.5;
    else return { total: -1, textScore: -1 }; // عبارت دقیق برقرار نیست
  }

  const titleWords = normTitle.split(' ');
  const bodyWords = doc.normalizedText.split(' ');

  /**
   * تطبیق در سطح «کلمه» انجام می‌شود نه صرفاً زیررشته؛ در غیر این صورت جست‌وجوی
   * «آب» با کلمه «خیابان» هم مطابقت پیدا می‌کرد. زیررشته فقط برای توکن‌های بلندتر
   * و با امتیاز کمتر پذیرفته می‌شود.
   */
  const wordHit = (words: string[], token: string) =>
    words.some((w) => w === token || w.startsWith(token) || (token.length >= 4 && w.includes(token)));

  for (const token of parsed.tokens) {
    const inTitle = wordHit(titleWords, token);
    const inBody = wordHit(bodyWords, token);
    if (inTitle) textScore += RANKING.titleToken;
    else if (inBody) textScore += RANKING.bodyToken;
    else if (fuzzyMatches(token, normTitle)) textScore += RANKING.titleToken * RANKING.fuzzyPenalty;
    else if (fuzzyMatches(token, doc.normalizedText)) textScore += RANKING.bodyToken * RANKING.fuzzyPenalty;
    else {
      const syns = parsed.synonyms.get(token) ?? [];
      const hit = syns.find((s) => doc.normalizedText.includes(s));
      if (hit) textScore += RANKING.bodyToken * RANKING.synonymFactor;
    }
  }

  let score = textScore + (RANKING.entityBoost[doc.entityType] ?? 0);

  // تازگی محتوا
  if (doc.publishedAt) {
    const ageDays = (Date.now() - doc.publishedAt.getTime()) / 86_400_000;
    if (ageDays < RANKING.freshnessDays) score += RANKING.freshnessWeight * (1 - ageDays / RANKING.freshnessDays);
  }
  score += doc.popularity * RANKING.popularityWeight;
  return { total: score, textScore };
}

function makeHighlight(text: string, tokens: string[]): string | undefined {
  if (!tokens.length) return undefined;
  const idx = tokens.map((t) => text.indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
  if (idx === undefined) return undefined;
  const start = Math.max(0, idx - 60);
  return `${start > 0 ? '…' : ''}${text.slice(start, start + 180)}…`;
}

export async function search(query: SearchQuery, userId?: string | null) {
  const db = getDb();
  const page = Math.max(1, query.page ?? 1);
  const pageSize = Math.min(50, Math.max(1, query.pageSize ?? 12));

  const rawQuery = [query.q, query.exactPhrase, query.allWords, query.anyWords].filter(Boolean).join(' ').trim();
  const phrase = query.exactPhrase ? normalizePersian(query.exactPhrase) : null;
  const tokens = [...new Set([...tokenize(query.q ?? ''), ...tokenize(query.allWords ?? ''), ...tokenize(query.anyWords ?? '')])];
  const requiredTokens = tokenize(query.allWords ?? '');
  const excludedTokens = tokenize(query.noneWords ?? '');
  const synonyms = await expandSynonyms(tokens);

  const conditions = buildFilters(query);
  const rows = await db
    .select()
    .from(searchDocuments)
    .where(conditions.length ? and(...conditions) : undefined)
    .orderBy(desc(searchDocuments.publishedAt))
    .limit(3000);

  let scored: (SearchHit & { normalizedText: string })[] = [];
  for (const doc of rows) {
    // «بدون این کلمات»
    if (excludedTokens.some((t) => doc.normalizedText.includes(t))) continue;
    // «همه کلمات» باید موجود باشند (با تحمل غلط)
    if (requiredTokens.length && !requiredTokens.every((t) => doc.normalizedText.includes(t) || fuzzyMatches(t, doc.normalizedText))) continue;

    const hasCriteria = tokens.length > 0 || Boolean(phrase);
    const scored_ = hasCriteria ? scoreDocument(doc, { tokens, phrase, synonyms }) : { total: 1, textScore: 1 };
    // فقط اسنادی که واقعاً با عبارت جست‌وجو تطابق متنی دارند وارد نتایج می‌شوند
    if (scored_.textScore <= 0) continue;
    const score = scored_.total;

    scored.push({
      entityType: doc.entityType, entityId: doc.entityId, title: doc.title, summary: doc.summary,
      url: doc.url, publishedAt: doc.publishedAt, score, facets: doc.facets,
      normalizedText: doc.normalizedText,
      highlight: makeHighlight(doc.normalizedText, tokens),
    });
  }

  switch (query.sort) {
    case 'newest': scored.sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0)); break;
    case 'oldest': scored.sort((a, b) => (a.publishedAt?.getTime() ?? 0) - (b.publishedAt?.getTime() ?? 0)); break;
    case 'most_viewed':
    case 'most_discussed':
    case 'most_saved': scored.sort((a, b) => b.score - a.score); break;
    default: scored.sort((a, b) => b.score - a.score);
  }

  const total = scored.length;
  const items = scored.slice((page - 1) * pageSize, page * pageSize).map(({ normalizedText, ...rest }) => rest);

  // شمارش دسته‌ای نتایج برای نمایش فیلترها
  const typeCounts: Record<string, number> = {};
  for (const hit of scored) typeCounts[hit.entityType] = (typeCounts[hit.entityType] ?? 0) + 1;

  if (rawQuery) {
    await db.insert(searchQueries).values({
      userId: userId ?? null, rawQuery, normalizedQuery: normalizePersian(rawQuery), resultCount: total,
    });
    await db.insert(analyticsEvents).values({
      kind: total === 0 ? 'failed_search' : 'search',
      metadata: { query: rawQuery, total }, userId: userId ?? null,
    });
  }

  return {
    total, page, pageSize, items, typeCounts,
    normalizedQuery: normalizePersian(rawQuery),
    suggestions: total === 0 ? await suggestAlternatives(tokens) : [],
  };
}

/** پیشنهاد جایگزین وقتی نتیجه‌ای پیدا نشد (بر پایه عناوین موجود در ایندکس) */
async function suggestAlternatives(tokens: string[]): Promise<string[]> {
  if (!tokens.length) return [];
  const db = getDb();
  const rows = await db.select({ title: searchDocuments.title }).from(searchDocuments).limit(1500);
  const out = new Set<string>();
  for (const row of rows) {
    const words = normalizePersian(row.title).split(' ');
    for (const token of tokens) {
      for (const word of words) {
        if (word.length > 2 && fuzzyMatches(token, word)) out.add(word);
      }
    }
  }
  return [...out].slice(0, 6);
}

/** تکمیل خودکار برای نوار جست‌وجو */
export async function autocomplete(term: string, limit = 8) {
  const normalized = normalizePersian(term);
  if (!normalized) return [];
  const db = getDb();
  const rows = await db
    .select({
      title: searchDocuments.title, url: searchDocuments.url, entityType: searchDocuments.entityType,
      popularity: searchDocuments.popularity,
    })
    .from(searchDocuments)
    .where(sql`${searchDocuments.normalizedText} LIKE ${'%' + normalized + '%'}`)
    .orderBy(desc(searchDocuments.popularity))
    .limit(limit * 3);

  const scored = rows
    .map((r) => ({ ...r, s: normalizePersian(r.title).startsWith(normalized) ? 2 : 1 }))
    .sort((a, b) => b.s - a.s || b.popularity - a.popularity)
    .slice(0, limit);
  return scored.map(({ s, ...rest }) => rest);
}

/** جست‌وجوهای پرتکرار اخیر (برای نمایش «داغ‌ترین جست‌وجوها») */
export async function trendingSearches(limit = 8) {
  const db = getDb();
  const rows = await db.execute<{ normalized_query: string; c: number }>(sql`
    SELECT normalized_query, COUNT(*)::int AS c
    FROM search_queries
    WHERE created_at > now() - interval '14 days' AND result_count > 0
    GROUP BY normalized_query
    ORDER BY c DESC
    LIMIT ${limit}
  `);
  return (rows.rows ?? []).map((r) => ({ query: r.normalized_query, count: r.c }));
}
