import { Router } from 'express';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { authenticateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import { createNotification } from '../config/notifications.js';
import { sendGuestInviteEmail } from '../config/mailer.js';
import { getAppName } from '../config/appName.js';
import {
  createMeeting,
  joinMeeting,
  endMeeting,
  getMeetingInfo,
  isMeetingRunning,
  getAnalyticsToken,
} from '../config/bbb.js';
import {
  isFederationEnabled,
  getFederationDomain,
  signPayload,
  discoverInstance,
} from '../config/federation.js';

// Avatar image filenames are produced by the upload endpoint as
// "<userId>_<timestamp>.<ext>". Reject anything that doesn't match this shape
// so a guest can't smuggle path traversal or arbitrary URL fragments through
// the unauthenticated guest-join endpoint.
const SAFE_AVATAR_FILENAME_RE = /^[0-9]+_[0-9]+\.(jpg|png|gif|webp)$/;
// Mirrors auth.js: only allow hex/hsl/named CSS colors.
const SAFE_AVATAR_COLOR_RE = /^(?:#[0-9a-fA-F]{3,8}|hsl\(\d{1,3},\s*\d{1,3}%,\s*\d{1,3}%\)|[a-zA-Z]{1,30})$/;

// L6: constant-time string comparison for access/moderator codes
function timingSafeEqual(a, b) {
  if (typeof a !== 'string' || typeof b !== 'string') return false;
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const presentationsDir = path.join(__dirname, '..', '..', 'uploads', 'presentations');
if (!fs.existsSync(presentationsDir)) fs.mkdirSync(presentationsDir, { recursive: true });

const PRESENTATION_TOKEN_SECRET = process.env.BBB_SECRET || crypto.randomBytes(32).toString('hex');
const PRESENTATION_TOKEN_TTL = 60 * 60 * 1000; // 1 hour

function signPresentationUrl(roomUid, filename) {
  const expires = Date.now() + PRESENTATION_TOKEN_TTL;
  const token = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
    .update(`${roomUid}/${filename}:${expires}`)
    .digest('hex');
  return { token, expires };
}

// M8: rate limit unauthenticated guest-join to prevent access_code brute-force
const guestJoinLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 15,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many join attempts. Please try again later.' },
});

// Rate limit email invitations: each request may carry up to 50 addresses, so
// without a cap any registered account could be abused as an SMTP spam relay.
const inviteEmailLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many email invitations. Please try again later.' },
});

const router = Router();

// Build avatar URL for a user (uploaded image or generated initials)
function getUserAvatarURL(req, user) {
  const baseUrl = getBaseUrl(req);
  if (user.avatar_image) {
    return `${baseUrl}/api/auth/avatar/${user.avatar_image}`;
  }
  const color = user.avatar_color ? `?color=${encodeURIComponent(user.avatar_color)}` : '';
  return `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(user.display_name || user.name)}${color}`;
}

// GET /api/rooms - List user's rooms (owned + shared)
router.get('/', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const ownRooms = await db.all(`
      SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name, 0 as shared
      FROM rooms r 
      JOIN users u ON r.user_id = u.id 
      WHERE r.user_id = ?
      ORDER BY r.created_at DESC
    `, [req.user.id]);

    const sharedRooms = await db.all(`
      SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name, 1 as shared
      FROM rooms r
      JOIN users u ON r.user_id = u.id
      JOIN room_shares rs ON rs.room_id = r.id
      WHERE rs.user_id = ?
      ORDER BY r.created_at DESC
    `, [req.user.id]);

    res.json({ rooms: [...ownRooms, ...sharedRooms] });
  } catch (err) {
    log.rooms.error(`List rooms error: ${err.message}`);
    res.status(500).json({ error: 'Rooms could not be loaded' });
  }
});

