import { Router } from 'express';
import crypto from 'crypto';
import { getDb } from '../config/database.js';
import { authenticateToken, getBaseUrl } from '../middleware/auth.js';
import { log } from '../config/logger.js';
import { sendCalendarInviteEmail, sendCalendarEventEmail } from '../config/mailer.js';
import { getAppName } from '../config/appName.js';
import {
  isFederationEnabled,
  getFederationDomain,
  signPayload,
  verifyPayload,
  discoverInstance,
  parseAddress,
} from '../config/federation.js';
import { rateLimit } from 'express-rate-limit';

const router = Router();

// Allowlist for CSS color values - only permits hsl(), hex (#rgb/#rrggbb) and plain names
const SAFE_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})$/;

// Allowed reminder intervals in minutes
const VALID_REMINDERS = new Set([5, 15, 30, 60, 120, 1440]);

// 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 calendarEmailInviteLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many email invitations. Please try again later.' },
});

// Rate limit for federation calendar receive
const calendarFederationLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests. Please try again later.' },
});

// ── GET /api/calendar/events — List events for the current user ─────────────
router.get('/events', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const { from, to } = req.query;

    let sql = `
      SELECT ce.*, COALESCE(NULLIF(u.display_name,''), u.name) as organizer_name
      FROM calendar_events ce
      JOIN users u ON ce.user_id = u.id
      WHERE (ce.user_id = ? OR ce.id IN (
        SELECT event_id FROM calendar_event_shares WHERE user_id = ?
      ))
    `;
    const params = [req.user.id, req.user.id];

    if (from) {
      sql += ' AND ce.end_time >= ?';
      params.push(from);
    }
    if (to) {
      sql += ' AND ce.start_time <= ?';
      params.push(to);
    }

    sql += ' ORDER BY ce.start_time ASC';
    const events = await db.all(sql, params);

    // Mark shared events
    for (const ev of events) {
      ev.is_owner = ev.user_id === req.user.id;
    }

    res.json({ events });
  } catch (err) {
    log.server.error(`Calendar list error: ${err.message}`);
    res.status(500).json({ error: 'Events could not be loaded' });
  }
});

// ── GET /api/calendar/events/:id — Get single event ─────────────────────────
router.get('/events/:id', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const event = await db.get(`
      SELECT ce.*, COALESCE(NULLIF(u.display_name,''), u.name) as organizer_name
      FROM calendar_events ce
      JOIN users u ON ce.user_id = u.id
      WHERE ce.id = ?
    `, [req.params.id]);

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

    // Check access
    if (event.user_id !== req.user.id) {
      const share = await db.get('SELECT id FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [event.id, req.user.id]);
      if (!share) return res.status(403).json({ error: 'No permission' });
    }

    // 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 calendar_event_shares ces
      JOIN users u ON ces.user_id = u.id
      WHERE ces.event_id = ?
    `, [event.id]);

    event.is_owner = event.user_id === req.user.id;

    let pendingInvitations = [];
    if (event.user_id === req.user.id) {
      pendingInvitations = await db.all(`
        SELECT cli.id, cli.to_user_id as user_id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
        FROM calendar_local_invitations cli
        JOIN users u ON cli.to_user_id = u.id
        WHERE cli.event_id = ? AND cli.status = 'pending'
      `, [event.id]);
    }

    res.json({ event, sharedUsers, pendingInvitations });
  } catch (err) {
    log.server.error(`Calendar get event error: ${err.message}`);
    res.status(500).json({ error: 'Event could not be loaded' });
  }
});

// ── POST /api/calendar/events — Create event ────────────────────────────────
router.post('/events', authenticateToken, async (req, res) => {
  try {
    const { title, description, start_time, end_time, room_uid, color, reminder_minutes } = req.body;

    if (!title || !title.trim()) return res.status(400).json({ error: 'Title is required' });
    if (!start_time || !end_time) return res.status(400).json({ error: 'Start and end time are required' });
    if (title.length > 200) return res.status(400).json({ error: 'Title must not exceed 200 characters' });
    if (description && description.length > 5000) return res.status(400).json({ error: 'Description must not exceed 5000 characters' });

    // Validate color format
    if (color && !SAFE_COLOR_RE.test(color)) {
      return res.status(400).json({ error: 'Invalid color format' });
    }

    const startDate = new Date(start_time);
    const endDate = new Date(end_time);
    if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
      return res.status(400).json({ error: 'Invalid date format' });
    }
    if (endDate <= startDate) {
      return res.status(400).json({ error: 'End time must be after start time' });
    }

    // Verify room exists if specified
    const db = getDb();
    if (room_uid) {
      const room = await db.get('SELECT id FROM rooms WHERE uid = ?', [room_uid]);
      if (!room) return res.status(400).json({ error: 'Linked room not found' });
    }

    const uid = crypto.randomBytes(12).toString('hex');
    const validReminder = (reminder_minutes != null && VALID_REMINDERS.has(Number(reminder_minutes)))
      ? Number(reminder_minutes) : null;

    const result = await db.run(`
      INSERT INTO calendar_events (uid, title, description, start_time, end_time, room_uid, user_id, color, reminder_minutes)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
      uid,
      title.trim(),
      description || null,
      startDate.toISOString(),
      endDate.toISOString(),
      room_uid || null,
      req.user.id,
      color || '#6366f1',
      validReminder,
    ]);

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

