/**
 * API endpoint to fetch Discord guild members with roles
 * Optionally filters by channel visibility using permission overwrites
 */
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';

// Discord permission flags
const VIEW_CHANNEL = 0x400n;  // 1024
const ADMINISTRATOR = 0x8n;   // 8

/**
 * Calculate which members can view a specific channel
 * Emulates Discord's permission calculation:
 * 1. Start with @everyone role permissions
 * 2. Apply role-based overwrites (allow/deny)
 * 3. Apply member-specific overwrites (allow/deny)
 * 4. Check if VIEW_CHANNEL permission is granted
 */
interface RoleOverwrite {
  allow: bigint;
  deny: bigint;
}

async function getChannelVisibleMembers(channelId: string, guildId: string): Promise<Set<string>> {
  // Get guild info including owner
  const guildInfo = await sql`
    SELECT owner_id FROM guilds WHERE guild_id = ${guildId}
  `;
  const ownerId = guildInfo[0]?.owner_id;

  // Get @everyone role (same ID as guild)
  const everyoneRole = await sql`
    SELECT role_id, permissions FROM roles WHERE role_id = ${guildId}
  `;
  const basePermissions = everyoneRole[0]?.permissions ? BigInt(everyoneRole[0].permissions) : 0n;

  // Get all channel permission overwrites
  const overwrites = await sql`
    SELECT target_id, target_type, allow_permissions, deny_permissions
    FROM channel_permission_overwrites
    WHERE channel_id = ${channelId}
  `;

  // Build overwrite lookups
  const roleOverwrites = new Map<string, RoleOverwrite>();
  const memberOverwrites = new Map<string, RoleOverwrite>();
  for (const ow of overwrites) {
    const targetId = ow.target_id.toString();
    const allow = BigInt(ow.allow_permissions);
    const deny = BigInt(ow.deny_permissions);
    if (ow.target_type === 'role') {
      roleOverwrites.set(targetId, { allow, deny });
    } else {
      memberOverwrites.set(targetId, { allow, deny });
    }
  }

  // Get all guild members with their roles
  const members = await sql`
    SELECT gm.user_id, gm.roles
    FROM guild_members gm
    WHERE gm.guild_id = ${guildId}
  `;

  // Get all role permissions for the guild
  const allRoles = await sql`
    SELECT role_id, permissions FROM roles WHERE guild_id = ${guildId}
  `;
  const rolePermissions = new Map<string, bigint>();
  for (const role of allRoles) {
    rolePermissions.set(role.role_id.toString(), BigInt(role.permissions || 0));
  }

  const visibleMemberIds = new Set<string>();

  for (const member of members) {
    const userId = member.user_id.toString();
    const memberRoles = (member.roles || []).map((r: bigint | string) => r.toString());

    // Owner always has access
    if (ownerId && member.user_id.toString() === ownerId.toString()) {
      visibleMemberIds.add(userId);
      continue;
    }

    // Check if any role has ADMINISTRATOR
    let isAdmin = false;
    for (const roleId of memberRoles) {
      const perms = rolePermissions.get(roleId) || 0n;
      if (perms & ADMINISTRATOR) {
        isAdmin = true;
        break;
      }
    }

    if (isAdmin) {
      visibleMemberIds.add(userId);
      continue;
    }

    // Calculate effective permissions
    let permissions = basePermissions;

    // Apply @everyone role overwrite first
    const everyoneOverwrite = roleOverwrites.get(guildId.toString());
    if (everyoneOverwrite) {
      permissions = (permissions & ~everyoneOverwrite.deny) | everyoneOverwrite.allow;
    }

    // Apply role overwrites (combined)
    let allowCombined = 0n;
    let denyCombined = 0n;
    for (const roleId of memberRoles) {
      const ow = roleOverwrites.get(roleId);
      if (ow) {
        allowCombined |= ow.allow;
        denyCombined |= ow.deny;
      }
    }
    permissions = (permissions & ~denyCombined) | allowCombined;

    // Apply member-specific overwrite (highest priority)
    const memberOverwrite = memberOverwrites.get(userId);
    if (memberOverwrite) {
      permissions = (permissions & ~memberOverwrite.deny) | memberOverwrite.allow;
    }

    // Check VIEW_CHANNEL permission
    if (permissions & VIEW_CHANNEL) {
      visibleMemberIds.add(userId);
    }
  }

  return visibleMemberIds;
}

