/**
 * API endpoint to fetch Discord messages from database
 * Includes user info, attachments, embeds, and reactions
 */
import sql from '../../../utils/db.ts';
import { requireApiAuth } from '../../../utils/apiAuth.ts';
import {
  checkRateLimit,
  recordRequest,
} from '../../../utils/rateLimit.ts';
import { signImageId } from './cached-image.ts';
import { signVideoId } from './cached-video.ts';
import crypto from 'crypto';

import type { APIContext } from 'astro';

const IMAGE_PROXY_SECRET = import.meta.env.IMAGE_PROXY_SECRET;
if (!IMAGE_PROXY_SECRET) {
  console.error('WARNING: IMAGE_PROXY_SECRET not set, image signing will fail');
}

// Trusted domains that don't need proxying
const TRUSTED_DOMAINS = new Set([
  'cdn.discordapp.com',
  'media.discordapp.net',
  'i.imgur.com',
  'imgur.com',
  'i.redd.it',
  'preview.redd.it',
  'youtube.com',
  'i.ytimg.com',
  'img.youtube.com',
  'youtu.be',
  'twitter.com',
  'pbs.twimg.com',
  'abs.twimg.com',
  'instagram.com',
  'github.com',
  'raw.githubusercontent.com',
  'avatars.githubusercontent.com',
  'user-images.githubusercontent.com',
]);

function isTrustedDomain(url: string): boolean {
  try {
    const hostname = new URL(url).hostname.toLowerCase();
    return TRUSTED_DOMAINS.has(hostname) || 
           Array.from(TRUSTED_DOMAINS).some(domain => hostname.endsWith('.' + domain));
  } catch {
    return false;
  }
}

function generateSignature(url: string): string {
  return crypto
    .createHmac('sha256', IMAGE_PROXY_SECRET)
    .update(url)
    .digest('hex');
}

/**
 * Get the best URL for an image - prefer cached version if available
 * @param {number|null} cachedImageId - The cached image ID from database
 * @param {string|null} originalUrl - The original CDN URL
 * @param {string} baseUrl - Base URL for constructing cached image URLs
 * @returns {string|null} The URL to use
 */
function getCachedOrProxyUrl(cachedImageId: number | null, originalUrl: string | null, baseUrl: string): string | null {
  // Prefer cached image if available
  if (cachedImageId) {
    const sig = signImageId(cachedImageId);
    return `${baseUrl}/api/discord/cached-image?id=${cachedImageId}&sig=${sig}`;
  }
  
  // Fall back to original URL with proxy if needed
  if (!originalUrl) return null;
  
  if (isTrustedDomain(originalUrl)) {
    return originalUrl;
  }
  
  const signature = generateSignature(originalUrl);
  const encodedUrl = encodeURIComponent(originalUrl);
  return `${baseUrl}/api/image-proxy?url=${encodedUrl}&sig=${signature}`;
}

function getSafeImageUrl(originalUrl: string | null, baseUrl: string): string | null {
  if (!originalUrl) return null;
  
  if (isTrustedDomain(originalUrl)) {
    return originalUrl;
  }
  
  const signature = generateSignature(originalUrl);
  const encodedUrl = encodeURIComponent(originalUrl);
  return `${baseUrl}/api/image-proxy?url=${encodedUrl}&sig=${signature}`;
}