// ── PUT /api/calendar/events/:id — Update event ─────────────────────────────
router.put('/events/:id', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });

    const { title, description, start_time, end_time, room_uid, color, reminder_minutes } = req.body;

    if (title && title.length > 200) return res.status(400).json({ error: 'Title must not exceed 200 characters' });
    if (description && description.length > 5000) return res.status(400).json({ error: 'Description must not exceed 5000 characters' });

    // Validate color format
    if (color && !SAFE_COLOR_RE.test(color)) {
      return res.status(400).json({ error: 'Invalid color format' });
    }

    if (start_time && end_time) {
      const s = new Date(start_time);
      const e = new Date(end_time);
      if (isNaN(s.getTime()) || isNaN(e.getTime())) return res.status(400).json({ error: 'Invalid date format' });
      if (e <= s) return res.status(400).json({ error: 'End time must be after start time' });
    }

    if (room_uid) {
      const room = await db.get('SELECT id FROM rooms WHERE uid = ?', [room_uid]);
      if (!room) return res.status(400).json({ error: 'Linked room not found' });
    }

    const validReminder = (reminder_minutes !== undefined)
      ? ((reminder_minutes != null && VALID_REMINDERS.has(Number(reminder_minutes))) ? Number(reminder_minutes) : null)
      : undefined;
    // Reset reminder_sent_at when start_time or reminder_minutes changes so the job re-fires
    const resetReminder = (start_time !== undefined && start_time !== event.start_time)
      || (reminder_minutes !== undefined && validReminder !== event.reminder_minutes);

    await db.run(`
      UPDATE calendar_events SET
        title = COALESCE(?, title),
        description = ?,
        start_time = COALESCE(?, start_time),
        end_time = COALESCE(?, end_time),
        room_uid = ?,
        color = COALESCE(?, color),
        reminder_minutes = ?,
        reminder_sent_at = ${resetReminder ? 'NULL' : 'reminder_sent_at'},
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `, [
      title || null,
      description !== undefined ? description : event.description,
      start_time || null,
      end_time || null,
      room_uid !== undefined ? (room_uid || null) : event.room_uid,
      color || null,
      // Not in payload → keep existing; present (incl. null) → set/clear directly.
      // COALESCE can't be used here: it would treat an explicit null as "keep".
      validReminder !== undefined ? validReminder : event.reminder_minutes,
      req.params.id,
    ]);

    const updated = await db.get('SELECT * FROM calendar_events WHERE id = ?', [req.params.id]);
    res.json({ event: updated });
  } catch (err) {
    log.server.error(`Calendar update error: ${err.message}`);
    res.status(500).json({ error: 'Event could not be updated' });
  }
});

