/**
 * CalDAV server for Redlight
 *
 * Supports PROPFIND, REPORT, GET, PUT, DELETE, OPTIONS — enough for
 * Thunderbird/Lightning, Apple Calendar, GNOME Calendar and DAVx⁵ (Android).
 *
 * Authentication: HTTP Basic Auth  →  email:caldav_token
 * Token management: POST/GET/DELETE /api/calendar/caldav-tokens
 *
 * Mounted at: /caldav
 */

import { Router, text } from 'express';
import crypto from 'crypto';
import { rateLimit } from 'express-rate-limit';
import { getDb } from '../config/database.js';
import { log } from '../config/logger.js';
import { getBaseUrl } from '../middleware/auth.js';

const router = Router();

// Brute-force protection for the Basic-Auth tokens: only failed requests count
// (skipSuccessfulRequests), so legitimate sync clients polling frequently are
// not throttled.
router.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 30,
  skipSuccessfulRequests: true,
  standardHeaders: true,
  legacyHeaders: false,
  message: 'Too many failed CalDAV requests. Please try again later.',
}));

// ── Body parsing for XML and iCalendar payloads ────────────────────────────
router.use(text({ type: ['application/xml', 'text/xml', 'text/calendar', 'application/octet-stream'] }));

// ── Helpers ────────────────────────────────────────────────────────────────

function xmlHeader() {
  return '<?xml version="1.0" encoding="UTF-8"?>';
}