export async function GET({ request }: APIContext) {
  // Rate limit check
  const rateCheck = checkRateLimit(request, {
    limit: 100,
    windowMs: 1000,
    burstLimit: 400,
    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;
  
  // Helper to create responses with optional Set-Cookie header
  const createResponse = (data: unknown, 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: string | null) => !id || /^\d{17,20}$/.test(id);

  try {
    const url = new URL(request.url);
    const channelId = url.searchParams.get('channelId');
    const limit = Math.min(parseInt(url.searchParams.get('limit') || '50', 10), 100);
    const before = url.searchParams.get('before'); // For pagination
    const after = url.searchParams.get('after');
    const around = url.searchParams.get('around'); // For jumping to specific message
    const editedSince = url.searchParams.get('editedSince'); // ISO timestamp for fetching recently edited messages
    const oldest = url.searchParams.get('oldest') === 'true'; // Fetch oldest messages first
    
    const baseUrl = `${url.protocol}//${url.host}`;

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

    // Validate snowflake formats
    if (!isValidSnowflake(channelId) || !isValidSnowflake(before) || !isValidSnowflake(after) || !isValidSnowflake(around)) {
      return createResponse({ error: 'Invalid ID format' }, 400);
    }

    // Get guild_id from channel
    const channelInfo = await sql`
      SELECT guild_id FROM channels WHERE channel_id = ${channelId}
    `;
    const guildId = channelInfo[0]?.guild_id;

    // Build the query with optional pagination
    // Joins guild_members for nickname and gets highest role color
    let messages;
    
    if (editedSince) {
      // Fetch messages edited since the given timestamp
      const editedSinceDate = new Date(editedSince);
      messages = await sql`
        SELECT 
          m.message_id,
          m.raw_data,
          m.message_type,
          m.content,
          m.created_at,
          m.edited_at,
          m.reference_message_id,
          m.channel_id,
          m.webhook_id,
          m.reference_guild_id,
          c.guild_id as message_guild_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
        WHERE m.channel_id = ${channelId}
          AND m.edited_at > ${editedSinceDate}
          AND m.is_deleted = FALSE
        ORDER BY m.edited_at DESC
        LIMIT ${limit}
      `;
    } else if (before) {
      messages = await sql`
        SELECT 
          m.message_id,
          m.raw_data,
          m.message_type,
          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
        WHERE m.channel_id = ${channelId}
          AND m.message_id < ${before}
          AND m.is_deleted = FALSE
        ORDER BY m.created_at DESC
        LIMIT ${limit}
      `;
    } else if (after) {
      messages = await sql`
        SELECT 
          m.message_id,
          m.raw_data,
          m.message_type,
          m.content,
          m.created_at,
          m.edited_at,
          m.reference_message_id,
          m.channel_id,
          m.webhook_id,
          m.reference_guild_id,
          c.guild_id as message_guild_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
        WHERE m.channel_id = ${channelId}
          AND m.message_id > ${after}
          AND m.is_deleted = FALSE
        ORDER BY m.created_at ASC
        LIMIT ${limit}
      `;
    } else if (around) {
      // Fetch messages around a specific message ID (half before, half after)
      const halfLimit = Math.floor(limit / 2);
      messages = await sql`
        (
          SELECT 
            m.message_id,
            m.raw_data,
            m.message_type,
            m.content,
            m.created_at,
            m.edited_at,
            m.reference_message_id,
            m.channel_id,
            m.webhook_id,
            m.reference_guild_id,
            c.guild_id as message_guild_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
          WHERE m.channel_id = ${channelId}
            AND m.message_id <= ${around}
            AND m.is_deleted = FALSE
          ORDER BY m.created_at DESC
          LIMIT ${halfLimit + 1}
        )
        UNION ALL
        (
          SELECT 
            m.message_id,
            m.raw_data,
            m.message_type,
            m.content,
            m.created_at,
            m.edited_at,
            m.reference_message_id,
            m.channel_id,
            m.webhook_id,
            m.reference_guild_id,
            c.guild_id as message_guild_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
          WHERE m.channel_id = ${channelId}
            AND m.message_id > ${around}
            AND m.is_deleted = FALSE
          ORDER BY m.created_at ASC
          LIMIT ${halfLimit}
        )
        ORDER BY created_at ASC
      `;
    } else if (oldest) {
      // Fetch oldest messages first
      messages = await sql`
        SELECT 
          m.message_id,
          m.raw_data,
          m.message_type,
          m.content,
          m.created_at,
          m.edited_at,
          m.reference_message_id,
          m.channel_id,
          m.webhook_id,
          m.reference_guild_id,
          c.guild_id as message_guild_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
        WHERE m.channel_id = ${channelId}
          AND m.is_deleted = FALSE
        ORDER BY m.created_at ASC
        LIMIT ${limit}
      `;
    } else {
      messages = await sql`
        SELECT 
          m.message_id,
          m.raw_data,
          m.message_type,
          m.content,
          m.created_at,
          m.edited_at,
          m.reference_message_id,
          m.channel_id,
          m.webhook_id,
          m.reference_guild_id,
          c.guild_id as message_guild_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
        WHERE m.channel_id = ${channelId}
          AND m.is_deleted = FALSE
        ORDER BY m.created_at DESC
        LIMIT ${limit}
      `;
    }

    // Messages are already in DESC order (newest first) - no need to reverse

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

    // Get all message IDs for fetching related data
    const messageIds = messages.map(m => m.message_id);
    
    // Get unique author IDs for fetching cached avatar info
    const authorIds = [...new Set(messages.map(m => m.author_id).filter(Boolean))];
    
    // Fetch cached avatar IDs for all authors
    const userAvatarCache: Record<string, { cachedAvatarId: number | null }> = {};
    if (authorIds.length > 0) {
      const avatarInfo = await sql`
        SELECT 
          u.user_id,
          u.cached_avatar_id,
          gm.cached_guild_avatar_id
        FROM users u
        LEFT JOIN guild_members gm ON u.user_id = gm.user_id AND gm.guild_id = ${guildId}
        WHERE u.user_id = ANY(${authorIds})
      ` as unknown as Array<{ user_id: string; cached_avatar_id: number | null; cached_guild_avatar_id: number | null }>;
      for (const info of avatarInfo) {
        userAvatarCache[info.user_id.toString()] = {
          cachedAvatarId: info.cached_guild_avatar_id || info.cached_avatar_id,
        };
      }
    }

    // Extract all mentioned user IDs from message content
    const mentionedUserIds = new Set();
    const userMentionRegex = /<@!?(\d+)>/g;
    for (const msg of messages) {
      if (msg.content) {
        let match;
        while ((match = userMentionRegex.exec(msg.content)) !== null) {
          mentionedUserIds.add(match[1]);
        }
      }
    }

    // Fetch mentioned users with their display names and colors
    let mentionedUsers: { user_id: string; username: string; display_name: string; color: number | null }[] = [];
    if (mentionedUserIds.size > 0) {
      const mentionedIds = Array.from(mentionedUserIds);
      mentionedUsers = await sql`
        SELECT 
          u.user_id,
          u.username,
          COALESCE(gm.nickname, u.global_name, u.username) as display_name,
          (
            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 color
        FROM users u
        LEFT JOIN guild_members gm ON u.user_id = gm.user_id AND gm.guild_id = ${guildId}
        WHERE u.user_id = ANY(${mentionedIds})
      ` as unknown as { user_id: string; username: string; display_name: string; color: number | null }[];
    }

    // Build users map for mentions
    const usersMap = {};
    for (const user of mentionedUsers) {
      const colorHex = user.color ? `#${user.color.toString(16).padStart(6, '0')}` : null;
      usersMap[user.user_id.toString()] = {
        username: user.username,
        displayName: user.display_name,
        color: colorHex,
      };
    }

    // Fetch attachments for all messages
    const attachments = await sql`
      SELECT 
        a.attachment_id, a.message_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})
    `;

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

    // Fetch message components (buttons, selects, etc.) for all messages
    const messageComponents = await sql`
      SELECT
        component_id, message_id, component_type, custom_id, label, style, url, disabled, placeholder,
        min_values, max_values, raw_data
      FROM message_components
      WHERE message_id = ANY(${messageIds})
      ORDER BY component_id
    `;

    // Fetch embed fields for all embeds
    const embedIds = (embeds as unknown as Array<{ embed_id: number }>).map(e => e.embed_id);
    let embedFields: { embed_id: number; name: string; value: string; inline: boolean; position: number }[] = [];
    if (embedIds.length > 0) {
      embedFields = await sql`
        SELECT embed_id, name, value, inline, position
        FROM embed_fields
        WHERE embed_id = ANY(${embedIds})
        ORDER BY position ASC
      ` as unknown as { embed_id: number; name: string; value: string; inline: boolean; position: number }[];
    }

    // Index embed fields by embed_id
    const fieldsByEmbed: Record<number, Array<{ name: string; value: string; inline: boolean }>> = {};
    for (const field of embedFields) {
      if (!fieldsByEmbed[field.embed_id]) {
        fieldsByEmbed[field.embed_id] = [];
      }
      fieldsByEmbed[field.embed_id].push({
        name: field.name,
        value: field.value,
        inline: field.inline || false,
      });
    }

    // Fetch reactions for all messages (aggregate counts since each reaction is a row)
    // Join with emojis table to get cached_image_id for custom emojis
    const reactions = await sql`
      SELECT 
        r.message_id, r.emoji_id, r.emoji_name, r.emoji_animated,
        e.cached_image_id as emoji_cached_image_id,
        COUNT(*) FILTER (WHERE r.is_removed = FALSE) as count
      FROM reactions r
      LEFT JOIN emojis e ON r.emoji_id = e.emoji_id
      WHERE r.message_id = ANY(${messageIds})
        AND r.is_removed = FALSE
      GROUP BY r.message_id, r.emoji_id, r.emoji_name, r.emoji_animated, e.cached_image_id
    `;

    // Fetch stickers for all messages (include cached_image_id and lottie_data for locally cached stickers)
    const stickers = await sql`
      SELECT 
        ms.message_id,
        s.sticker_id,
        s.name,
        s.format_type,
        s.cached_image_id,
        s.lottie_data
      FROM message_stickers ms
      JOIN stickers s ON ms.sticker_id = s.sticker_id
      WHERE ms.message_id = ANY(${messageIds})
    `;

    // Fetch polls for all messages
    const polls = await sql`
      SELECT 
        p.message_id,
        p.question_text,
        p.question_emoji_id,
        p.question_emoji_name,
        p.question_emoji_animated,
        p.allow_multiselect,
        p.expiry,
        p.is_finalized,
        p.total_votes
      FROM polls p
      WHERE p.message_id = ANY(${messageIds})
    `;

    // Fetch poll answers
    const pollAnswers = await sql`
      SELECT 
        pa.message_id,
        pa.answer_id,
        pa.answer_text,
        pa.answer_emoji_id,
        pa.answer_emoji_name,
        pa.answer_emoji_animated,
        pa.vote_count
      FROM poll_answers pa
      WHERE pa.message_id = ANY(${messageIds})
      ORDER BY pa.answer_id
    `;

    // Extract all custom emoji IDs from message content to look up cached images
    const allEmojiIds = new Set();
    for (const msg of messages) {
      if (msg.content) {
        // Match <:name:id> and <a:name:id> patterns
        const matches = msg.content.matchAll(/<a?:\w+:(\d+)>/g);
        for (const match of matches) {
          allEmojiIds.add(match[1]);
        }
      }
    }
    // Also include poll emojis
    for (const poll of polls) {
      if (poll.question_emoji_id) allEmojiIds.add(String(poll.question_emoji_id));
    }
    for (const answer of pollAnswers) {
      if (answer.answer_emoji_id) allEmojiIds.add(String(answer.answer_emoji_id));
    }

    // Fetch emoji cache info for all referenced emojis
    let emojiCacheMap: Record<string, { url: string; animated: boolean }> = {};
    const emojiIdArray = Array.from(allEmojiIds);
    if (emojiIdArray.length > 0) {
      const emojiCacheRows = await sql`
        SELECT emoji_id, emoji_animated, cached_image_id
        FROM emojis
        WHERE emoji_id = ANY(${emojiIdArray}::bigint[])
          AND cached_image_id IS NOT NULL
      ` as unknown as Array<{ emoji_id: string; emoji_animated: boolean; cached_image_id: number }>;
      for (const row of emojiCacheRows) {
        const sig = signImageId(row.cached_image_id);
        emojiCacheMap[String(row.emoji_id)] = {
          url: `${baseUrl}/api/discord/cached-image?id=${row.cached_image_id}&sig=${sig}`,
          animated: row.emoji_animated,
        };
      }
    }

    // Fetch poll votes with user info
    const pollMessageIds = (polls as unknown as Array<{ message_id: string }>).map(p => p.message_id);
    let pollVotes: Array<{ message_id: string; answer_id: string; user_id: string; username: string; display_name: string; avatar_url: string | null }> = [];
    if (pollMessageIds.length > 0) {
      pollVotes = await sql`
        SELECT 
          pv.message_id,
          pv.answer_id,
          pv.user_id,
          u.username,
          COALESCE(gm.nickname, u.global_name, u.username) as display_name,
          COALESCE(gm.guild_avatar_url, u.avatar_url) as avatar_url
        FROM poll_votes pv
        LEFT JOIN users u ON pv.user_id = u.user_id
        LEFT JOIN guild_members gm ON pv.user_id = gm.user_id AND gm.guild_id = ${guildId}
        WHERE pv.message_id = ANY(${pollMessageIds})
          AND pv.is_removed = FALSE
        ORDER BY pv.voted_at ASC
      ` as unknown as Array<{ message_id: string; answer_id: string; user_id: string; username: string; display_name: string; avatar_url: string | null }>;
    }

    // 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_avatar: string | null; author_display_name: string; author_guild_avatar: string | null }> = [];
    if (referencedIds.length > 0) {
      referencedMessages = await sql`
        SELECT 
          m.message_id,
          m.content,
          u.user_id as author_id,
          u.username as author_username,
          u.avatar_url as author_avatar,
          COALESCE(gm.nickname, u.global_name, u.username) as author_display_name,
          COALESCE(gm.guild_avatar_url, u.avatar_url) as author_guild_avatar
        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})
      ` as Array<{ message_id: string; content: string; author_id: string; author_username: string; author_avatar: string | null; author_display_name: string; author_guild_avatar: string | null }>;
    }

    // Build a map of video cache entries for any attachment/embed video URLs
    const candidateVideoUrls: (string | null)[] = [];
    for (const att of attachments as unknown as Array<{ content_type: string | null; url: string | null; proxy_url: string | null }>) {
      if (att.content_type?.startsWith('video/')) {
        candidateVideoUrls.push(att.url || att.proxy_url);
      }
    }
    for (const emb of embeds as unknown as Array<{ video_url: string | null }>) {
      if (emb.video_url) candidateVideoUrls.push(emb.video_url);
    }

    // Query video_cache for any matching source_url values
    // Helper: normalize video source URL by stripping query/hash (Discord adds signatures)
    const normalizeVideoSrc = (url: string) => {
      if (!url) return url;
      try {
        const u = new URL(url);
        return `${u.origin}${u.pathname}`;
      } catch {
        return url.split('#')[0].split('?')[0];
      }
    };

    let videoCacheRows: Array<{ video_id: number; source_url: string; file_path: string; content_type: string; is_youtube: boolean; youtube_id: string | null }> = [];
    if (candidateVideoUrls.length > 0) {
      // dedupe and normalize (strip query params since Discord adds signatures)
      const uniqueUrls = Array.from(new Set(candidateVideoUrls.filter((u): u is string => Boolean(u)).map(normalizeVideoSrc)));
      if (uniqueUrls.length > 0) {
        // Query all video cache entries that start with any of our normalized URLs
        // Since source_url may have query params, we use LIKE prefix matching
        const likePatterns = uniqueUrls.map(u => u + '%');
        videoCacheRows = await sql`
          SELECT video_id, source_url, file_path, content_type, is_youtube, youtube_id 
          FROM video_cache 
          WHERE source_url LIKE ANY(${likePatterns})
        `;
      }
    }

    // Map video cache rows by source_url for quick lookup
    const videoCacheBySource = {};
    for (const r of videoCacheRows) {
      if (r?.source_url) {
        videoCacheBySource[r.source_url] = r;
        const normalized = normalizeVideoSrc(r.source_url);
        videoCacheBySource[normalized] = r;
      }
    }

    // Index related data by message_id for quick lookup
    const attachmentsByMessage = {};
    const embedsByMessage = {};
    const reactionsByMessage = {};
    const stickersByMessage = {};
    const componentsByMessage = {};
    const pollsByMessage = {};
    const referencedById = {};

    for (const att of attachments) {
      if (!attachmentsByMessage[att.message_id]) {
        attachmentsByMessage[att.message_id] = [];
      }
      
      // For videos use cached video if available; otherwise use direct URL
      const isVideo = att.content_type?.startsWith('video/');
      let attachmentUrl;
      if (isVideo) {
        // 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 = `${baseUrl}/api/discord/cached-video?id=${att.cached_video_id}&sig=${sig}`;
        } else {
          // Fallback to URL-based lookup in videoCacheBySource
          const srcRaw = att.url || att.proxy_url;
          const src = normalizeVideoSrc(srcRaw);
          const cached = videoCacheBySource[src];
          if (cached && cached.file_path) {
            const sig = signVideoId(cached.video_id);
            attachmentUrl = `${baseUrl}/api/discord/cached-video?id=${cached.video_id}&sig=${sig}`;
          } else {
            // Fallback: use direct URL (Discord CDN) when no cache entry
            attachmentUrl = srcRaw;
          }
        }
      } else {
        // For images prefer cached image or proxy
        attachmentUrl = getCachedOrProxyUrl(att.cached_image_id, att.url || att.proxy_url, baseUrl);
      }
      
      attachmentsByMessage[att.message_id].push({
        id: att.attachment_id.toString(),
        filename: att.filename,
        url: attachmentUrl,
        // Keep original/origin URL so the client can de-dupe previews when we return a cached URL
        originalUrl: att.url || att.proxy_url || null,
        content_type: att.content_type,
        size: att.size,
        width: att.width,
        height: att.height,
      });
    }

    for (const embed of embeds) {
      if (!embedsByMessage[embed.message_id]) {
        embedsByMessage[embed.message_id] = [];
      }
      embedsByMessage[embed.message_id].push({
        type: embed.embed_type || 'rich',
        title: embed.title,
        description: embed.description,
        url: embed.url,
        color: embed.color,
        timestamp: embed.embed_timestamp,
        footer: embed.footer_text ? {
          text: embed.footer_text,
          iconUrl: getCachedOrProxyUrl(embed.cached_footer_icon_id, embed.footer_icon_url, baseUrl),
        } : null,
        image: embed.image_url ? {
          url: getCachedOrProxyUrl(embed.cached_image_id, embed.image_url, baseUrl),
          width: embed.image_width,
          height: embed.image_height,
        } : null,
        thumbnail: embed.thumbnail_url ? {
          url: getCachedOrProxyUrl(embed.cached_thumbnail_id, embed.thumbnail_url, baseUrl),
          width: embed.thumbnail_width,
          height: embed.thumbnail_height,
        } : null,
        video: embed.video_url ? (() => {
          const src = normalizeVideoSrc(embed.video_url);
          const cached = videoCacheBySource[src];
          if (cached && cached.file_path) {
            const sig = signVideoId(cached.video_id);
            return { url: `${baseUrl}/api/discord/cached-video?id=${cached.video_id}&sig=${sig}`, width: embed.video_width, height: embed.video_height };
          }
          // fallback to original url
          return { url: embed.video_url, width: embed.video_width, height: embed.video_height };
        })() : null,
        provider: embed.provider_name ? {
          name: embed.provider_name,
          url: embed.provider_url,
        } : null,
        author: embed.author_name ? {
          name: embed.author_name,
          url: embed.author_url,
          iconUrl: getCachedOrProxyUrl(embed.cached_author_icon_id, embed.author_icon_url, baseUrl),
        } : null,
        fields: fieldsByEmbed[embed.embed_id] || [],
        rawData: embed.raw_data || null,
      });
    }

    for (const reaction of reactions) {
      if (!reactionsByMessage[reaction.message_id]) {
        reactionsByMessage[reaction.message_id] = [];
      }
      // Build emoji object with optional cached image URL for custom emojis
      let emojiObj;
      if (reaction.emoji_id) {
        // Custom emoji - use cached image if available, otherwise construct Discord CDN URL
        let emojiUrl;
        if (reaction.emoji_cached_image_id) {
          const sig = signImageId(reaction.emoji_cached_image_id);
          emojiUrl = `${baseUrl}/api/discord/cached-image?id=${reaction.emoji_cached_image_id}&sig=${sig}`;
        } else {
          const ext = reaction.emoji_animated ? 'gif' : 'png';
          emojiUrl = `https://cdn.discordapp.com/emojis/${reaction.emoji_id}.${ext}?size=32`;
        }
        emojiObj = { id: reaction.emoji_id.toString(), name: reaction.emoji_name, animated: reaction.emoji_animated, url: emojiUrl };
      } else {
        // Standard Unicode emoji
        emojiObj = { name: reaction.emoji_name };
      }
      reactionsByMessage[reaction.message_id].push({
        emoji: emojiObj,
        count: parseInt(reaction.count, 10),
      });
    }

    // Sticker format types: 1=PNG, 2=APNG, 3=Lottie, 4=GIF
    const stickerExtensions = { 1: 'png', 2: 'png', 3: 'png', 4: 'gif' };
    for (const sticker of stickers) {
      if (!stickersByMessage[sticker.message_id]) {
        stickersByMessage[sticker.message_id] = [];
      }
      const ext = stickerExtensions[sticker.format_type] || 'png';
      // Use cached sticker image if available, otherwise fall back to Discord CDN
      let stickerUrl: string | null = null;
      if (sticker.cached_image_id) {
        const sig = signImageId(sticker.cached_image_id);
        stickerUrl = `${baseUrl}/api/discord/cached-image?id=${sticker.cached_image_id}&sig=${sig}`;
      } else if (sticker.format_type !== 3) {
        // Only use CDN fallback for non-Lottie stickers (Lottie will use lottie_data)
        stickerUrl = `https://media.discordapp.net/stickers/${sticker.sticker_id}.${ext}?size=160`;
      }
      stickersByMessage[sticker.message_id].push({
        id: sticker.sticker_id.toString(),
        name: sticker.name,
        formatType: sticker.format_type,
        url: stickerUrl,
        lottieData: sticker.lottie_data || null,
      });
    }

    // Index message components
    for (const comp of messageComponents) {
      if (!componentsByMessage[comp.message_id]) componentsByMessage[comp.message_id] = [];
      componentsByMessage[comp.message_id].push({
        id: comp.component_id.toString(),
        type: comp.component_type,
        customId: comp.custom_id,
        label: comp.label,
        style: comp.style,
        url: comp.url,
        disabled: !!comp.disabled,
        placeholder: comp.placeholder,
        minValues: comp.min_values,
        maxValues: comp.max_values,
        rawData: comp.raw_data || null,
      });
    }

    // Index poll votes by message_id and answer_id
    const pollVotesByAnswer: Record<string, Array<{ id: string; username: string; displayName: string; avatar: string | null }>> = {};
    for (const vote of pollVotes) {
      const key = `${vote.message_id}-${vote.answer_id}`;
      if (!pollVotesByAnswer[key]) {
        pollVotesByAnswer[key] = [];
      }
      // Build avatar URL
      let avatarUrl: string | null = null;
      if (vote.avatar_url) {
        if (vote.avatar_url.startsWith('http')) {
          avatarUrl = vote.avatar_url;
        } else {
          avatarUrl = `https://cdn.discordapp.com/avatars/${vote.user_id}/${vote.avatar_url}.png?size=32`;
        }
      }
      pollVotesByAnswer[key].push({
        id: vote.user_id.toString(),
        username: vote.username,
        displayName: vote.display_name || vote.username,
        avatar: avatarUrl,
      });
    }

    // Index poll answers by message_id
    const pollAnswersByMessage = {};
    for (const answer of pollAnswers) {
      if (!pollAnswersByMessage[answer.message_id]) {
        pollAnswersByMessage[answer.message_id] = [];
      }
      const voteKey = `${answer.message_id}-${answer.answer_id}`;
      pollAnswersByMessage[answer.message_id].push({
        id: answer.answer_id,
        text: answer.answer_text,
        emoji: answer.answer_emoji_id 
          ? { id: answer.answer_emoji_id.toString(), name: answer.answer_emoji_name, animated: answer.answer_emoji_animated }
          : answer.answer_emoji_name ? { name: answer.answer_emoji_name } : null,
        voteCount: answer.vote_count || 0,
        voters: pollVotesByAnswer[voteKey] || [],
      });
    }

    // Index polls by message_id
    for (const poll of polls) {
      const answers = pollAnswersByMessage[poll.message_id] || [];
      pollsByMessage[poll.message_id] = {
        question: {
          text: poll.question_text,
          emoji: poll.question_emoji_id 
            ? { id: poll.question_emoji_id.toString(), name: poll.question_emoji_name, animated: poll.question_emoji_animated }
            : poll.question_emoji_name ? { name: poll.question_emoji_name } : null,
        },
        answers,
        allowMultiselect: poll.allow_multiselect || false,
        expiry: poll.expiry?.toISOString() || null,
        isFinalized: poll.is_finalized || false,
        totalVotes: poll.total_votes || 0,
      };
    }

    for (const ref of referencedMessages) {
      // Handle avatar URL for referenced message author
      const refAvatarSource = ref.author_guild_avatar || ref.author_avatar;
      let refAvatarUrl;
      if (refAvatarSource) {
        if (refAvatarSource.startsWith('http')) {
          refAvatarUrl = refAvatarSource;
        } else {
          refAvatarUrl = `https://cdn.discordapp.com/avatars/${ref.author_id}/${refAvatarSource}.png?size=32`;
        }
      }
      
      referencedById[ref.message_id] = {
        id: ref.message_id.toString(),
        content: ref.content,
        author: { 
          id: ref.author_id?.toString(),
          username: ref.author_username,
          displayName: ref.author_display_name,
          avatar: refAvatarUrl,
        },
      };
    }

    // Build the formatted response
    const formattedMessages = messages.map(msg => {
      // Check for cached avatar first
      const authorId = msg.author_id?.toString();
      const cachedInfo = userAvatarCache[authorId];
      
      let avatarUrl;
      if (cachedInfo?.cachedAvatarId) {
        // Use cached avatar from database
        const sig = signImageId(cachedInfo.cachedAvatarId);
        avatarUrl = `${baseUrl}/api/discord/cached-image?id=${cachedInfo.cachedAvatarId}&sig=${sig}`;
      } else {
        // Handle avatar URL - prefer guild avatar, then user avatar
        const avatarSource = msg.author_guild_avatar || msg.author_avatar;
        if (avatarSource) {
          // If it's already a full URL, use it directly
          if (avatarSource.startsWith('http')) {
            avatarUrl = avatarSource;
          } else {
            // Otherwise, construct the CDN URL from the hash
            // Guild avatars use a different path
            if (msg.author_guild_avatar && guildId) {
              avatarUrl = `https://cdn.discordapp.com/guilds/${guildId}/users/${msg.author_id}/avatars/${msg.author_guild_avatar}.png`;
            } else {
              avatarUrl = `https://cdn.discordapp.com/avatars/${msg.author_id}/${avatarSource}.png`;
            }
          }
        } else {
          avatarUrl = `https://cdn.discordapp.com/embed/avatars/${(parseInt(msg.author_discriminator || '0', 10) || parseInt(msg.author_id, 10)) % 5}.png`;
        }
      }

      // Convert color from decimal to hex if present
      const colorHex = msg.author_color ? `#${msg.author_color.toString(16).padStart(6, '0')}` : null;

      // Determine if this is a server-forwarded webhook message
      // Server-forwarded messages have a webhook_id AND reference_guild_id differs from current guild
      const isWebhook = !!msg.webhook_id;
      const isServerForwarded = isWebhook && msg.reference_guild_id && 
        msg.reference_guild_id.toString() !== msg.message_guild_id?.toString();

      return {
        id: msg.message_id.toString(),
        type: msg.message_type,
        content: msg.content,
        timestamp: msg.created_at?.toISOString(),
        editedTimestamp: msg.edited_at?.toISOString() || null,
        author: {
          id: msg.author_id?.toString(),
          username: msg.author_username || 'Unknown User',
          displayName: msg.author_display_name || msg.author_username || 'Unknown User',
          discriminator: msg.author_discriminator || '0000',
          avatar: avatarUrl,
          bot: msg.author_bot || false,
          isWebhook: isWebhook,
          isServerForwarded: isServerForwarded,
          color: colorHex,
        },
        attachments: attachmentsByMessage[msg.message_id] || [],
        embeds: embedsByMessage[msg.message_id] || [],
        components: componentsByMessage[msg.message_id] || [],
        rawData: msg.raw_data || null,
        stickers: stickersByMessage[msg.message_id] || [],
        reactions: reactionsByMessage[msg.message_id] || [],
        poll: pollsByMessage[msg.message_id] || null,
        referencedMessage: msg.reference_message_id 
          ? referencedById[msg.reference_message_id] || null 
          : null,
      };
    });

    return createResponse({ messages: formattedMessages, users: usersMap, emojiCache: emojiCacheMap });
  } catch (error) {
    console.error('Error fetching messages:', error);
    return createResponse({ error: 'Failed to fetch messages' }, 500);
  }
}
