/**
 * API endpoint to search Discord messages
 * Searches message content and embed content
 */
import sql from '../../../utils/db.ts';
import { requireApiAuth } from '../../../utils/apiAuth.ts';
import {
  checkRateLimit,
  recordRequest,
  getCookieId,
} from '../../../utils/rateLimit.ts';
import { signVideoId } from './cached-video.ts';
import { signImageId } from './cached-image.ts';

/**
 * Escapes special characters in a string for safe use in SQL LIKE/ILIKE queries.
 *
 * This function prevents SQL injection and pattern manipulation by escaping
 * the LIKE/ILIKE metacharacters: %, _, and \. It should be used on any user input
 * that will be interpolated into a LIKE or ILIKE pattern.
 *
 * @param {string} str - The input string to escape.
 * @returns {string} The escaped string, safe for use in LIKE/ILIKE patterns.
 */
function escapeLikePattern(str) {
  return str.replace(/[%_\\]/g, '\\$&');
}

/**
 * Normalize video URL by removing query parameters and hash fragments for cache lookup
 */
function normalizeVideoUrl(url) {
  if (!url) return url;
  try {
    const urlObj = new URL(url);
    return urlObj.origin + urlObj.pathname;
  } catch {
    return url;
  }
}

/**
 * Returns cached image URL if available, otherwise returns proxy URL
 */
function getCachedOrProxyUrl(cachedImageId, proxyUrl) {
  if (cachedImageId) {
    const sig = signImageId(cachedImageId);
    return `/api/discord/cached-image?id=${cachedImageId}&sig=${sig}`;
  }
  return proxyUrl;
}