// GET /api/rooms/users/search - Search users for sharing (must be before /:uid routes)
router.get('/users/search', authenticateToken, async (req, res) => {
  try {
    const { q } = req.query;
    if (!q || q.length < 2) {
      return res.json({ users: [] });
    }
    const db = getDb();
    const searchTerm = `%${q}%`;
    const users = await db.all(`
      SELECT id, name, display_name, email, avatar_color, avatar_image
      FROM users
      WHERE (name LIKE ? OR display_name LIKE ? OR email LIKE ?) AND id != ?
      LIMIT 10
    `, [searchTerm, searchTerm, searchTerm, req.user.id]);
    res.json({ users });
  } catch (err) {
    log.rooms.error(`Search users error: ${err.message}`);
    res.status(500).json({ error: 'User search failed' });
  }
});

// GET /api/rooms/:uid - Get room details
router.get('/:uid', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get(`
      SELECT r.*, COALESCE(NULLIF(u.display_name,''), u.name) as owner_name 
      FROM rooms r 
      JOIN users u ON r.user_id = u.id 
      WHERE r.uid = ?
    `, [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // Check access: owner, admin, or shared
    if (room.user_id !== req.user.id && req.user.role !== 'admin') {
      const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
      if (!share) {
        return res.status(403).json({ error: 'No permission' });
      }
      room.shared = 1;
    }

    // Get shared users
    const sharedUsers = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM room_shares rs
      JOIN users u ON rs.user_id = u.id
      WHERE rs.room_id = ?
    `, [room.id]);

    res.json({ room, sharedUsers });
  } catch (err) {
    log.rooms.error(`Get room error: ${err.message}`);
    res.status(500).json({ error: 'Room could not be loaded' });
  }
});

// POST /api/rooms - Create room
router.post('/', authenticateToken, async (req, res) => {
  try {
    const {
      name,
      welcome_message,
      max_participants,
      access_code,
      mute_on_join,
      require_approval,
      anyone_can_start,
      all_join_moderator,
      record_meeting,
      guest_access,
      moderator_code,
      recording_transcript,
    } = req.body;

    if (!name || name.trim().length === 0) {
      return res.status(400).json({ error: 'Room name is required' });
    }
    if (name.trim().length < 2) {
      return res.status(400).json({ error: 'Room name must be at least 2 characters' });
    }

    // M7: field length limits
    if (name.trim().length > 100) {
      return res.status(400).json({ error: 'Room name must not exceed 100 characters' });
    }
    if (welcome_message && welcome_message.length > 2000) {
      return res.status(400).json({ error: 'Welcome message must not exceed 2000 characters' });
    }
    if (access_code && access_code.length > 50) {
      return res.status(400).json({ error: 'Access code must not exceed 50 characters' });
    }
    if (moderator_code && moderator_code.length > 50) {
      return res.status(400).json({ error: 'Moderator code must not exceed 50 characters' });
    }
    // S2: validate max_participants as non-negative integer
    if (max_participants !== undefined && max_participants !== null) {
      const mp = Number(max_participants);
      if (!Number.isInteger(mp) || mp < 0 || mp > 10000) {
        return res.status(400).json({ error: 'max_participants must be a non-negative integer (max 10000)' });
      }
    }

    const uid = crypto.randomBytes(8).toString('hex');
    const db = getDb();

    const result = await db.run(`
      INSERT INTO rooms (uid, name, user_id, welcome_message, max_participants, access_code, mute_on_join, require_approval, anyone_can_start, all_join_moderator, record_meeting, guest_access, moderator_code, recording_transcript)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
      uid,
      name.trim(),
      req.user.id,
      welcome_message || 'Willkommen im Meeting!',
      max_participants || 0,
      access_code || null,
      mute_on_join !== false ? 1 : 0,
      require_approval ? 1 : 0,
      anyone_can_start ? 1 : 0,
      all_join_moderator ? 1 : 0,
      record_meeting !== false ? 1 : 0,
      guest_access ? 1 : 0,
      moderator_code || null,
      // Transcription/AI summary requires recording to be enabled
      (record_meeting !== false && recording_transcript) ? 1 : 0,
    ]);

    const room = await db.get('SELECT * FROM rooms WHERE id = ?', [result.lastInsertRowid]);
    res.status(201).json({ room });
  } catch (err) {
    log.rooms.error(`Create room error: ${err.message}`);
    res.status(500).json({ error: 'Room could not be created' });
  }
});