// ── DELETE /api/calendar/events/:id — Delete event ──────────────────────────
router.delete('/events/:id', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });

    // Propagate deletion to all remote instances that received this event
    if (isFederationEnabled()) {
      try {
        const outbound = await db.all(
          'SELECT remote_domain FROM calendar_event_outbound WHERE event_uid = ?',
          [event.uid]
        );
        for (const { remote_domain } of outbound) {
          try {
            const payload = {
              event_uid: event.uid,
              from_user: `@${req.user.name}@${getFederationDomain()}`,
              timestamp: new Date().toISOString(),
            };
            const signature = signPayload(payload);
            const { baseUrl: remoteApi } = await discoverInstance(remote_domain);
            await fetch(`${remoteApi}/calendar-event-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 (remoteErr) {
            log.server.warn(`Calendar deletion propagation failed for ${remote_domain}: ${remoteErr.message}`);
          }
        }
        await db.run('DELETE FROM calendar_event_outbound WHERE event_uid = ?', [event.uid]);
      } catch (propErr) {
        log.server.warn(`Calendar deletion propagation error: ${propErr.message}`);
      }
    }

    await db.run('DELETE FROM calendar_events WHERE id = ?', [req.params.id]);
    res.json({ message: 'Event deleted' });
  } catch (err) {
    log.server.error(`Calendar delete error: ${err.message}`);
    res.status(500).json({ error: 'Event could not be deleted' });
  }
});

// ── POST /api/calendar/events/:id/share — Invite local user to event ────────
router.post('/events/:id/share', 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 event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });
    if (user_id === req.user.id) return res.status(400).json({ error: 'Cannot share with yourself' });

    const existing = await db.get('SELECT id FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [event.id, user_id]);
    if (existing) return res.status(400).json({ error: 'Already shared with this user' });

    const pendingCheck = await db.get(
      "SELECT id FROM calendar_local_invitations WHERE event_id = ? AND to_user_id = ? AND status = 'pending'",
      [event.id, user_id]
    );
    if (pendingCheck) return res.status(400).json({ error: 'Invitation already pending for this user' });

    await db.run(
      'INSERT INTO calendar_local_invitations (event_id, from_user_id, to_user_id) VALUES (?, ?, ?)',
      [event.id, req.user.id, user_id]
    );

    // Send notification email (fire-and-forget)
    const targetUser = await db.get('SELECT name, display_name, email, language FROM users WHERE id = ?', [user_id]);
    if (targetUser?.email) {
      const appUrl = getBaseUrl(req);
      const inboxUrl = `${appUrl}/federation/inbox`;
      const appName = await getAppName();
      const senderUser = await db.get('SELECT name, display_name FROM users WHERE id = ?', [req.user.id]);
      const fromDisplay = (senderUser?.display_name && senderUser.display_name !== '') ? senderUser.display_name : (senderUser?.name || req.user.name);
      sendCalendarInviteEmail(
        targetUser.email,
        (targetUser.display_name && targetUser.display_name !== '') ? targetUser.display_name : targetUser.name,
        fromDisplay,
        event.title, event.start_time, event.end_time, event.description,
        inboxUrl, appName, targetUser.language || 'en'
      ).catch(mailErr => {
        log.server.warn('Calendar local share mail failed (non-fatal):', mailErr.message);
      });
    }

    const sharedUsers = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM calendar_event_shares ces
      JOIN users u ON ces.user_id = u.id
      WHERE ces.event_id = ?
    `, [event.id]);

    const pendingInvitations = await db.all(`
      SELECT cli.id, cli.to_user_id as user_id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM calendar_local_invitations cli
      JOIN users u ON cli.to_user_id = u.id
      WHERE cli.event_id = ? AND cli.status = 'pending'
    `, [event.id]);

    res.json({ sharedUsers, pendingInvitations });
  } catch (err) {
    log.server.error(`Calendar share error: ${err.message}`);
    res.status(500).json({ error: 'Could not share event' });
  }
});

// ── DELETE /api/calendar/events/:id/share/:userId — Remove share or cancel invitation ──
router.delete('/events/:id/share/:userId', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });

    // Remove accepted share
    await db.run('DELETE FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [event.id, parseInt(req.params.userId)]);

    // Also cancel any pending local invitation for this user
    await db.run(
      "UPDATE calendar_local_invitations SET status = 'declined' WHERE event_id = ? AND to_user_id = ? AND status = 'pending'",
      [event.id, parseInt(req.params.userId)]
    );

    const sharedUsers = await db.all(`
      SELECT u.id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM calendar_event_shares ces
      JOIN users u ON ces.user_id = u.id
      WHERE ces.event_id = ?
    `, [event.id]);

    const pendingInvitations = await db.all(`
      SELECT cli.id, cli.to_user_id as user_id, u.name, u.display_name, u.email, u.avatar_color, u.avatar_image
      FROM calendar_local_invitations cli
      JOIN users u ON cli.to_user_id = u.id
      WHERE cli.event_id = ? AND cli.status = 'pending'
    `, [event.id]);

    res.json({ sharedUsers, pendingInvitations });
  } catch (err) {
    log.server.error(`Calendar unshare error: ${err.message}`);
    res.status(500).json({ error: 'Could not remove share' });
  }
});