export async function GET({ request }) {
  // Rate limit check for authenticated endpoints
  const rateCheck = checkRateLimit(request, {
    limit: 10,
    windowMs: 1000,
    burstLimit: 50,
    burstWindowMs: 10_000,
  });

  if (!rateCheck.allowed) {
    return new Response(JSON.stringify({ 
      error: rateCheck.isFlooding ? 'Too many requests - please slow down' : 'Rate limit exceeded' 
    }), {
      status: 429,
      headers: { 'Content-Type': 'application/json', 'Retry-After': '1' },
    });
  }

  recordRequest(request, 1000);

  // Check authentication
  const { user, error: authError, setCookieHeader } = await requireApiAuth(request);
  if (authError) return authError;
  if (authError) return authError;
  
  // Helper to create responses with optional Set-Cookie header
  const createResponse = (data, status = 200) => {
    const headers = new Headers({ 'Content-Type': 'application/json' });
    if (setCookieHeader) {
      const cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
      for (const c of cookies) if (c) headers.append('Set-Cookie', c.trim());
    }
    return new Response(JSON.stringify(data), { status, headers });
  };

  // Helper to validate Discord snowflake IDs (17-20 digit strings)
  const isValidSnowflake = (id) => !id || /^\d{17,20}$/.test(id);

  try {
    const url = new URL(request.url);
    const channelId = url.searchParams.get('channelId');
    const query = url.searchParams.get('q');
    const limit = Math.min(parseInt(url.searchParams.get('limit') || '50', 10), 100);

    if (!channelId) {
      return createResponse({ error: 'channelId is required' }, 400);
    }

    // Validate snowflake format
    if (!isValidSnowflake(channelId)) {
      return createResponse({ error: 'Invalid channelId format' }, 400);
    }

    if (!query || query.trim().length < 2) {
      return createResponse({ error: 'Search query must be at least 2 characters' }, 400);
    }

    // Escape LIKE metacharacters to prevent pattern injection
    const escapedQuery = escapeLikePattern(query);
    const searchPattern = `%${escapedQuery}%`;

    // Search messages by content, author username, or embed content
    const messages = await sql`
      SELECT DISTINCT
        m.message_id,
        m.content,
        m.created_at,
        m.edited_at,
        m.reference_message_id,
        m.channel_id,
        u.user_id as author_id,
        u.username as author_username,
        u.discriminator as author_discriminator,
        u.avatar_url as author_avatar,
        u.is_bot as author_bot,
        COALESCE(gm.nickname, u.global_name, u.username) as author_display_name,
        COALESCE(gm.guild_avatar_url, u.avatar_url) as author_guild_avatar,
        (
          SELECT r.color 
          FROM roles r 
          WHERE r.role_id = ANY(gm.roles) 
            AND r.color IS NOT NULL 
            AND r.color != 0
          ORDER BY r.position DESC 
          LIMIT 1
        ) as author_color
      FROM messages m
      LEFT JOIN users u ON m.author_id = u.user_id
      LEFT JOIN channels c ON m.channel_id = c.channel_id
      LEFT JOIN guild_members gm ON u.user_id = gm.user_id AND c.guild_id = gm.guild_id
      LEFT JOIN embeds e ON m.message_id = e.message_id
      WHERE m.channel_id = ${channelId}
        AND m.is_deleted = FALSE
        AND (
          m.content ILIKE ${searchPattern}
          OR u.username ILIKE ${searchPattern}
          OR u.global_name ILIKE ${searchPattern}
          OR gm.nickname ILIKE ${searchPattern}
          OR e.title ILIKE ${searchPattern}
          OR e.description ILIKE ${searchPattern}
          OR e.author_name ILIKE ${searchPattern}
        )
      ORDER BY m.created_at DESC
      LIMIT ${limit}
    `;

    if (messages.length === 0) {
      return createResponse({ messages: [], totalCount: 0 });
    }

    // Get message IDs for fetching related data
    const messageIds = messages.map(m => m.message_id);

    // Fetch attachments with cached video info via JOIN
    const attachments = await sql`
      SELECT 
        a.message_id,
        a.attachment_id,
        a.filename,
        a.url,
        a.proxy_url,
        a.content_type,
        a.size,
        a.width,
        a.height,
        a.cached_image_id,
        a.cached_video_id,
        vc.file_path as video_file_path
      FROM attachments a
      LEFT JOIN video_cache vc ON a.cached_video_id = vc.video_id
      WHERE a.message_id = ANY(${messageIds})
      ORDER BY a.attachment_id
    ` as unknown as Array<{
      message_id: string;
      attachment_id: string;
      filename: string | null;
      url: string;
      proxy_url: string | null;
      content_type: string | null;
      size: number | null;
      width: number | null;
      height: number | null;
      cached_image_id: number | null;
      cached_video_id: number | null;
      video_file_path: string | null;
    }>;

    // Fetch cached video URLs for video attachments
    const videoUrls = attachments
      .filter(att => att.content_type && att.content_type.startsWith('video/'))
      .map(att => normalizeVideoUrl(att.url));
    
    const cachedVideos = videoUrls.length > 0 ? await sql`
      SELECT source_url, video_id
      FROM video_cache
      WHERE source_url = ANY(${videoUrls})
    ` as unknown as Array<{ source_url: string; video_id: number }> : [];

    const videoCacheMap: Record<string, number> = {};
    cachedVideos.forEach(v => {
      videoCacheMap[v.source_url] = v.video_id;
    });

    // Fetch embeds
    const embeds = await sql`
      SELECT 
        embed_id, message_id, embed_type, title, description, url,
        color, timestamp as embed_timestamp,
        footer_text, footer_icon_url,
        image_url, image_width, image_height,
        thumbnail_url, thumbnail_width, thumbnail_height,
        video_url, video_width, video_height,
        provider_name, provider_url,
        author_name, author_url, author_icon_url
      FROM embeds
      WHERE message_id = ANY(${messageIds})
    `;

    // Fetch reactions
    const reactions = await sql`
      SELECT 
        message_id,
        emoji_id,
        emoji_name,
        emoji_animated,
        COUNT(*) as count
      FROM reactions
      WHERE message_id = ANY(${messageIds})
        AND is_removed = FALSE
      GROUP BY message_id, emoji_id, emoji_name, emoji_animated
    `;

    // Fetch referenced messages (for replies)
    const referencedIds = messages
      .filter(m => m.reference_message_id)
      .map(m => m.reference_message_id);

    let referencedMessages: Array<{
      message_id: string;
      content: string;
      author_id: string;
      author_username: string;
      author_display_name: string;
    }> = [];
    if (referencedIds.length > 0) {
      referencedMessages = await sql`
        SELECT 
          m.message_id,
          m.content,
          u.user_id as author_id,
          u.username as author_username,
          COALESCE(gm.nickname, u.global_name, u.username) as author_display_name
        FROM messages m
        LEFT JOIN users u ON m.author_id = u.user_id
        LEFT JOIN channels c ON m.channel_id = c.channel_id
        LEFT JOIN guild_members gm ON u.user_id = gm.user_id AND c.guild_id = gm.guild_id
        WHERE m.message_id = ANY(${referencedIds})
      `;
    }

    // Build referenced message map
    const referencedMap = {};
    referencedMessages.forEach(ref => {
      referencedMap[ref.message_id] = {
        id: ref.message_id,
        content: ref.content,
        author: {
          id: ref.author_id,
          username: ref.author_username,
          displayName: ref.author_display_name,
        },
      };
    });

    // Group attachments, embeds, and reactions by message
    const attachmentsByMessage = {};
    attachments.forEach(att => {
      if (!attachmentsByMessage[att.message_id]) {
        attachmentsByMessage[att.message_id] = [];
      }
      
      // Determine the best URL to use
      let attachmentUrl = att.url;
      let attachmentProxyUrl = att.proxy_url;
      
      // Check if this is a video and has a cached version
      if (att.content_type && att.content_type.startsWith('video/')) {
        // Use cached_video_id from the JOIN if available
        if (att.cached_video_id && att.video_file_path) {
          const sig = signVideoId(att.cached_video_id);
          attachmentUrl = `/api/discord/cached-video?id=${att.cached_video_id}&sig=${sig}`;
          attachmentProxyUrl = attachmentUrl;
        } else {
          // Fallback to URL-based lookup
          const normalizedUrl = normalizeVideoUrl(att.url);
          const cachedVideoId = videoCacheMap[normalizedUrl];
          if (cachedVideoId) {
            const sig = signVideoId(cachedVideoId);
            attachmentUrl = `/api/discord/cached-video?id=${cachedVideoId}&sig=${sig}`;
            attachmentProxyUrl = attachmentUrl;
          }
        }
      } else {
        // For images, use cached version if available
        const cachedUrl = getCachedOrProxyUrl(att.cached_image_id, att.proxy_url);
        attachmentUrl = cachedUrl;
        attachmentProxyUrl = cachedUrl;
      }
      
      attachmentsByMessage[att.message_id].push({
        id: att.attachment_id,
        filename: att.filename,
        url: attachmentUrl,
        proxyUrl: attachmentProxyUrl,
        content_type: att.content_type,
        size: att.size,
        width: att.width,
        height: att.height,
      });
    });

    const embedsByMessage = {};
    embeds.forEach(emb => {
      if (!embedsByMessage[emb.message_id]) {
        embedsByMessage[emb.message_id] = [];
      }
      embedsByMessage[emb.message_id].push({
        type: emb.embed_type,
        title: emb.title,
        description: emb.description,
        url: emb.url,
        color: emb.color,
        timestamp: emb.embed_timestamp,
        footer: emb.footer_text ? {
          text: emb.footer_text,
          iconUrl: emb.footer_icon_url,
        } : null,
        image: emb.image_url ? {
          url: emb.image_url,
          width: emb.image_width,
          height: emb.image_height,
        } : null,
        thumbnail: emb.thumbnail_url ? {
          url: emb.thumbnail_url,
          width: emb.thumbnail_width,
          height: emb.thumbnail_height,
        } : null,
        video: emb.video_url ? {
          url: emb.video_url,
          width: emb.video_width,
          height: emb.video_height,
        } : null,
        provider: emb.provider_name ? {
          name: emb.provider_name,
          url: emb.provider_url,
        } : null,
        author: emb.author_name ? {
          name: emb.author_name,
          url: emb.author_url,
          iconUrl: emb.author_icon_url,
        } : null,
        fields: [],
      });
    });

    const reactionsByMessage = {};
    reactions.forEach(r => {
      if (!reactionsByMessage[r.message_id]) {
        reactionsByMessage[r.message_id] = [];
      }
      reactionsByMessage[r.message_id].push({
        emoji: {
          id: r.emoji_id,
          name: r.emoji_name,
          animated: r.emoji_animated,
        },
        count: r.count,
      });
    });

    // Format response
    const formattedMessages = messages.map(msg => ({
      id: msg.message_id,
      content: msg.content,
      timestamp: msg.created_at,
      editedAt: msg.edited_at,
      author: {
        id: msg.author_id,
        username: msg.author_username,
        discriminator: msg.author_discriminator,
        avatar: msg.author_guild_avatar || msg.author_avatar,
        displayName: msg.author_display_name,
        bot: msg.author_bot,
        color: msg.author_color ? `#${msg.author_color.toString(16).padStart(6, '0')}` : null,
      },
      attachments: attachmentsByMessage[msg.message_id] || [],
      embeds: embedsByMessage[msg.message_id] || [],
      reactions: reactionsByMessage[msg.message_id] || [],
      referencedMessage: msg.reference_message_id ? referencedMap[msg.reference_message_id] : null,
    }));

    return createResponse({ 
      messages: formattedMessages,
      totalCount: formattedMessages.length,
    });
  } catch (error) {
    console.error('Search error:', error);
    return createResponse({ error: 'Failed to search messages' }, 500);
  }
}