// PUT /api/rooms/:uid - Update room
router.put('/:uid', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found or no permission' });
    }

    const {
      name,
      welcome_message,
      max_participants,
      access_code,
      mute_on_join,
      require_approval,
      anyone_can_start,
      all_join_moderator,
      record_meeting,
      guest_access,
      moderator_code,
      learning_analytics,
      analytics_visibility,
      recording_transcript,
    } = req.body;

    // M12: field length limits (same as create)
    if (name && name.trim().length < 2) {
      return res.status(400).json({ error: 'Room name must be at least 2 characters' });
    }
    if (name && name.trim().length > 100) {
      return res.status(400).json({ error: 'Room name must not exceed 100 characters' });
    }
    if (welcome_message && welcome_message.length > 2000) {
      return res.status(400).json({ error: 'Welcome message must not exceed 2000 characters' });
    }
    if (access_code && access_code.length > 50) {
      return res.status(400).json({ error: 'Access code must not exceed 50 characters' });
    }
    if (moderator_code && moderator_code.length > 50) {
      return res.status(400).json({ error: 'Moderator code must not exceed 50 characters' });
    }
    // S2: validate max_participants on update
    if (max_participants !== undefined && max_participants !== null) {
      const mp = Number(max_participants);
      if (!Number.isInteger(mp) || mp < 0 || mp > 10000) {
        return res.status(400).json({ error: 'max_participants must be a non-negative integer (max 10000)' });
      }
    }

    // Transcription/AI summary requires recording — force it off when recording ends up disabled
    const effectiveRecordMeeting = record_meeting !== undefined ? !!record_meeting : !!room.record_meeting;

    await db.run(`
      UPDATE rooms SET
        name = COALESCE(?, name),
        welcome_message = COALESCE(?, welcome_message),
        max_participants = COALESCE(?, max_participants),
        access_code = ?,
        mute_on_join = COALESCE(?, mute_on_join),
        require_approval = COALESCE(?, require_approval),
        anyone_can_start = COALESCE(?, anyone_can_start),
        all_join_moderator = COALESCE(?, all_join_moderator),
        record_meeting = COALESCE(?, record_meeting),
        guest_access = COALESCE(?, guest_access),
        moderator_code = ?,
        learning_analytics = COALESCE(?, learning_analytics),
        analytics_visibility = COALESCE(?, analytics_visibility),
        recording_transcript = COALESCE(?, recording_transcript),
        updated_at = CURRENT_TIMESTAMP
      WHERE uid = ?
    `, [
      name,
      welcome_message,
      max_participants,
      access_code ?? room.access_code,
      mute_on_join !== undefined ? (mute_on_join ? 1 : 0) : null,
      require_approval !== undefined ? (require_approval ? 1 : 0) : null,
      anyone_can_start !== undefined ? (anyone_can_start ? 1 : 0) : null,
      all_join_moderator !== undefined ? (all_join_moderator ? 1 : 0) : null,
      record_meeting !== undefined ? (record_meeting ? 1 : 0) : null,
      guest_access !== undefined ? (guest_access ? 1 : 0) : null,
      moderator_code !== undefined ? (moderator_code || null) : room.moderator_code,
      learning_analytics !== undefined ? (learning_analytics ? 1 : 0) : null,
      analytics_visibility && ['owner', 'shared'].includes(analytics_visibility) ? analytics_visibility : null,
      !effectiveRecordMeeting ? 0 : (recording_transcript !== undefined ? (recording_transcript ? 1 : 0) : null),
      req.params.uid,
    ]);

    const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
    res.json({ room: updated });
  } catch (err) {
    log.rooms.error(`Update room error: ${err.message}`);
    res.status(500).json({ error: 'Room could not be updated' });
  }
});