// ── GET /api/calendar/local-invitations — List local calendar invitations for current user ──
router.get('/local-invitations', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const invitations = await db.all(`
      SELECT
        cli.id, cli.event_id, cli.status, cli.created_at,
        ce.title, ce.start_time, ce.end_time, ce.description, ce.color,
        COALESCE(NULLIF(u.display_name,''), u.name) as from_name
      FROM calendar_local_invitations cli
      JOIN calendar_events ce ON cli.event_id = ce.id
      JOIN users u ON cli.from_user_id = u.id
      WHERE cli.to_user_id = ?
      ORDER BY cli.created_at DESC
    `, [req.user.id]);
    res.json({ invitations });
  } catch (err) {
    log.server.error(`Calendar local invitations error: ${err.message}`);
    res.status(500).json({ error: 'Could not load invitations' });
  }
});

// ── POST /api/calendar/local-invitations/:id/accept — Accept local invitation ──
router.post('/local-invitations/:id/accept', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const inv = await db.get(
      "SELECT * FROM calendar_local_invitations WHERE id = ? AND to_user_id = ? AND status = 'pending'",
      [req.params.id, req.user.id]
    );
    if (!inv) return res.status(404).json({ error: 'Invitation not found' });

    await db.run("UPDATE calendar_local_invitations SET status = 'accepted' WHERE id = ?", [inv.id]);
    // Insert into calendar_event_shares so the event appears in the user's calendar
    const existingShare = await db.get('SELECT id FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [inv.event_id, req.user.id]);
    if (!existingShare) {
      await db.run('INSERT INTO calendar_event_shares (event_id, user_id) VALUES (?, ?)', [inv.event_id, req.user.id]);
    }
    res.json({ message: 'Invitation accepted' });
  } catch (err) {
    log.server.error(`Calendar local invitation accept error: ${err.message}`);
    res.status(500).json({ error: 'Could not accept invitation' });
  }
});

// ── DELETE /api/calendar/local-invitations/:id — Decline/remove local invitation ──
router.delete('/local-invitations/:id', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const inv = await db.get(
      'SELECT * FROM calendar_local_invitations WHERE id = ? AND to_user_id = ?',
      [req.params.id, req.user.id]
    );
    if (!inv) return res.status(404).json({ error: 'Invitation not found' });

    if (inv.status === 'pending') {
      await db.run("UPDATE calendar_local_invitations SET status = 'declined' WHERE id = ?", [inv.id]);
    } else {
      // Accepted/declined - remove the share too if it was accepted
      if (inv.status === 'accepted') {
        await db.run('DELETE FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [inv.event_id, req.user.id]);
      }
      await db.run('DELETE FROM calendar_local_invitations WHERE id = ?', [inv.id]);
    }
    res.json({ message: 'Invitation removed' });
  } catch (err) {
    log.server.error(`Calendar local invitation delete error: ${err.message}`);
    res.status(500).json({ error: 'Could not remove invitation' });
  }
});