function escapeXml(str) {
  return String(str || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function escapeICS(str) {
  return String(str || '')
    .replace(/\\/g, '\\\\')
    .replace(/;/g, '\\;')
    .replace(/,/g, '\\,')
    .replace(/\n/g, '\\n');
}

function foldICSLine(line) {
  // RFC 5545: fold lines longer than 75 octets
  const bytes = Buffer.from(line, 'utf8');
  if (bytes.length <= 75) return line;
  const chunks = [];
  let offset = 0;
  let first = true;
  while (offset < bytes.length) {
    const chunk = first ? bytes.slice(0, 75) : bytes.slice(offset, offset + 74);
    chunks.push((first ? '' : ' ') + chunk.toString('utf8'));
    offset += first ? 75 : 74;
    first = false;
  }
  return chunks.join('\r\n');
}

function toICSDate(dateStr) {
  const d = new Date(dateStr);
  return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
}

function parseICSDate(str) {
  if (!str) return null;
  // Strip TZID= prefix if present
  const raw = str.includes(':') ? str.split(':').pop() : str;
  if (raw.length === 8) {
    // All-day: YYYYMMDD
    return new Date(`${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}T00:00:00Z`);
  }
  return new Date(
    `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}T` +
    `${raw.slice(9, 11)}:${raw.slice(11, 13)}:${raw.slice(13, 15)}` +
    (raw.endsWith('Z') ? 'Z' : 'Z'),
  );
}

function getICSProp(ics, key) {
  const re = new RegExp(`^${key}(?:;[^:]*)?:(.+)$`, 'im');
  const m = ics.match(re);
  if (!m) return null;
  // Unfold: join continuation lines
  let v = m[1];
  const unfoldRe = /\r?\n[ \t](.+)/g;
  v = v.replace(unfoldRe, '$1');
  return v.trim();
}

function eventToICS(event, base, user) {
  // Determine the most useful join URL
  const joinUrl = event.federated_join_url
    || (event.room_uid ? `${base}/join/${event.room_uid}` : null);
  const roomUrl = event.room_uid ? `${base}/rooms/${event.room_uid}` : null;

  const lines = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Redlight//CalDAV//EN',
    'CALSCALE:GREGORIAN',
    'BEGIN:VEVENT',
    `UID:${event.uid}`,
    `SUMMARY:${escapeICS(event.title)}`,
    `DTSTART:${toICSDate(event.start_time)}`,
    `DTEND:${toICSDate(event.end_time)}`,
    `DTSTAMP:${toICSDate(event.updated_at || event.created_at)}`,
    `LAST-MODIFIED:${toICSDate(event.updated_at || event.created_at)}`,
  ];

  // LOCATION: show join link so calendar apps display "where" the meeting is
  if (joinUrl) {
    lines.push(`LOCATION:${escapeICS(joinUrl)}`);
    lines.push(`URL:${joinUrl}`);
  } else if (roomUrl) {
    lines.push(`LOCATION:${escapeICS(roomUrl)}`);
    lines.push(`URL:${roomUrl}`);
  }

  // DESCRIPTION: combine user description + join link hint
  const descParts = [];
  if (event.description) descParts.push(event.description);
  if (joinUrl) {
    descParts.push(`Join meeting: ${joinUrl}`);
  }
  if (roomUrl && roomUrl !== joinUrl) {
    descParts.push(`Room page: ${roomUrl}`);
  }
  if (descParts.length > 0) {
    lines.push(`DESCRIPTION:${escapeICS(descParts.join('\n'))}`);
  }

  // ORGANIZER
  if (user) {
    const cn = user.display_name || user.name || user.email;
    lines.push(`ORGANIZER;CN=${escapeICS(cn)}:mailto:${user.email}`);
  }

  if (event.room_uid) {
    lines.push(`X-REDLIGHT-ROOM-UID:${event.room_uid}`);
  }
  if (joinUrl) {
    lines.push(`X-REDLIGHT-JOIN-URL:${escapeICS(joinUrl)}`);
  }
  if (event.reminder_minutes) {
    lines.push(
      'BEGIN:VALARM',
      'ACTION:DISPLAY',
      `DESCRIPTION:${escapeICS(event.title)}`,
      `TRIGGER:-PT${event.reminder_minutes}M`,
      'END:VALARM',
    );
  }
  lines.push('END:VEVENT', 'END:VCALENDAR');
  return lines.map(foldICSLine).join('\r\n');
}

function parseICSBody(body) {
  const uid = getICSProp(body, 'UID');
  const summary = (getICSProp(body, 'SUMMARY') || '')
    .replace(/\\n/g, '\n').replace(/\\,/g, ',').replace(/\\;/g, ';');
  const description = (getICSProp(body, 'DESCRIPTION') || '')
    .replace(/\\n/g, '\n').replace(/\\,/g, ',').replace(/\\;/g, ';');
  const dtstart = getICSProp(body, 'DTSTART');
  const dtend = getICSProp(body, 'DTEND');
  return {
    uid: uid || null,
    title: summary || 'Untitled',
    description: description || null,
    start_time: parseICSDate(dtstart),
    end_time: parseICSDate(dtend),
  };
}

// Build etag from updated_at
function etag(event) {
  return `"${Buffer.from(event.updated_at || event.created_at).toString('base64')}"`;
}

// ── CalDAV authentication middleware ───────────────────────────────────────
async function caldavAuth(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
    return res.status(401).end();
  }
  try {
    const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
    const colonIdx = decoded.indexOf(':');
    if (colonIdx === -1) throw new Error('no colon');
    const email = decoded.slice(0, colonIdx);
    const token = decoded.slice(colonIdx + 1);

    const db = getDb();
    // Emails are stored lowercased
    const user = await db.get(
      'SELECT id, name, display_name, email FROM users WHERE email = ?',
      [email.toLowerCase()],
    );
    if (!user) {
      res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
      return res.status(401).end();
    }
    // Hash the provided token with SHA-256 for constant-time comparison in SQL
    const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
    const tokenRow = await db.get(
      'SELECT * FROM caldav_tokens WHERE user_id = ? AND token_hash = ?',
      [user.id, tokenHash],
    );
    // Fallback: also check legacy plaintext tokens for backward compatibility
    const tokenRowLegacy = !tokenRow ? await db.get(
      'SELECT * FROM caldav_tokens WHERE user_id = ? AND token = ?',
      [user.id, token],
    ) : null;
    const matchedToken = tokenRow || tokenRowLegacy;
    if (!matchedToken) {
      res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
      return res.status(401).end();
    }
    // Migrate legacy plaintext token to hashed version
    if (tokenRowLegacy && !tokenRow) {
      db.run("UPDATE caldav_tokens SET token_hash = ?, token = '' WHERE id = ?", [tokenHash, matchedToken.id]).catch(() => {});
    }
    // Update last_used_at (fire and forget)
    db.run('UPDATE caldav_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?', [matchedToken.id]).catch(() => {});
    req.caldavUser = user;
    next();
  } catch (err) {
    log.server.error(`CalDAV auth error: ${err.message}`);
    res.set('WWW-Authenticate', 'Basic realm="Redlight CalDAV"');
    return res.status(401).end();
  }
}