// DELETE /api/rooms/:uid - Delete room
router.delete('/:uid', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    if (room.user_id !== req.user.id && req.user.role !== 'admin') {
      return res.status(403).json({ error: 'No permission' });
    }

    // Notify federated instances about deletion (fire-and-forget)
    if (isFederationEnabled()) {
      try {
        const outbound = await db.all(
          'SELECT remote_domain FROM federation_outbound_invites WHERE room_uid = ?',
          [room.uid]
        );
        for (const { remote_domain } of outbound) {
          const payload = {
            room_uid: room.uid,
            timestamp: new Date().toISOString(),
          };
          const signature = signPayload(payload);
          discoverInstance(remote_domain).then(({ baseUrl: remoteApi }) => {
            fetch(`${remoteApi}/room-deleted`, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'X-Federation-Signature': signature,
                'X-Federation-Origin': getFederationDomain(),
              },
              body: JSON.stringify(payload),
              signal: AbortSignal.timeout(10_000),
            }).catch(err => log.federation.warn(`Delete notify to ${remote_domain} failed: ${err.message}`));
          }).catch(err => log.federation.warn(`Discovery for ${remote_domain} failed: ${err.message}`));
        }
        // Clean up outbound records
        await db.run('DELETE FROM federation_outbound_invites WHERE room_uid = ?', [room.uid]);
      } catch (fedErr) {
        log.federation.warn(`Delete notification error (non-fatal): ${fedErr.message}`);
      }
    }

    await db.run('DELETE FROM rooms WHERE uid = ?', [req.params.uid]);
    res.json({ message: 'Room deleted successfully' });
  } catch (err) {
    log.rooms.error(`Delete room error: ${err.message}`);
    res.status(500).json({ error: 'Room could not be deleted' });
  }
});
// GET /api/rooms/:uid/shares - Get shared users for a room
router.get('/:uid/shares', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
    if (!room) {
      return res.status(404).json({ error: 'Room not found or no permission' });
    }
    const shares = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM room_shares rs
      JOIN users u ON rs.user_id = u.id
      WHERE rs.room_id = ?
    `, [room.id]);
    res.json({ shares });
  } catch (err) {
    log.rooms.error(`Get shares error: ${err.message}`);
    res.status(500).json({ error: 'Error loading shares' });
  }
});

// POST /api/rooms/:uid/shares - Share room with a user
router.post('/:uid/shares', authenticateToken, async (req, res) => {
  try {
    const { user_id } = req.body;
    if (!user_id) {
      return res.status(400).json({ error: 'User ID is required' });
    }
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
    if (!room) {
      return res.status(404).json({ error: 'Room not found or no permission' });
    }
    if (user_id === req.user.id) {
      return res.status(400).json({ error: 'You cannot share the room with yourself' });
    }
    const targetUser = await db.get('SELECT id FROM users WHERE id = ?', [user_id]);
    if (!targetUser) {
      return res.status(404).json({ error: 'User not found' });
    }
    // Check if already shared
    const existing = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, user_id]);
    if (existing) {
      return res.status(400).json({ error: 'Room is already shared with this user' });
    }
    await db.run('INSERT INTO room_shares (room_id, user_id) VALUES (?, ?)', [room.id, user_id]);
    const shares = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM room_shares rs
      JOIN users u ON rs.user_id = u.id
      WHERE rs.room_id = ?
    `, [room.id]);
    // Notify the user who was given access
    const sharerName = req.user.display_name || req.user.name;
    await createNotification(
      user_id,
      'room_share_added',
      room.name,
      sharerName,
      `/rooms/${room.uid}`,
    );
    res.json({ shares });
  } catch (err) {
    log.rooms.error(`Share room error: ${err.message}`);
    res.status(500).json({ error: 'Error sharing room' });
  }
});