// ── GET /api/calendar/events/:id/ics — Download event as ICS ────────────────
router.get('/events/:id/ics', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const event = await db.get(`
      SELECT ce.*, COALESCE(NULLIF(u.display_name,''), u.name) as organizer_name, u.email as organizer_email
      FROM calendar_events ce
      JOIN users u ON ce.user_id = u.id
      WHERE ce.id = ?
    `, [req.params.id]);

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

    // Check access
    if (event.user_id !== req.user.id) {
      const share = await db.get('SELECT id FROM calendar_event_shares WHERE event_id = ? AND user_id = ?', [event.id, req.user.id]);
      if (!share) return res.status(403).json({ error: 'No permission' });
    }

    // Build room join URL if linked
    const baseUrl = getBaseUrl(req);
    let location = '';
    if (event.room_uid) {
      location = `${baseUrl}/join/${event.room_uid}`;
    }

    const ics = generateICS(event, location, baseUrl);

    res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
    res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(event.title)}.ics"`);
    res.send(ics);
  } catch (err) {
    log.server.error(`ICS download error: ${err.message}`);
    res.status(500).json({ error: 'Could not generate ICS file' });
  }
});

// ── POST /api/calendar/events/:id/federation — Send event to remote user ────
router.post('/events/:id/federation', authenticateToken, async (req, res) => {
  try {
    if (!isFederationEnabled()) {
      return res.status(400).json({ error: 'Federation is not configured on this instance' });
    }

    const { to } = req.body;
    if (!to) return res.status(400).json({ error: 'Remote address is required' });

    const { username, domain } = parseAddress(to);
    if (!domain) return res.status(400).json({ error: 'Remote address must be in format username@domain' });
    if (domain === getFederationDomain()) {
      return res.status(400).json({ error: 'Cannot send to your own instance. Use local sharing instead.' });
    }

    const db = getDb();
    const event = await db.get('SELECT * FROM calendar_events WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });

    const baseUrl = getBaseUrl(req);
    let joinUrl = null;
    if (event.room_uid) {
      joinUrl = `${baseUrl}/join/${event.room_uid}`;
    }

    const payload = {
      type: 'calendar_event',
      event_uid: event.uid,
      title: event.title,
      description: event.description || '',
      start_time: event.start_time,
      end_time: event.end_time,
      room_uid: event.room_uid || null,
      join_url: joinUrl,
      from_user: `@${req.user.name}@${getFederationDomain()}`,
      to_user: to,
      timestamp: new Date().toISOString(),
    };

    const signature = signPayload(payload);
    const { baseUrl: remoteApi } = await discoverInstance(domain);

    const response = await fetch(`${remoteApi}/calendar-event`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Federation-Signature': signature,
        'X-Federation-Origin': getFederationDomain(),
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(15_000),
    });

    if (!response.ok) {
      const data = await response.json().catch(() => ({}));
      throw new Error(data.error || `Remote server responded with ${response.status}`);
    }

    // Track outbound send for deletion propagation
    try {
      await db.run(
        `INSERT INTO calendar_event_outbound (event_uid, remote_domain) VALUES (?, ?)
         ON CONFLICT(event_uid, remote_domain) DO NOTHING`,
        [event.uid, domain]
      );
    } catch { /* table may not exist yet on upgrade */ }

    res.json({ success: true });
  } catch (err) {
    log.server.error(`Calendar federation send error: ${err.message}`);
    res.status(500).json({ error: err.message || 'Could not send event to remote instance' });
  }
});