export async function GET({ request }) {
  // Rate limit check
  const rateCheck = checkRateLimit(request, {
    limit: 80,
    windowMs: 1000,
    burstLimit: 320,
    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, 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 guildId = url.searchParams.get('guildId');
    const channelId = url.searchParams.get('channelId'); // Optional: filter by channel visibility

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

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

    // If channelId provided, get visible members for that channel
    let visibleMemberIds: Set<string> | null = null;
    if (channelId) {
      visibleMemberIds = await getChannelVisibleMembers(channelId, guildId);
    }

    // Fetch all members for the guild with their user info
    const members = await sql`
      SELECT 
        gm.user_id,
        gm.nickname,
        gm.roles,
        gm.guild_avatar_url,
        gm.cached_guild_avatar_id,
        u.username,
        u.display_name,
        u.global_name,
        u.avatar_url,
        u.cached_avatar_id,
        u.is_bot
      FROM guild_members gm
      JOIN users u ON gm.user_id = u.user_id
      WHERE gm.guild_id = ${guildId}
      ORDER BY u.username ASC
    `;

    // Fetch all roles for the guild
    const roles = await sql`
      SELECT 
        role_id,
        name,
        color,
        position,
        hoist
      FROM roles
      WHERE guild_id = ${guildId}
      ORDER BY position DESC
    `;

    // Create role lookup map
    interface RoleInfo {
      id: string;
      name: string;
      color: string | null;
      position: number;
      hoist: boolean;
    }

    const roleMap: Record<string, RoleInfo> = {};
    roles.forEach(role => {
      roleMap[role.role_id.toString()] = {
        id: role.role_id.toString(),
        name: role.name,
        color: role.color ? `#${Number(role.color).toString(16).padStart(6, '0')}` : null,
        position: Number(role.position ?? 0),
        hoist: Boolean(role.hoist), // "hoist" means the role is displayed separately in member list
      };
    });

    // Filter members by channel visibility if channelId was provided
    const filteredMembers = visibleMemberIds 
      ? members.filter(m => visibleMemberIds.has(m.user_id.toString()))
      : members;

    // Format members with their highest role for color
    interface FormattedMember {
      id: string;
      username: string;
      displayName: string;
      avatar: string | null;
      isBot: boolean;
      color: string | null;
      primaryRoleId: string | null;
      primaryRoleName: string | null;
      roles: string[];
    }

    const formattedMembers: FormattedMember[] = filteredMembers.map(member => {
      const memberRoles = (member.roles || []).map((r: bigint | string) => r.toString());
      
      // Find highest positioned role with color for display
      let displayRole: RoleInfo | null = null;
      let displayRoleColor: string | null = null;
      let highestPosition = -1;
      
      memberRoles.forEach(roleId => {
        const role = roleMap[roleId];
        if (role && role.position > highestPosition) {
          highestPosition = role.position;
          if (role.color && role.color !== '#000000') {
            displayRole = role;
            displayRoleColor = role.color;
          }
        }
      });

      // Find all hoisted roles for grouping
      const hoistedRoles = memberRoles
        .map(rid => roleMap[rid])
        .filter((r): r is RoleInfo => Boolean(r && r.hoist))
        .sort((a, b) => b.position - a.position);

      const primaryRole = hoistedRoles[0] || displayRole || null;
      
      // Prefer cached avatar, then guild avatar, then user avatar
      const cachedAvatarId = member.cached_guild_avatar_id || member.cached_avatar_id;
      let avatarUrl;
      if (cachedAvatarId) {
        const sig = signImageId(cachedAvatarId);
        avatarUrl = `/api/discord/cached-image?id=${cachedAvatarId}&sig=${sig}`;
      } else {
        avatarUrl = member.guild_avatar_url || member.avatar_url;
      }

      return {
        id: member.user_id.toString(),
        username: member.username,
        displayName: member.nickname || member.global_name || member.username,
        avatar: avatarUrl,
        isBot: member.is_bot || false,
        color: displayRoleColor,
        primaryRoleId: primaryRole?.id || null,
        primaryRoleName: primaryRole?.name || null,
        roles: memberRoles,
      };
    });

    // Group members by their primary (highest hoisted) role
    const membersByRole: Record<string, { role: RoleInfo; members: FormattedMember[] }> = {};
    const noRoleMembers: FormattedMember[] = [];

    formattedMembers.forEach(member => {
      if (member.primaryRoleId) {
        if (!membersByRole[member.primaryRoleId]) {
          membersByRole[member.primaryRoleId] = {
            role: roleMap[member.primaryRoleId],
            members: [],
          };
        }
        membersByRole[member.primaryRoleId].members.push(member);
      } else {
        noRoleMembers.push(member);
      }
    });

    // Sort groups by role position (highest first)
    const sortedGroups = Object.values(membersByRole)
      .sort((a, b) => b.role.position - a.role.position);

    // Add "Online" or default group for members without hoisted roles
    if (noRoleMembers.length > 0) {
      sortedGroups.push({
        role: { id: 'none', name: 'Members', color: null, position: -1, hoist: false },
        members: noRoleMembers,
      });
    }

    return createResponse({
      roles: Object.values(roleMap).sort((a, b) => b.position - a.position),
      groups: sortedGroups,
      totalMembers: formattedMembers.length,
    });
  } catch (error) {
    console.error('Error fetching members:', error);
    return createResponse({ error: 'Failed to fetch members' }, 500);
  }
}
