import type { Context } from 'telegraf';
import type { Db } from '../db/sqlite.js';
import { searchMessages, type StoredMessage } from '../db/messages.js';
import { generateTextCLI } from '../gemini/cli.js';
import { sendChunked } from '../agent/chunker.js';
import { logger } from '../utils/logger.js';

export interface SearchHandlerOptions {
  db: Db;
  chatId: number;
  googleApiKey: string;
}

// --- Query parser ---

interface ParsedSearch {
  mode: 'literal' | 'semantic';
  keywords: string[];
  userFilter: string | null;
  fromDate: number | null;
  intent: string; // original intent for semantic re-ranking
}

async function parseSearchQuery(query: string, apiKey: string): Promise<ParsedSearch> {
  const now = Math.floor(Date.now() / 1000);

  const prompt = `Parse this Telegram group chat search query into structured JSON.
Now (Unix timestamp): ${now}

Query: "${query}"

Decide if this is:
- "literal": a simple keyword/string search (e.g. "Astro", "Morning API", "login bug")
- "semantic": a natural language description of something (e.g. "that payment provider AP mentioned", "the decision we made about auth")

Return ONLY valid JSON:
{
  "mode": "literal" | "semantic",
  "keywords": [<1-4 key search terms extracted from the query>],
  "userFilter": <username or first name if a specific person is mentioned, else null>,
  "fromDate": <Unix timestamp if a time range is mentioned, else null>,
  "intent": <the original search intent rephrased as a clear description>
}

Examples:
- "Astro" → mode: literal, keywords: ["Astro"], userFilter: null, fromDate: null
- "Morning API error" → mode: literal, keywords: ["Morning", "API", "error"], userFilter: null, fromDate: null
- "that payment provider AP mentioned last week" → mode: semantic, keywords: ["payment", "provider"], userFilter: "AP", fromDate: ${now - 604800}, intent: "payment provider that AP mentioned"
- "the auth decision we made" → mode: semantic, keywords: ["auth", "authentication"], userFilter: null, fromDate: null, intent: "decision about authentication approach"`;

  const raw = await generateTextCLI(prompt, apiKey, 'search');
  const match = raw.match(/\{[\s\S]*\}/);
  if (!match) throw new Error('Could not parse search query');
  return JSON.parse(match[0]) as ParsedSearch;
}

// --- Semantic re-ranker ---

async function semanticFilter(
  messages: StoredMessage[],
  intent: string,
  apiKey: string,
): Promise<{ msg: StoredMessage; reason: string }[]> {
  if (messages.length === 0) return [];

  const transcript = messages
    .map((m) => {
      const sender = m.username ? `@${m.username}` : (m.first_name ?? 'Unknown');
      const time = new Date(m.date * 1000).toLocaleString('he-IL', { timeZone: 'Asia/Jerusalem' });
      return `[ID:${m.id}] [${time}] ${sender}: ${m.text}`;
    })
    .join('\n');

  const prompt = `You are searching a Telegram group chat history.

Search intent: "${intent}"

Messages:
${transcript}

Return ONLY valid JSON - an array of relevant matches:
[
  { "id": <message id>, "reason": "<one short sentence why this matches>" },
  ...
]

Only include messages that genuinely match the search intent. Return empty array [] if nothing matches. Max 10 results.`;

  const raw = await generateTextCLI(prompt, apiKey, 'search');
  const match = raw.match(/\[[\s\S]*\]/);
  if (!match) return [];

  const hits = JSON.parse(match[0]) as { id: number; reason: string }[];
  const msgById = new Map(messages.map((m) => [m.id, m]));

  return hits
    .filter((h) => msgById.has(h.id))
    .map((h) => ({ msg: msgById.get(h.id)!, reason: h.reason }));
}

// --- Result formatter ---

function formatMessage(m: StoredMessage): string {
  const sender = m.username ? `@${m.username}` : (m.first_name ?? 'Unknown');
  const time = new Date(m.date * 1000).toLocaleString('he-IL', {
    timeZone: 'Asia/Jerusalem',
    day: '2-digit',
    month: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  });
  const text = m.text.length > 200 ? m.text.slice(0, 200) + '…' : m.text;
  return `[${time}] ${sender}: ${text}`;
}

// --- Handler factory ---

export function makeSearchHandler(opts: SearchHandlerOptions) {
  return async function searchHandler(ctx: Context): Promise<void> {
    const msgText = ctx.message && 'text' in ctx.message ? ctx.message.text : '';
    const query = msgText.replace(/^\/search\s*/i, '').trim();

    if (!query) {
      await ctx.reply(
        'Usage: /search <query>\n\nExamples:\n' +
          '  /search Astro\n' +
          '  /search Morning API error\n' +
          '  /search for that payment provider AP mentioned last week',
      );
      return;
    }

    await ctx.reply('🔍 Searching...');

    let parsed: ParsedSearch;
    try {
      parsed = await parseSearchQuery(query, opts.googleApiKey);
      logger.debug('search query parsed', { parsed });
    } catch {
      await ctx.reply('Could not understand that search. Try: /search Astro');
      return;
    }

    const candidates = searchMessages(opts.db, {
      chatId: opts.chatId,
      keywords: parsed.keywords,
      userFilter: parsed.userFilter ?? undefined,
      fromDate: parsed.fromDate ?? undefined,
      limit: parsed.mode === 'semantic' ? 150 : 30,
    });

    if (candidates.length === 0) {
      await ctx.reply(`No messages found for: "${query}"`);
      return;
    }

    if (parsed.mode === 'literal') {
      const lines = candidates.slice(0, 20).map(formatMessage);
      const header = `🔍 *${candidates.length} result${candidates.length !== 1 ? 's' : ''}* for "${query}":`;
      await sendChunked(ctx, `${header}\n\n${lines.join('\n\n')}`);
      return;
    }

    // Semantic: re-rank with Gemini
    try {
      const hits = await semanticFilter(candidates, parsed.intent, opts.googleApiKey);

      if (hits.length === 0) {
        await ctx.reply(`No relevant messages found for: "${query}"`);
        return;
      }

      const lines = hits.map(({ msg, reason }) => `${formatMessage(msg)}\n  ↳ _${reason}_`);
      const header = `🔍 *${hits.length} match${hits.length !== 1 ? 'es' : ''}* for "${query}":`;
      await sendChunked(ctx, `${header}\n\n${lines.join('\n\n')}`);
    } catch (err) {
      logger.error('semantic search failed', { error: String(err) });
      // Fallback to literal results
      const lines = candidates.slice(0, 10).map(formatMessage);
      await sendChunked(ctx, `🔍 Results for "${query}":\n\n${lines.join('\n\n')}`);
    }
  };
}