// ── Common response headers ────────────────────────────────────────────────
function setDAVHeaders(res) {
  res.set('DAV', '1, 2, calendar-access');
  res.set('Allow', 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, REPORT');
  res.set('MS-Author-Via', 'DAV');
}

// ── CalDAV username authorization ──────────────────────────────────────────
// Ensures the :username param matches the authenticated user's email
function validateCalDAVUser(req, res, next) {
  if (req.params.username && decodeURIComponent(req.params.username).toLowerCase() !== req.caldavUser.email) {
    return res.status(403).end();
  }
  next();
}

// ── Base URL helper (uses shared getBaseUrl from auth.js) ──────────────────
const baseUrl = getBaseUrl;

// ── PROPFIND response builders ─────────────────────────────────────────────

function multistatus(responses) {
  return `${xmlHeader()}
<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"
               xmlns:c="urn:ietf:params:xml:ns:caldav">
${responses.join('\n')}
</d:multistatus>`;
}

function propResponse(href, props, status = '200 OK') {
  const propXml = Object.entries(props)
    .map(([k, v]) => `    <${k}>${v}</${k}>`)
    .join('\n');
  return `  <d:response>
    <d:href>${escapeXml(href)}</d:href>
    <d:propstat>
      <d:prop>
${propXml}
      </d:prop>
      <d:status>HTTP/1.1 ${status}</d:status>
    </d:propstat>
  </d:response>`;
}

// ── OPTIONS ────────────────────────────────────────────────────────────────
router.options('/*splat', (req, res) => {
  setDAVHeaders(res);
  res.status(200).end();
});

// ── PROPFIND / ─────────────────────────────────────────────────────────────
// Service discovery root: tells the client where the user principal lives.
router.all('/', caldavAuth, async (req, res) => {
  if (req.method !== 'PROPFIND') {
    setDAVHeaders(res);
    return res.status(405).end();
  }
  const base = baseUrl(req);
  const principalHref = `/caldav/${encodeURIComponent(req.caldavUser.email)}/`;
  setDAVHeaders(res);
  res.status(207).type('application/xml; charset=utf-8').send(
    multistatus([
      propResponse('/caldav/', {
        'd:current-user-principal': `<d:href>${principalHref}</d:href>`,
        'd:principal-URL': `<d:href>${principalHref}</d:href>`,
        'd:resourcetype': '<d:collection/>',
        'd:displayname': 'Redlight CalDAV',
      }),
    ]),
  );
});

// ── PROPFIND /{username}/ ──────────────────────────────────────────────────
// User principal: tells the client where the calendar home is.
router.all('/:username/', caldavAuth, validateCalDAVUser, async (req, res) => {
  if (req.method !== 'PROPFIND') {
    setDAVHeaders(res);
    return res.status(405).end();
  }
  const principalHref = `/caldav/${encodeURIComponent(req.caldavUser.email)}/`;
  const calendarHref = `/caldav/${encodeURIComponent(req.caldavUser.email)}/calendar/`;
  setDAVHeaders(res);
  res.status(207).type('application/xml; charset=utf-8').send(
    multistatus([
      propResponse(principalHref, {
        'd:resourcetype': '<d:collection/><d:principal/>',
        'd:displayname': escapeXml(req.caldavUser.display_name || req.caldavUser.name),
        'd:principal-URL': `<d:href>${principalHref}</d:href>`,
        'c:calendar-home-set': `<d:href>${calendarHref}</d:href>`,
        'd:current-user-principal': `<d:href>${principalHref}</d:href>`,
      }),
    ]),
  );
});

// ── PROPFIND + REPORT /{username}/calendar/ ────────────────────────────────
router.all('/:username/calendar/', caldavAuth, validateCalDAVUser, async (req, res) => {
  const db = getDb();
  const calendarHref = `/caldav/${encodeURIComponent(req.caldavUser.email)}/calendar/`;

  // PROPFIND: return calendar collection metadata
  if (req.method === 'PROPFIND') {
    const latestEvent = await db.get(
      'SELECT updated_at, created_at FROM calendar_events WHERE user_id = ? ORDER BY COALESCE(updated_at, created_at) DESC LIMIT 1',
      [req.caldavUser.id],
    );
    const ctag = latestEvent
      ? Buffer.from(String(latestEvent.updated_at || latestEvent.created_at)).toString('base64')
      : '0';

    // If Depth: 1, also return all event hrefs
    const depth = req.headers.depth || '0';
    const responses = [
      propResponse(calendarHref, {
        'd:resourcetype': '<d:collection/><c:calendar/>',
        'd:displayname': 'Redlight Calendar',
        'c:supported-calendar-component-set': '<c:comp name="VEVENT"/>',
        'cs:getctag': escapeXml(ctag),
        'd:sync-token': escapeXml(ctag),
      }),
    ];

    if (depth === '1') {
      const events = await db.all(
        'SELECT uid, updated_at, created_at FROM calendar_events WHERE user_id = ?',
        [req.caldavUser.id],
      );
      for (const ev of events) {
        responses.push(
          propResponse(`${calendarHref}${ev.uid}.ics`, {
            'd:resourcetype': '',
            'd:getcontenttype': 'text/calendar; charset=utf-8',
            'd:getetag': escapeXml(etag(ev)),
          }),
        );
      }
    }

    setDAVHeaders(res);
    return res.status(207).type('application/xml; charset=utf-8').send(multistatus(responses));
  }

  // REPORT: calendar-query or calendar-multiget
  if (req.method === 'REPORT') {
    const body = typeof req.body === 'string' ? req.body : '';

    // calendar-multiget: client sends explicit hrefs
    if (body.includes('calendar-multiget')) {
      const hrefMatches = [...body.matchAll(/<[^:>]*:?href[^>]*>([^<]+)<\//gi)];
      const uids = hrefMatches
        .map(m => m[1].trim())
        .filter(h => h.endsWith('.ics'))
        .map(h => h.split('/').pop().replace('.ics', ''));

      const responses = [];
      for (const uid of uids) {
        const ev = await db.get(
          'SELECT * FROM calendar_events WHERE uid = ? AND user_id = ?',
          [uid, req.caldavUser.id],
        );
        if (!ev) continue;
        const ics = eventToICS(ev);
        responses.push(
          propResponse(`${calendarHref}${ev.uid}.ics`, {
            'd:getetag': escapeXml(etag(ev)),
            'c:calendar-data': escapeXml(eventToICS(ev, baseUrl(req), req.caldavUser)),
          }),
        );
      }
      setDAVHeaders(res);
      return res.status(207).type('application/xml; charset=utf-8').send(multistatus(responses));
    }

    // calendar-query: filter by time range
    let sql = 'SELECT * FROM calendar_events WHERE user_id = ?';
    const params = [req.caldavUser.id];

    const startMatch = body.match(/start="([^"]+)"/i);
    const endMatch = body.match(/end="([^"]+)"/i);
    if (startMatch) {
      const startIso = parseICSDate(startMatch[1])?.toISOString();
      if (startIso) { sql += ' AND end_time >= ?'; params.push(startIso); }
    }
    if (endMatch) {
      const endIso = parseICSDate(endMatch[1])?.toISOString();
      if (endIso) { sql += ' AND start_time <= ?'; params.push(endIso); }
    }
    sql += ' ORDER BY start_time ASC';

    const events = await db.all(sql, params);
    const responses = events.map(ev =>
      propResponse(`${calendarHref}${ev.uid}.ics`, {
        'd:getetag': escapeXml(etag(ev)),
        'c:calendar-data': escapeXml(eventToICS(ev, baseUrl(req), req.caldavUser)),
      }),
    );
    setDAVHeaders(res);
    return res.status(207).type('application/xml; charset=utf-8').send(multistatus(responses));
  }

  setDAVHeaders(res);
  res.status(405).end();
});

// ── GET /{username}/calendar/{uid}.ics ────────────────────────────────────
router.get('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
  const uid = req.params.filename.replace(/\.ics$/, '');
  const db = getDb();
  const ev = await db.get(
    'SELECT * FROM calendar_events WHERE uid = ? AND user_id = ?',
    [uid, req.caldavUser.id],
  );
  if (!ev) return res.status(404).end();
  setDAVHeaders(res);
  res.set('ETag', etag(ev));
  res.set('Content-Type', 'text/calendar; charset=utf-8');
  res.send(eventToICS(ev, baseUrl(req), req.caldavUser));
});