// ── POST /api/calendar/events/:id/email — Send event invitation by email ────
router.post('/events/:id/email', authenticateToken, calendarEmailInviteLimiter, async (req, res) => {
  try {
    const { emails } = req.body;
    if (!emails || !Array.isArray(emails) || !emails.length) {
      return res.status(400).json({ error: 'At least one email address is required' });
    }
    if (emails.length > 50) {
      return res.status(400).json({ error: 'Maximum 50 email addresses allowed' });
    }

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

    const db = getDb();
    const event = await db.get(`
      SELECT ce.*, COALESCE(NULLIF(u.display_name,''), u.name) as organizer_name, u.email as organizer_email
      FROM calendar_events ce
      JOIN users u ON ce.user_id = u.id
      WHERE ce.id = ? AND ce.user_id = ?
    `, [req.params.id, req.user.id]);
    if (!event) return res.status(404).json({ error: 'Event not found or no permission' });

    const baseUrl = getBaseUrl(req);
    let joinUrl = null;
    if (event.room_uid) {
      joinUrl = `${baseUrl}/join/${event.room_uid}`;
    }

    const ics = generateICS(event, joinUrl || '', baseUrl);
    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 =>
        sendCalendarEventEmail(
          email, fromUser, event.title, event.start_time, event.end_time,
          event.description, joinUrl, ics, 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.server.warn(`${failed.length}/${emails.length} calendar email invitations failed`);
    }

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

// ── POST /receive-event or /calendar-event — Receive calendar event from remote ──
// '/receive-event' when mounted at /api/calendar
// '/calendar-event' when mounted at /api/federation (for remote instance discovery)
router.post(['/receive-event', '/calendar-event'], calendarFederationLimiter, async (req, res) => {
  try {
    if (!isFederationEnabled()) {
      return res.status(400).json({ error: 'Federation is not configured on this instance' });
    }

    const signature = req.headers['x-federation-signature'];
    const payload = req.body || {};

    if (!signature) return res.status(401).json({ error: 'Missing federation signature' });

    const { event_uid, title, description, start_time, end_time, room_uid, join_url, from_user, to_user } = payload;

    if (!event_uid || !title || !start_time || !end_time || !from_user || !to_user) {
      return res.status(400).json({ error: 'Incomplete event payload' });
    }

    // Validate lengths
    if (event_uid.length > 100 || title.length > 200 || (description && description.length > 5000) ||
        from_user.length > 200 || to_user.length > 200 || (join_url && join_url.length > 2000)) {
      return res.status(400).json({ error: 'Payload fields exceed maximum allowed length' });
    }

    // Verify signature
    const { domain: senderDomain } = parseAddress(from_user);
    if (!senderDomain) return res.status(400).json({ error: 'Sender address must include a domain' });

    const { publicKey } = await discoverInstance(senderDomain);
    if (!publicKey) return res.status(400).json({ error: 'Sender instance did not provide a public key' });
    if (!verifyPayload(payload, signature, publicKey)) {
      return res.status(403).json({ error: 'Invalid federation signature' });
    }

    // Find local user
    const { username } = parseAddress(to_user);
    const db = getDb();
    const targetUser = await db.get('SELECT id, name, email, language FROM users WHERE LOWER(name) = LOWER(?)', [username]);
    if (!targetUser) return res.status(404).json({ error: 'User not found on this instance' });

    // Check duplicate (already in invitations or already accepted into calendar)
    const existingInv = await db.get('SELECT id FROM calendar_invitations WHERE event_uid = ? AND to_user_id = ?', [event_uid, targetUser.id]);
    if (existingInv) return res.json({ success: true, message: 'Calendar invitation already received' });

    // Store as pending invitation — user must accept before it appears in calendar
    await db.run(`
      INSERT INTO calendar_invitations (event_uid, title, description, start_time, end_time, room_uid, join_url, from_user, to_user_id, color)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `, [
      event_uid,
      title,
      description || null,
      start_time,
      end_time,
      room_uid || null,
      join_url || null,
      from_user,
      targetUser.id,
      '#6366f1',
    ]);

    // Send notification email (fire-and-forget)
    if (targetUser.email) {
      const appUrl = getBaseUrl(req);
      const inboxUrl = `${appUrl}/federation/inbox`;
      const appName = await getAppName();
      sendCalendarInviteEmail(
        targetUser.email, targetUser.name, from_user,
        title, start_time, end_time, description || null,
        inboxUrl, appName, targetUser.language || 'en'
      ).catch(mailErr => {
        log.server.warn('Calendar invite mail failed (non-fatal):', mailErr.message);
      });
    }

    res.json({ success: true });
  } catch (err) {
    log.server.error(`Calendar federation receive error: ${err.message}`);
    res.status(500).json({ error: 'Failed to process calendar event' });
  }
});

// ── Helper: Generate ICS content ────────────────────────────────────────────
function generateICS(event, location, prodIdDomain) {
  const formatDate = (dateStr) => {
    const d = new Date(dateStr);
    return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
  };

  const escapeICS = (str) => {
    if (!str) return '';
    return str.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
  };

  const now = formatDate(new Date().toISOString());
  const dtStart = formatDate(event.start_time);
  const dtEnd = formatDate(event.end_time);

  let ics = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    `PRODID:-//${prodIdDomain}//Redlight Calendar//EN`,
    'CALSCALE:GREGORIAN',
    'METHOD:PUBLISH',
    'BEGIN:VEVENT',
    `UID:${event.uid}@${prodIdDomain}`,
    `DTSTAMP:${now}`,
    `DTSTART:${dtStart}`,
    `DTEND:${dtEnd}`,
    `SUMMARY:${escapeICS(event.title)}`,
  ];

  if (event.description) {
    ics.push(`DESCRIPTION:${escapeICS(event.description)}`);
  }
  if (location) {
    ics.push(`LOCATION:${escapeICS(location)}`);
    ics.push(`URL:${location}`);
  }
  if (event.organizer_name && event.organizer_email) {
    ics.push(`ORGANIZER;CN=${escapeICS(event.organizer_name)}:mailto:${event.organizer_email}`);
  }

  if (event.reminder_minutes) {
    ics.push(
      'BEGIN:VALARM',
      'ACTION:DISPLAY',
      `DESCRIPTION:Reminder: ${escapeICS(event.title)}`,
      `TRIGGER:-PT${event.reminder_minutes}M`,
      'END:VALARM',
    );
  }
  ics.push('END:VEVENT', 'END:VCALENDAR');
  return ics.join('\r\n');
}

// ── CalDAV token management ────────────────────────────────────────────────

// GET /api/calendar/caldav-tokens
router.get('/caldav-tokens', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const tokens = await db.all(
      'SELECT id, name, created_at, last_used_at FROM caldav_tokens WHERE user_id = ? ORDER BY created_at DESC',
      [req.user.id],
    );
    res.json({ tokens });
  } catch (err) {
    log.server.error(`CalDAV list tokens error: ${err.message}`);
    res.status(500).json({ error: 'Could not load tokens' });
  }
});