// DELETE /api/rooms/:uid/shares/:userId - Remove share
router.delete('/:uid/shares/:userId', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
    if (!room) {
      return res.status(404).json({ error: 'Room not found or no permission' });
    }
    const removedUserId = parseInt(req.params.userId);
    await db.run('DELETE FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, removedUserId]);
    const shares = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM room_shares rs
      JOIN users u ON rs.user_id = u.id
      WHERE rs.room_id = ?
    `, [room.id]);
    // Notify the user whose access was removed
    await createNotification(
      removedUserId,
      'room_share_removed',
      room.name,
      null,
      '/dashboard',
    );
    res.json({ shares });
  } catch (err) {
    log.rooms.error(`Remove share error: ${err.message}`);
    res.status(500).json({ error: 'Error removing share' });
  }
});

// POST /api/rooms/:uid/start - Start meeting
router.post('/:uid/start', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // Check access: owner, admin, shared, or anyone_can_start
    const isOwner = room.user_id === req.user.id;
    const isAdmin = req.user.role === 'admin';
    if (!isOwner && !isAdmin && !room.anyone_can_start) {
      const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
      if (!share) {
        return res.status(403).json({ error: 'No permission' });
      }
    }

    const baseUrl = getBaseUrl(req);
    const loginURL = `${baseUrl}/join/${room.uid}`;
    let presentationUrl = null;
    if (room.presentation_file) {
      const { token, expires } = signPresentationUrl(room.uid, room.presentation_file);
      presentationUrl = `${baseUrl}/api/rooms/presentations/${token}/${expires}/${room.uid}/${encodeURIComponent(room.presentation_file)}`;
    }
    const analyticsCallbackURL = room.learning_analytics
      ? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
      : null;
    // Localise BBB's default welcome message in the room owner's language
    const owner = await db.get('SELECT language FROM users WHERE id = ?', [room.user_id]);
    await createMeeting(room, baseUrl, loginURL, presentationUrl, analyticsCallbackURL, owner?.language || 'en');
    const avatarURL = getUserAvatarURL(req, req.user);
    const displayName = req.user.display_name || req.user.name;
    const joinUrl = await joinMeeting(room.uid, displayName, true, avatarURL, !!req.body.dark_mode);
    res.json({ joinUrl });
  } catch (err) {
    log.rooms.error(`Start meeting error: ${err.message}`);
    res.status(500).json({ error: 'Meeting could not be started' });
  }
});

// POST /api/rooms/:uid/join - Join meeting
router.post('/:uid/join', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // Check access code if set
    if (room.access_code && !timingSafeEqual(req.body.access_code || '', room.access_code)) {
      return res.status(403).json({ error: 'Wrong access code' });
    }

    // Check if meeting is running
    const running = await isMeetingRunning(room.uid);
    if (!running) {
      return res.status(400).json({ error: 'Meeting is not running. Please wait for the moderator to start the meeting.' });
    }

    // Owner and shared users join as moderator
    const isOwner = room.user_id === req.user.id;
    const isShared = !isOwner && await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
    const isModerator = isOwner || !!isShared || room.all_join_moderator;
    const avatarURL = getUserAvatarURL(req, req.user);
    const joinUrl = await joinMeeting(room.uid, req.user.display_name || req.user.name, isModerator, avatarURL, !!req.body.dark_mode);
    res.json({ joinUrl });
  } catch (err) {
    log.rooms.error(`Join meeting error: ${err.message}`);
    res.status(500).json({ error: 'Could not join meeting' });
  }
});

// POST /api/rooms/:uid/end - End meeting
router.post('/:uid/end', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // Check access: owner, admin, or shared user
    const isOwner = room.user_id === req.user.id;
    const isAdmin = req.user.role === 'admin';
    if (!isOwner && !isAdmin) {
      const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
      if (!share) {
        return res.status(403).json({ error: 'No permission' });
      }
    }

    await endMeeting(room.uid);
    res.json({ message: 'Meeting ended' });
  } catch (err) {
    log.rooms.error(`End meeting error: ${err.message}`);
    res.status(500).json({ error: 'Meeting could not be ended' });
  }
});

// GET /api/rooms/:uid/public - Get public room info (no auth needed)
router.get('/:uid/public', async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get(`
      SELECT r.uid, r.name, r.welcome_message, r.access_code, r.record_meeting, r.max_participants, r.anyone_can_start,
        COALESCE(NULLIF(u.display_name,''), u.name) as owner_name
      FROM rooms r
      JOIN users u ON r.user_id = u.id
      WHERE r.uid = ?
    `, [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    const running = await isMeetingRunning(room.uid);

    res.json({
      room: {
        uid: room.uid,
        name: room.name,
        owner_name: room.owner_name,
        welcome_message: room.welcome_message,
        has_access_code: !!room.access_code,
        allow_recording: !!room.record_meeting,
        max_participants: room.max_participants ?? 0,
        anyone_can_start: !!room.anyone_can_start,
      },
      running,
    });
  } catch (err) {
    log.rooms.error(`Public room info error: ${err.message}`);
    res.status(500).json({ error: 'Room info could not be loaded' });
  }
});

// POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed)
router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
  try {
    const { name, access_code, moderator_code, avatar_image, avatar_color, dark_mode } = req.body;

    if (!name || name.trim().length === 0) {
      return res.status(400).json({ error: 'Name is required' });
    }

    // L1: limit guest name length
    if (name.trim().length > 100) {
      return res.status(400).json({ error: 'Name must not exceed 100 characters' });
    }

    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // Check access code if set
    if (room.access_code && !timingSafeEqual(access_code || '', room.access_code)) {
      return res.status(403).json({ error: 'Wrong access code' });
    }

    // Check if meeting is running (or if anyone_can_start is enabled)
    const running = await isMeetingRunning(room.uid);
    if (!running && !room.anyone_can_start) {
      return res.status(400).json({ error: 'Meeting is not running. Please wait for the moderator to start the meeting.' });
    }

    // If meeting not running but anyone_can_start, create it
    if (!running && room.anyone_can_start) {
      const baseUrl = getBaseUrl(req);
      const loginURL = `${baseUrl}/join/${room.uid}`;
      const analyticsCallbackURL = room.learning_analytics
        ? `${baseUrl}/api/analytics/callback/${room.uid}?token=${getAnalyticsToken(room.uid)}`
        : null;
      // Localise BBB's default welcome message in the room owner's language
      const owner = await db.get('SELECT language FROM users WHERE id = ?', [room.user_id]);
      await createMeeting(room, baseUrl, loginURL, null, analyticsCallbackURL, owner?.language || 'en');
    }

    // Check moderator code
    let isModerator = !!room.all_join_moderator;
    if (!isModerator && moderator_code && room.moderator_code && timingSafeEqual(moderator_code, room.moderator_code)) {
      isModerator = true;
    }

    const baseUrl = getBaseUrl(req);
    // Validate client-supplied avatar fields before embedding them in a URL
    // that BBB will fetch — guest-join is unauthenticated.
    const safeAvatarImage = (typeof avatar_image === 'string' && SAFE_AVATAR_FILENAME_RE.test(avatar_image))
      ? avatar_image : null;
    const safeAvatarColor = (typeof avatar_color === 'string' && SAFE_AVATAR_COLOR_RE.test(avatar_color))
      ? avatar_color : null;
    let guestAvatarURL;
    if (safeAvatarImage) {
      guestAvatarURL = `${baseUrl}/api/auth/avatar/${encodeURIComponent(safeAvatarImage)}`;
    } else if (safeAvatarColor) {
      guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}?color=${encodeURIComponent(safeAvatarColor)}`;
    } else {
      guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`;
    }
    const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL, !!dark_mode);
    res.json({ joinUrl });
  } catch (err) {
    log.rooms.error(`Guest join error: ${err.message}`);
    res.status(500).json({ error: 'Guest join failed' });
  }
});

// GET /api/rooms/:uid/status - Check if meeting is running (public, no guard needed)
router.get('/:uid/status', async (req, res) => {
  try {
    const running = await isMeetingRunning(req.params.uid);
    let info = null;
    if (running) {
      try {
        info = await getMeetingInfo(req.params.uid);
      } catch (e) {
        // Meeting info might fail
      }
    }
    res.json({
      running,
      participantCount: info?.participantCount || 0,
      moderatorCount: info?.moderatorCount || 0,
    });
  } catch (err) {
    res.json({ running: false, participantCount: 0, moderatorCount: 0 });
  }
});

// GET /api/rooms/presentations/:token/:expires/:roomUid/:filename - Serve presentation file (token-protected for BBB)
// Token and expires are path segments so the URL ends with the original filename,
// allowing BBB to detect the file type from the extension.
router.get('/presentations/:token/:expires/:roomUid/:filename', (req, res) => {
  const { token, expires, roomUid, filename } = req.params;

  if (!token || !expires) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const expiresNum = Number(expires);
  if (isNaN(expiresNum) || Date.now() > expiresNum) {
    return res.status(403).json({ error: 'Token expired' });
  }

  const expected = crypto.createHmac('sha256', PRESENTATION_TOKEN_SECRET)
    .update(`${roomUid}/${filename}:${expires}`)
    .digest('hex');

  // timingSafeEqual throws on length mismatch, so compare lengths first
  const tokenBuf = Buffer.from(token);
  const expectedBuf = Buffer.from(expected);
  if (tokenBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(tokenBuf, expectedBuf)) {
    return res.status(403).json({ error: 'Invalid token' });
  }

  // S8: prevent path traversal
  const roomDir = path.resolve(presentationsDir, roomUid);
  const filepath = path.resolve(roomDir, filename);
  if (!filepath.startsWith(presentationsDir + path.sep) || !filepath.startsWith(roomDir + path.sep)) {
    return res.status(400).json({ error: 'Invalid filename' });
  }

  if (!fs.existsSync(filepath)) {
    return res.status(404).json({ error: 'File not found' });
  }

  res.sendFile(filepath);
});

// POST /api/rooms/:uid/presentation - Upload a presentation file for the room
router.post('/:uid/presentation', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
    if (!room) return res.status(404).json({ error: 'Room not found or no permission' });

    // M16: stream-level size limit - abort as soon as 50 MB is exceeded
    const MAX_PRESENTATION_SIZE = 50 * 1024 * 1024;
    const buffer = await new Promise((resolve, reject) => {
      const chunks = [];
      let totalSize = 0;
      req.on('data', chunk => {
        totalSize += chunk.length;
        if (totalSize > MAX_PRESENTATION_SIZE) {
          req.destroy();
          return reject(new Error('LIMIT_EXCEEDED'));
        }
        chunks.push(chunk);
      });
      req.on('end', () => resolve(Buffer.concat(chunks)));
      req.on('error', reject);
    }).catch(err => {
      if (err.message === 'LIMIT_EXCEEDED') return null;
      throw err;
    });

    if (!buffer) {
      return res.status(400).json({ error: 'File must not exceed 50MB' });
    }

    const contentType = req.headers['content-type'] || '';
    const extMap = {
      'application/pdf': 'pdf',
      'application/vnd.ms-powerpoint': 'ppt',
      'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
      'application/vnd.oasis.opendocument.presentation': 'odp',
      'application/msword': 'doc',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
    };
    const ext = extMap[contentType];
    if (!ext) return res.status(400).json({ error: 'Unsupported file type. Allowed: PDF, PPT, PPTX, ODP, DOC, DOCX' });

    // Validate magic bytes to prevent Content-Type spoofing
    const magic = buffer.slice(0, 8);
    const isPDF = magic[0] === 0x25 && magic[1] === 0x50 && magic[2] === 0x44 && magic[3] === 0x46; // %PDF
    const isZip = magic[0] === 0x50 && magic[1] === 0x4B && magic[2] === 0x03 && magic[3] === 0x04; // PK (PPTX, DOCX, ODP, etc.)
    const isOle = magic[0] === 0xD0 && magic[1] === 0xCF && magic[2] === 0x11 && magic[3] === 0xE0; // OLE2 (PPT, DOC)
    if (ext === 'pdf' && !isPDF) return res.status(400).json({ error: 'File content does not match PDF format' });
    if (['pptx', 'docx', 'odp'].includes(ext) && !isZip) return res.status(400).json({ error: 'File content does not match expected archive format' });
    if (['ppt', 'doc'].includes(ext) && !isOle) return res.status(400).json({ error: 'File content does not match expected document format' });

    // Preserve original filename (sent as X-Filename header)
    const rawName = req.headers['x-filename'];
    const filename = rawName
      ? decodeURIComponent(rawName).replace(/[^a-zA-Z0-9._\- ]/g, '_').slice(0, 200)
      : `presentation.${ext}`;

    // Each room gets its own folder: uploads/presentations/{roomUID}/
    const roomDir = path.join(presentationsDir, room.uid);
    if (!fs.existsSync(roomDir)) fs.mkdirSync(roomDir, { recursive: true });
    const filepath = path.join(roomDir, filename);

    // S8: defense-in-depth path traversal check
    if (!path.resolve(filepath).startsWith(roomDir + path.sep)) {
      return res.status(400).json({ error: 'Invalid filename' });
    }

    // Remove old presentation file if exists
    if (room.presentation_file) {
      const oldPath = path.resolve(roomDir, room.presentation_file);
      if (oldPath.startsWith(roomDir + path.sep) && fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
    }

    fs.writeFileSync(filepath, buffer);
    await db.run('UPDATE rooms SET presentation_file = ?, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [filename, req.params.uid]);
    const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
    res.json({ room: updated });
  } catch (err) {
    log.rooms.error(`Presentation upload error: ${err.message}`);
    res.status(500).json({ error: 'Presentation could not be uploaded' });
  }
});

// DELETE /api/rooms/:uid/presentation - Remove presentation file
router.delete('/:uid/presentation', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ? AND user_id = ?', [req.params.uid, req.user.id]);
    if (!room) return res.status(404).json({ error: 'Room not found or no permission' });

    if (room.presentation_file) {
      // S8: defense-in-depth path traversal check
      const roomDir = path.join(presentationsDir, room.uid);
      const filepath = path.resolve(roomDir, room.presentation_file);
      if (filepath.startsWith(roomDir + path.sep) && fs.existsSync(filepath)) fs.unlinkSync(filepath);
      // Remove empty room folder
      if (fs.existsSync(roomDir) && fs.readdirSync(roomDir).length === 0) fs.rmdirSync(roomDir);
    }

    await db.run('UPDATE rooms SET presentation_file = NULL, updated_at = CURRENT_TIMESTAMP WHERE uid = ?', [req.params.uid]);
    const updated = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);
    res.json({ room: updated });
  } catch (err) {
    log.rooms.error(`Presentation delete error: ${err.message}`);
    res.status(500).json({ error: 'Presentation could not be removed' });
  }
});

// ── POST /api/rooms/invite-email — Send email invitation to guest(s) ────────
router.post('/invite-email', authenticateToken, inviteEmailLimiter, async (req, res) => {
  try {
    const { room_uid, emails, message } = req.body;
    if (!room_uid || !emails || !emails.length) {
      return res.status(400).json({ error: 'room_uid and emails are required' });
    }

    if (emails.length > 50) {
      return res.status(400).json({ error: 'Maximum 50 email addresses allowed' });
    }

    // Validate all emails
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    for (const email of emails) {
      if (!emailRegex.test(email) || email.length > 254) {
        return res.status(400).json({ error: `Invalid email address: ${email}` });
      }
    }

    if (message && message.length > 2000) {
      return res.status(400).json({ error: 'Message must not exceed 2000 characters' });
    }

    const db = getDb();

    // Verify room exists and user has access
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [room_uid]);
    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    const isOwner = room.user_id === req.user.id;
    if (!isOwner) {
      const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
      if (!share) {
        return res.status(403).json({ error: 'No permission to invite from this room' });
      }
    }

    // Build guest join URL
    const baseUrl = getBaseUrl(req);
    const joinUrl = room.access_code
      ? `${baseUrl}/join/${room.uid}?ac=${encodeURIComponent(room.access_code)}`
      : `${baseUrl}/join/${room.uid}`;

    const appName = await getAppName();
    const fromUser = req.user.display_name || req.user.name;
    const lang = req.user.language || 'en';

    // Send emails (in parallel but collect errors)
    const results = await Promise.allSettled(
      emails.map(email =>
        sendGuestInviteEmail(email, fromUser, room.name, message || null, joinUrl, appName, lang)
      )
    );

    const failed = results.filter(r => r.status === 'rejected');
    if (failed.length === emails.length) {
      return res.status(500).json({ error: 'Failed to send all email invitations' });
    }
    if (failed.length > 0) {
      log.rooms.warn(`${failed.length}/${emails.length} email invitations failed`);
    }

    res.json({ success: true, sent: emails.length - failed.length, failed: failed.length });
  } catch (err) {
    log.rooms.error('Email invite error:', err);
    res.status(500).json({ error: err.message || 'Failed to send email invitations' });
  }
});

export default router;