// ── PUT /{username}/calendar/{uid}.ics — create or update ─────────────────
router.put('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
  const uid = req.params.filename.replace(/\.ics$/, '');
  const body = typeof req.body === 'string' ? req.body : '';

  if (!body) return res.status(400).end();

  const parsed = parseICSBody(body);
  if (!parsed.start_time || !parsed.end_time) return res.status(400).end();

  // Normalize UID: prefer from ICS, fall back to filename
  const eventUid = parsed.uid || uid;

  const db = getDb();
  const existing = await db.get(
    'SELECT * FROM calendar_events WHERE uid = ? AND user_id = ?',
    [eventUid, req.caldavUser.id],
  );

  try {
    if (existing) {
      await db.run(
        `UPDATE calendar_events SET title = ?, description = ?, start_time = ?, end_time = ?,
         updated_at = CURRENT_TIMESTAMP WHERE uid = ? AND user_id = ?`,
        [
          parsed.title,
          parsed.description,
          parsed.start_time.toISOString(),
          parsed.end_time.toISOString(),
          eventUid,
          req.caldavUser.id,
        ],
      );
      const updated = await db.get('SELECT * FROM calendar_events WHERE uid = ?', [eventUid]);
      setDAVHeaders(res);
      res.set('ETag', etag(updated));
      return res.status(204).end();
    } else {
      await db.run(
        `INSERT INTO calendar_events (uid, title, description, start_time, end_time, user_id, color)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [
          eventUid,
          parsed.title,
          parsed.description,
          parsed.start_time.toISOString(),
          parsed.end_time.toISOString(),
          req.caldavUser.id,
          '#6366f1',
        ],
      );
      const created = await db.get('SELECT * FROM calendar_events WHERE uid = ?', [eventUid]);
      setDAVHeaders(res);
      res.set('ETag', etag(created));
      return res.status(201).end();
    }
  } catch (err) {
    log.server.error(`CalDAV PUT error: ${err.message}`);
    return res.status(500).end();
  }
});

// ── DELETE /{username}/calendar/{uid}.ics ─────────────────────────────────
router.delete('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, async (req, res) => {
  const uid = req.params.filename.replace(/\.ics$/, '');
  const db = getDb();
  const ev = await db.get(
    'SELECT * FROM calendar_events WHERE uid = ? AND user_id = ?',
    [uid, req.caldavUser.id],
  );
  if (!ev) return res.status(404).end();
  await db.run('DELETE FROM calendar_events WHERE uid = ? AND user_id = ?', [uid, req.caldavUser.id]);
  setDAVHeaders(res);
  res.status(204).end();
});

// ── Fallback ───────────────────────────────────────────────────────────────
router.all('/*splat', caldavAuth, (req, res) => {
  setDAVHeaders(res);
  res.status(405).end();
});

export default router;