// POST /api/calendar/caldav-tokens
router.post('/caldav-tokens', authenticateToken, async (req, res) => {
  try {
    const { name } = req.body;
    if (!name || !name.trim()) {
      return res.status(400).json({ error: 'Token name is required' });
    }
    const db = getDb();
    const count = await db.get(
      'SELECT COUNT(*) as c FROM caldav_tokens WHERE user_id = ?',
      [req.user.id],
    );
    if (count.c >= 10) {
      return res.status(400).json({ error: 'Maximum of 10 tokens allowed' });
    }
    const token = crypto.randomBytes(32).toString('hex');
    const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
    const result = await db.run(
      // Store only the hash — never the plaintext — to limit exposure on DB breach.
      'INSERT INTO caldav_tokens (user_id, token_hash, name) VALUES (?, ?, ?)',
      [req.user.id, tokenHash, name.trim()],
    );
    res.status(201).json({
      token: { id: result.lastInsertRowid, name: name.trim() },
      plainToken: token,
    });
  } catch (err) {
    log.server.error(`CalDAV create token error: ${err.message}`);
    res.status(500).json({ error: 'Could not create token' });
  }
});

// DELETE /api/calendar/caldav-tokens/:id
router.delete('/caldav-tokens/:id', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const result = await db.run(
      'DELETE FROM caldav_tokens WHERE id = ? AND user_id = ?',
      [req.params.id, req.user.id],
    );
    if (result.changes === 0) {
      return res.status(404).json({ error: 'Token not found' });
    }
    res.json({ ok: true });
  } catch (err) {
    log.server.error(`CalDAV delete token error: ${err.message}`);
    res.status(500).json({ error: 'Could not delete token' });
  }
});

export default router;
